diff --git a/README.md b/README.md index 17c70fd..5d1d4a4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ Console app for serial connection. * `COM[N]` is mandatory to specify serial port 4. Operate target device via the console 5. Press F1 to leave its serial session and to finish SimpleCom + * Press CTRL+C in batch mode + +> [!IMPORTANT] +> You have to use batch mode (`--batch`) if you want to redirect something into SimpleCom. Batch mode would not terminate automatically when stdin reaches EOF. So you have to type CTRL+C if you want terminate SimpleCom. ## Command line options @@ -38,6 +42,7 @@ Console app for serial connection. | `--auto-reconnect-timeout [num]` | 120 | Reconnect timeout | | `--log-file [logfile]` | <none> | Log serial communication to file | | `--stdin-logging` | false | Enable stdin logging

⚠️Possible to be logged each chars duplicately due to echo back from the console when this option is set, and also secrets (e.g. passphrase) typed into the console will be logged even if it is not shown on the console. | +| `--batch` | false | Perform in batch mode

⚠️You have to set serial port in command line arguments, and you cannot set with `--show-dialog`, `--tty-resizer`, `--auto-reconnect`, `--log-file`. | | `--help` | - | Show help message | # How to build @@ -69,7 +74,8 @@ Please see [Applications installed from the web](https://docs.microsoft.com/ja-j # Notes * SimpleCom sends / receives VT100 escape sequences. So the serial device to connect via SimpleCom needs to support VT100 or compatible shell. -* F1 key is hooked by SimpleCom, so escase sequence of F1 (`ESC O P`) would not be propagated. +* F1 key is hooked by SimpleCom (in interactive mode (default)), so escape sequence of F1 (`ESC O P`) would not be propagated. + * In batch mode, F1 would propergate to peripheral. * SimpleCom supports ANSI chars only, so it would not work if multibyte chars (e.g. CJK chars) are given. * Run [resize](https://linux.die.net/man/1/resize) provided by xterm if you want to align VT size of Linux box with your console window. diff --git a/SimpleCom/BatchRedirector.cpp b/SimpleCom/BatchRedirector.cpp new file mode 100644 index 0000000..82c2023 --- /dev/null +++ b/SimpleCom/BatchRedirector.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025, Yasumasa Suenaga + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ +#include "stdafx.h" +#include "BatchRedirector.h" + +static constexpr int buf_sz = 256; + +static void RedirectHandle(HANDLE hSource, HANDLE hDest) { + char buf[buf_sz]; + DWORD nBytesRead; + while (ReadFile(hSource, buf, sizeof(buf), &nBytesRead, NULL)) { + DWORD nBytesWritten; + DWORD nBytesRemain = nBytesRead; + while (nBytesRemain > 0) { + if (WriteFile(hDest, &buf[nBytesRead - nBytesRemain], nBytesRemain, &nBytesWritten, nullptr)) { + nBytesRemain -= nBytesWritten; + } + } + } +} + +DWORD WINAPI BatchStdInRedirector(_In_ LPVOID lpParameter) { + HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); + if (hStdIn == INVALID_HANDLE_VALUE) { + std::cerr << "Error: Could not get STDIN handle." << std::endl; + return -1; + } + + // Disable line input mode to read raw input from console. (for batch mode without file redirection) + DWORD mode; + GetConsoleMode(hStdIn, &mode); + mode &= ~ENABLE_LINE_INPUT; + SetConsoleMode(hStdIn, mode); + + HANDLE hSerial = reinterpret_cast(lpParameter); + RedirectHandle(hStdIn, hSerial); + return 0; +} + +DWORD WINAPI BatchStdOutRedirector(_In_ LPVOID lpParameter) { + HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hStdOut == INVALID_HANDLE_VALUE) { + std::cerr << "Error: Could not get STDOUT handle." << std::endl; + return -1; + } + + HANDLE hSerial = reinterpret_cast(lpParameter); + RedirectHandle(hSerial, hStdOut); + return 0; +} + +std::tuple SimpleCom::BatchRedirector::GetStdInRedirector() { + return { &BatchStdInRedirector, _hSerial }; +} + +std::tuple SimpleCom::BatchRedirector::GetStdOutRedirector() { + return { &BatchStdOutRedirector, _hSerial }; +} \ No newline at end of file diff --git a/SimpleCom/BatchRedirector.h b/SimpleCom/BatchRedirector.h new file mode 100644 index 0000000..89d39f2 --- /dev/null +++ b/SimpleCom/BatchRedirector.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025, Yasumasa Suenaga + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ +#pragma once +#include "stdafx.h" +#include "TerminalRedirectorBase.h" + +namespace SimpleCom { + + class BatchRedirector : + public TerminalRedirectorBase + { + protected: + virtual std::tuple GetStdInRedirector() override; + virtual std::tuple GetStdOutRedirector() override; + + public: + BatchRedirector(HANDLE hSerial) : TerminalRedirectorBase(hSerial) {}; + virtual ~BatchRedirector() {}; + }; + +} + diff --git a/SimpleCom/SerialConnection.cpp b/SimpleCom/SerialConnection.cpp index 2dcd2e9..58d80e5 100644 --- a/SimpleCom/SerialConnection.cpp +++ b/SimpleCom/SerialConnection.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023, 2024, Yasumasa Suenaga + * Copyright (C) 2023, 2025, Yasumasa Suenaga * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -19,42 +19,21 @@ #include "stdafx.h" #include "SerialConnection.h" #include "util.h" +#include "TerminalRedirector.h" +#include "BatchRedirector.h" #include "debug.h" #include "../common/common.h" static constexpr int buf_sz = 256; -typedef struct { - HANDLE hSerial; - OVERLAPPED *overlapped; - HANDLE hTermEvent; - SimpleCom::SerialConnection *conn; -} TStdOutRedirectorParam; -static COORD current_window_sz = { 0 }; - -SimpleCom::SerialConnection::SerialConnection(TString& device, DCB* dcb, HWND hwnd, HANDLE hStdIn, HANDLE hStdOut, bool useTTYResizer, LPCTSTR logfilename, bool enableStdinLogging) : +SimpleCom::SerialConnection::SerialConnection(TString& device, DCB* dcb, LPCTSTR logfilename, bool enableStdinLogging) : _device(device), - _parent_hwnd(hwnd), - _hStdIn(hStdIn), - _hStdOut(hStdOut), - _useTTYResizer(useTTYResizer), - _enableStdinLogging(enableStdinLogging), - _exception_queue() + _enableStdinLogging(enableStdinLogging) { CopyMemory(&_dcb, dcb, sizeof(_dcb)); _logwriter = (logfilename == nullptr) ? nullptr : new LogWriter(logfilename); - - if (_enableStdinLogging && (_logwriter == nullptr)) { - throw _T("Logger was not set up in spite of stdin logging was enabled."); - } -} - -SimpleCom::SerialConnection::~SerialConnection(){ - if (_logwriter != nullptr) { - delete _logwriter; - } } void SimpleCom::SerialConnection::InitSerialPort(const HANDLE hSerial) { @@ -77,245 +56,23 @@ void SimpleCom::SerialConnection::InitSerialPort(const HANDLE hSerial) { CALL_WINAPI_WITH_DEBUGLOG(SetCommTimeouts(hSerial, &comm_timeouts), TRUE, __FILE__, __LINE__) } -/* - * Ask user whether terminate current serial session via dialog box. - * Return true if session should be closed (active close). - */ -bool SimpleCom::SerialConnection::ShouldTerminate(SerialPortWriter& writer, const HANDLE hTermEvent) { - // Write all keys in the buffer before F1 - writer.WriteAsync(); - - if (MessageBox(_parent_hwnd, _T("Do you want to leave from this serial session?"), _T("SimpleCom"), MB_YESNO | MB_ICONQUESTION) == IDYES) { - SetEvent(hTermEvent); - return true; - } - - return false; -} - -/* - * Write key code in KEY_EVENT_RECORD to send buffer. - */ -void SimpleCom::SerialConnection::ProcessKeyEvents(const KEY_EVENT_RECORD keyevent, SerialPortWriter& writer) { - - if (keyevent.wVirtualKeyCode == VK_F1) { - // F1 key should not be propagated to peripheral. - return; - } - - if (keyevent.bKeyDown && (keyevent.uChar.AsciiChar != '\0')) { - for (int send_idx = 0; send_idx < keyevent.wRepeatCount; send_idx++) { - writer.Put(keyevent.uChar.AsciiChar); - if (_enableStdinLogging) { - _logwriter->Write(keyevent.uChar.AsciiChar); - } - } - } -} - -static void StdOutRedirectorLoopInner(const HANDLE hSerial, OVERLAPPED *overlapped, const HANDLE hStdOut, SimpleCom::LogWriter* logwriter) { - DWORD event_mask = 0; - if (!WaitCommEvent(hSerial, &event_mask, overlapped)) { - if (GetLastError() == ERROR_IO_PENDING) { - DWORD unused = 0; - if (!GetOverlappedResult(hSerial, overlapped, &unused, TRUE)) { - throw SimpleCom::SerialAPIException(GetLastError(), _T("GetOverlappedResult for WaitCommEvent")); - } - } - else { - throw SimpleCom::SerialAPIException(GetLastError(), _T("WaitCommEvent")); - } - } - - if (event_mask & EV_RXCHAR) { - DWORD errors; - COMSTAT comstat = { 0 }; - if (!ClearCommError(hSerial, &errors, &comstat)) { - throw SimpleCom::SerialAPIException(GetLastError(), _T("ClearCommError")); - } - - char buf[buf_sz] = { 0 }; - DWORD nBytesRead = 0; - DWORD remainBytes = comstat.cbInQue; - while (remainBytes > 0) { - - if (!ReadFile(hSerial, buf, min(buf_sz, remainBytes), &nBytesRead, overlapped)) { - if (GetLastError() == ERROR_IO_PENDING) { - if (!GetOverlappedResult(hSerial, overlapped, &nBytesRead, FALSE)) { - throw SimpleCom::SerialAPIException(GetLastError(), _T("GetOverlappedResult for ReadFile")); - } - } - else { - throw SimpleCom::SerialAPIException(GetLastError(), _T("ReadFile from serial device")); - } - } - - if (nBytesRead > 0) { - DWORD nBytesWritten; - if (!WriteFile(hStdOut, buf, nBytesRead, &nBytesWritten, NULL)) { - throw SimpleCom::WinAPIException(GetLastError(), _T("WriteFile to stdout")); - } - if (logwriter != nullptr) { - logwriter->Write(buf, nBytesRead); - } - remainBytes -= nBytesRead; - } - - } - - } -} - -/* - * Entry point for stdout redirector. - * stdout redirects serial (read op) to stdout. - */ -DWORD WINAPI SimpleCom::StdOutRedirector(_In_ LPVOID lpParameter) { - TStdOutRedirectorParam* param = reinterpret_cast(lpParameter); - - while (WaitForSingleObject(param->hTermEvent, 0) != WAIT_OBJECT_0) { - try { - - if (!ResetEvent(param->overlapped->hEvent)) { - throw SimpleCom::WinAPIException(GetLastError(), _T("ResetEvent for reading data from serial device")); - } - - StdOutRedirectorLoopInner(param->hSerial, param->overlapped, param->conn->_hStdOut, param->conn->_logwriter); - } - catch (SimpleCom::WinAPIException& e) { - if (WaitForSingleObject(param->hTermEvent, 0) != WAIT_OBJECT_0) { - SetEvent(param->hTermEvent); - param->conn->_exception_queue.push(e); - break; - } - } - } - return 0; -} - -/* - * Entry point for stdin redirector. - * stdin redirects stdin to serial (write op). - * Return true if F1 key is pushed (active close). - */ -bool SimpleCom::SerialConnection::StdInRedirector(const HANDLE hSerial, const HANDLE hTermEvent) { - INPUT_RECORD inputs[buf_sz]; - DWORD n_read; - - SimpleCom::SerialPortWriter writer(hSerial, buf_sz); - - try { - HANDLE waiters[] = { _hStdIn, hTermEvent }; - COORD newConsoleSize; - bool isConsoleSizeUpdated = false; - - while (true) { - DWORD result = WaitForMultipleObjects(sizeof(waiters) / sizeof(HANDLE), waiters, FALSE, INFINITE); - if (result == WAIT_OBJECT_0) { // hStdIn - if (!ReadConsoleInput(_hStdIn, inputs, sizeof(inputs) / sizeof(INPUT_RECORD), &n_read)) { - throw SimpleCom::WinAPIException(GetLastError()); - } - - for (DWORD idx = 0; idx < n_read; idx++) { - if (inputs[idx].EventType == KEY_EVENT) { - // Skip escape sequence of F1 (0x1B 0x4F 0x50) - // - // Input sequence on Windows (ENABLE_VIRTUAL_TERMINAL_INPUT): - // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#numpad--function-keys - if (idx + 2 < n_read && - (inputs[idx].Event.KeyEvent.wRepeatCount == 1 && inputs[idx].Event.KeyEvent.uChar.AsciiChar == '\x1b' /* ESC */) && - (inputs[idx + 1].Event.KeyEvent.wRepeatCount == 1 && inputs[idx + 1].Event.KeyEvent.uChar.AsciiChar == 'O') && - (inputs[idx + 2].Event.KeyEvent.wRepeatCount == 1 && inputs[idx + 2].Event.KeyEvent.uChar.AsciiChar == 'P')) { - idx += 2; - if (ShouldTerminate(writer, hTermEvent)) { - return true; - } - else{ - continue; - } - } - - ProcessKeyEvents(inputs[idx].Event.KeyEvent, writer); - } - else if ((inputs[idx].EventType == WINDOW_BUFFER_SIZE_EVENT) && _useTTYResizer) { - if(memcmp(¤t_window_sz, &inputs[idx].Event.WindowBufferSizeEvent.dwSize, sizeof(COORD)) != 0) { - newConsoleSize = inputs[idx].Event.WindowBufferSizeEvent.dwSize; - isConsoleSizeUpdated = true; - } - } - - if (isConsoleSizeUpdated) { - DWORD numOfEvents; - if (GetNumberOfConsoleInputEvents(_hStdIn, &numOfEvents) && (numOfEvents == 0)) { - char buf[RINGBUF_SZ]; - int len = snprintf(buf, sizeof(buf), "%c%d" RESIZER_SEPARATOR "%d%c", RESIZER_START_MARKER, newConsoleSize.Y, newConsoleSize.X, RESIZER_END_MARKER); - writer.PutData(buf, len); - isConsoleSizeUpdated = false; - current_window_sz = newConsoleSize; - } - } - } - - writer.WriteAsync(); - } - else if (result == (WAIT_OBJECT_0 + 1)) { // hTermEvent - break; - } - else { - throw SimpleCom::WinAPIException(GetLastError(), _T("WaitForMultipleObjects in StdInRedirector")); - } - } - } - catch (SimpleCom::WinAPIException& e) { - // Fire terminate event because other threads should be terminated immediately. - SetEvent(hTermEvent); - _exception_queue.push(e); - } - - writer.Shutdown(); - return false; -} /* * Talk with peripheral. * Set true to allowDetachDevice if the function would be finished silently when serial controller is detached. - * Return true if F1 key is pushed (active close). */ -bool SimpleCom::SerialConnection::DoSession(bool allowDetachDevice) { - _exception_queue.clear(); - +bool SimpleCom::SerialConnection::DoSession(bool allowDetachDevice, bool useTTYResizer, HWND parent_hwnd) { HandleHandler hSerial(CreateFile(_device.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL), _T("Open serial port")); InitSerialPort(hSerial.handle()); - HandleHandler hIoEvent(CreateEvent(NULL, TRUE, TRUE, NULL), _T("CreateEvent for reading from serial device")); - HandleHandler hTermEvent(CreateEvent(NULL, TRUE, FALSE, NULL), _T("CreateEvent for thread termination")); - OVERLAPPED serialReadOverlapped = { 0 }; - serialReadOverlapped.hEvent = hIoEvent.handle(); - - CONSOLE_SCREEN_BUFFER_INFO console_info = { 0 }; - CALL_WINAPI_WITH_DEBUGLOG(GetConsoleScreenBufferInfo(_hStdOut, &console_info), TRUE, __FILE__, __LINE__) - current_window_sz = console_info.dwSize; - - // Create stdout redirector - TStdOutRedirectorParam param = { - .hSerial = hSerial.handle(), - .overlapped = &serialReadOverlapped, - .hTermEvent = hTermEvent.handle(), - .conn = this - }; - HandleHandler threadHnd(CreateThread(NULL, 0, &StdOutRedirector, ¶m, 0, NULL), _T("CreateThread for StdOutRedirector")); + TerminalRedirector redirector(hSerial.handle(), _logwriter, _enableStdinLogging, useTTYResizer, parent_hwnd); + redirector.StartRedirector(); - // stdin redirector would perform in current thread - bool exited = StdInRedirector(hSerial.handle(), hTermEvent.handle()); - - // stdin redirector should be finished at this point. - // It means end of serial communication. So we should terminate stdout redirector. - CALL_WINAPI_WITH_DEBUGLOG(CancelIoEx(hSerial.handle(), &serialReadOverlapped), TRUE, __FILE__, __LINE__) - WaitForSingleObject(threadHnd.handle(), INFINITE); + redirector.AwaitTermination(); WinAPIException ex; - while (_exception_queue.try_pop(ex)) { + while (redirector.exception_queue().try_pop(ex)) { switch (ex.GetErrorCode()) { // Intended - this error would be reported when the user finishes the session. case ERROR_OPERATION_ABORTED: @@ -334,9 +91,19 @@ bool SimpleCom::SerialConnection::DoSession(bool allowDetachDevice) { [[fallthrough]]; default: - MessageBox(_parent_hwnd, ex.GetErrorText().c_str(), ex.GetErrorCaption(), MB_OK | MB_ICONERROR); + MessageBox(parent_hwnd, ex.GetErrorText().c_str(), ex.GetErrorCaption(), MB_OK | MB_ICONERROR); } } - return exited; + return redirector.Reattachable(); +} + +void SimpleCom::SerialConnection::DoBatch() { + HandleHandler hSerial(CreateFile(_device.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL), _T("Open serial port")); + InitSerialPort(hSerial.handle()); + + BatchRedirector redirector(hSerial.handle()); + + redirector.StartRedirector(); + redirector.AwaitTermination(); } \ No newline at end of file diff --git a/SimpleCom/SerialConnection.h b/SimpleCom/SerialConnection.h index 4464de9..6694534 100644 --- a/SimpleCom/SerialConnection.h +++ b/SimpleCom/SerialConnection.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023, 2024, Yasumasa Suenaga + * Copyright (C) 2023, 2025, Yasumasa Suenaga * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -19,9 +19,7 @@ #pragma once #include "stdafx.h" -#include "SerialPortWriter.h" #include "LogWriter.h" -#include "WinAPIException.h" namespace SimpleCom { @@ -31,26 +29,18 @@ namespace SimpleCom { private: TString _device; DCB _dcb; - HWND _parent_hwnd; - HANDLE _hStdIn; - HANDLE _hStdOut; - bool _useTTYResizer; LogWriter* _logwriter; bool _enableStdinLogging; - concurrency::concurrent_queue _exception_queue; void InitSerialPort(const HANDLE hSerial); - bool ShouldTerminate(SerialPortWriter& writer, const HANDLE hTermEvent); - void ProcessKeyEvents(const KEY_EVENT_RECORD keyevent, SerialPortWriter& writer); - bool StdInRedirector(const HANDLE hSerial, const HANDLE hTermEvent); - - friend DWORD WINAPI StdOutRedirector(_In_ LPVOID lpParameter); public: - SerialConnection(TString& device, DCB* dcb, HWND hwnd, HANDLE hStdIn, HANDLE hStdOut, bool useTTYResizer, LPCTSTR logfilename, bool enableStdinLogging); - virtual ~SerialConnection(); + SerialConnection(TString& device, DCB* dcb, LPCTSTR logfilename, bool enableStdinLogging); + SerialConnection(TString& device, DCB* dcb) : SerialConnection(device, dcb, nullptr, false) {}; + virtual ~SerialConnection() {}; - bool DoSession(bool allowDetachDevice); + bool DoSession(bool allowDetachDevice, bool useTTYResizer, HWND parent_hwnd); + void DoBatch(); }; } \ No newline at end of file diff --git a/SimpleCom/SerialSetup.cpp b/SimpleCom/SerialSetup.cpp index 30fe62e..6aea049 100644 --- a/SimpleCom/SerialSetup.cpp +++ b/SimpleCom/SerialSetup.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019, 2024, Yasumasa Suenaga + * Copyright (C) 2019, 2025, Yasumasa Suenaga * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -150,6 +150,7 @@ SimpleCom::SerialSetup::SerialSetup() : _options[_T("--auto-reconnect-timeout")] = new CommandlineOption(_T("[num]"), _T("Reconnect timeout"), 120); _options[_T("--log-file")] = new CommandlineOption(_T("[logfile]"), _T("Log serial communication to file"), nullptr); _options[_T("--stdin-logging")] = new CommandlineOption(_T(""), _T("Enable stdin logging"), false); + _options[_T("--batch")] = new CommandlineOption(_T(""), _T("Perform in batch mode"), false); _options[_T("--help")] = new CommandlineHelpOption(&_options); } @@ -526,8 +527,34 @@ void SimpleCom::SerialSetup::ParseArguments(int argc, LPCTSTR argv[]) { } } + Validate(); +} + +void SimpleCom::SerialSetup::Validate() { if (!IsShowDialog() && _port.empty()) { - throw SerialSetupException(_T("command line argument"), _T("Serial port is not specified")); + throw std::invalid_argument("Serial port is not specified"); + } + + if(IsEnableStdinLogging() && GetLogFile() == nullptr) { + throw std::invalid_argument("Log file should be configured when stdin logging is enabled"); + } + + if (IsBatchMode()) { + if (_port.empty()) { + throw std::invalid_argument("Serial port have to be set with batch mode"); + } + if(IsShowDialog()) { + throw std::invalid_argument("Configuration dialog cannot be configured with batch mode"); + } + if (GetUseTTYResizer()) { + throw std::invalid_argument("TTY resizer cannot be configured with batch mode"); + } + if (GetAutoReconnect()) { + throw std::invalid_argument("Auto reconnect cannot be configured with batch mode"); + } + if(GetLogFile() != nullptr) { + throw std::invalid_argument("Logging cannot be configured with batch mode"); + } } } diff --git a/SimpleCom/SerialSetup.h b/SimpleCom/SerialSetup.h index de676ce..cf15ced 100644 --- a/SimpleCom/SerialSetup.h +++ b/SimpleCom/SerialSetup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019, 2024, Yasumasa Suenaga + * Copyright (C) 2019, 2025, Yasumasa Suenaga * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -81,6 +81,8 @@ namespace SimpleCom { SerialDeviceScanner _scanner; std::map _options; + void Validate(); + public: SerialSetup(); virtual ~SerialSetup(); @@ -201,6 +203,14 @@ namespace SimpleCom { return static_cast*>(_options[_T("--stdin-logging")])->get(); } + inline void SetBatchMode(bool enabled) { + static_cast*>(_options[_T("--batch")])->set(enabled); + } + + inline bool IsBatchMode() { + return static_cast*>(_options[_T("--batch")])->get(); + } + inline SerialDeviceScanner& GetDeviceScanner() { return _scanner; } diff --git a/SimpleCom/SimpleCom.cpp b/SimpleCom/SimpleCom.cpp index 3f3f363..0b76f5d 100644 --- a/SimpleCom/SimpleCom.cpp +++ b/SimpleCom/SimpleCom.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019, 2024, Yasumasa Suenaga + * Copyright (C) 2019, 2025, Yasumasa Suenaga * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,9 +24,6 @@ #include "WinAPIException.h" #include "debug.h" -static LPCTSTR CLEAR_CONSOLE_COMMAND = _T("\x1b[2J"); -static DWORD CLEAR_CONSOLE_COMMAND_LEN = static_cast(_tcslen(CLEAR_CONSOLE_COMMAND)); - static HWND GetParentWindow() { HWND current = GetConsoleWindow(); @@ -49,37 +46,54 @@ static HWND GetParentWindow() { } -static std::tuple InitConsole(SimpleCom::SerialSetup& setup) { - TStringStream ss; - ss << "Current code page: " << GetConsoleCP(); - SimpleCom::debug::log(ss.str().c_str()); +static int DoInteractiveMode(TString& device, DCB *dcb, SimpleCom::SerialSetup &setup, HWND parent_hwnd) { + try { + while (true) { + SimpleCom::SerialConnection conn(device, dcb, setup.GetLogFile(), setup.IsEnableStdinLogging()); + bool reattachable = conn.DoSession(setup.GetAutoReconnect(), setup.GetUseTTYResizer(), parent_hwnd); - DWORD mode; - HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); - if (hStdIn == INVALID_HANDLE_VALUE) { - throw SimpleCom::WinAPIException(GetLastError(), _T("GetStdHandle(stdin)")); + if (setup.GetAutoReconnect() && reattachable) { + SimpleCom::debug::log(_T("Sleep before reconnecting...")); + Sleep(setup.GetAutoReconnectPauseInSec() * 1000); + + SimpleCom::debug::log(_T("Reconnect start")); + SimpleCom::SerialDeviceScanner scanner; + scanner.SetTargetPort(setup.GetPort()); + RegDisablePredefinedCacheEx(); + scanner.WaitSerialDevices(parent_hwnd, setup.GetAutoReconnectTimeoutInSec()); + if (scanner.GetDevices().empty()) { + throw SimpleCom::SerialDeviceScanException(_T("Waiting for serial device"), _T("Serial device is not available")); + } + SimpleCom::debug::log(_T("Reconnect device found")); + } + else { + break; + } + } } - CALL_WINAPI_WITH_DEBUGLOG(GetConsoleMode(hStdIn, &mode), TRUE, __FILE__, __LINE__) - mode &= ~ENABLE_PROCESSED_INPUT; - mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; - CALL_WINAPI_WITH_DEBUGLOG(SetConsoleMode(hStdIn, mode), TRUE, __FILE__, __LINE__) - - HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); - if (hStdOut == INVALID_HANDLE_VALUE) { - throw SimpleCom::WinAPIException(GetLastError(), _T("GetStdHandle(stdout)")); + catch (SimpleCom::WinAPIException& e) { + MessageBox(parent_hwnd, e.GetErrorText().c_str(), e.GetErrorCaption(), MB_OK | MB_ICONERROR); + return -4; } - CALL_WINAPI_WITH_DEBUGLOG(GetConsoleMode(hStdOut, &mode), TRUE, __FILE__, __LINE__); - mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT; - CALL_WINAPI_WITH_DEBUGLOG(SetConsoleMode(hStdOut, mode), TRUE, __FILE__, __LINE__); + catch (SimpleCom::SerialDeviceScanException& e) { + MessageBox(parent_hwnd, e.GetErrorText(), e.GetErrorCaption(), MB_OK | MB_ICONERROR); + return -5; + } + + return 0; +} - return { hStdIn, hStdOut }; +static int DoBatchMode(TString& device, DCB* dcb) { + SimpleCom::SerialConnection conn(device, dcb); + conn.DoBatch(); + + return 0; } int _tmain(int argc, LPCTSTR argv[]) { DCB dcb; TString device; - std::tuple std_handles; HWND parent_hwnd = GetParentWindow(); SimpleCom::SerialSetup setup; @@ -94,8 +108,6 @@ int _tmain(int argc, LPCTSTR argv[]) setup.SetShowDialog(true); } - std_handles = InitConsole(setup); - if (setup.GetWaitDevicePeriod() > 0) { setup.GetDeviceScanner().SetTargetPort(setup.GetPort()); setup.GetDeviceScanner().WaitSerialDevices(parent_hwnd, setup.GetWaitDevicePeriod()); @@ -126,6 +138,12 @@ int _tmain(int argc, LPCTSTR argv[]) MessageBox(parent_hwnd, e.GetErrorText(), e.GetErrorCaption(), MB_OK | MB_ICONERROR); return -3; } + catch(std::invalid_argument& e) { + // Only ASCII chars should be converted to wchar, so we can ignore deprecation since C++17. + std::wstring_convert> conv; + MessageBox(parent_hwnd, conv.from_bytes(e.what()).c_str(), _T("Invalid argument"), MB_OK | MB_ICONERROR); + return -4; + } if (setup.GetUseUTF8()) { CALL_WINAPI_WITH_DEBUGLOG(SetConsoleCP(CP_UTF8), TRUE, __FILE__, __LINE__); @@ -135,44 +153,5 @@ int _tmain(int argc, LPCTSTR argv[]) SimpleCom::debug::log(ss2.str().c_str()); } - try { - while (true) { - HANDLE hStdIn = std::get<0>(std_handles); - HANDLE hStdOut = std::get<1>(std_handles); - - // Clear console - WriteConsole(hStdOut, CLEAR_CONSOLE_COMMAND, CLEAR_CONSOLE_COMMAND_LEN, nullptr, nullptr); - - SimpleCom::SerialConnection conn(device, &dcb, parent_hwnd, hStdIn, hStdOut, setup.GetUseTTYResizer(), setup.GetLogFile(), setup.IsEnableStdinLogging()); - bool exited = conn.DoSession(setup.GetAutoReconnect()); - - if (setup.GetAutoReconnect() && !exited) { - SimpleCom::debug::log(_T("Sleep before reconnecting...")); - Sleep(setup.GetAutoReconnectPauseInSec() * 1000); - - SimpleCom::debug::log(_T("Reconnect start")); - SimpleCom::SerialDeviceScanner scanner; - scanner.SetTargetPort(setup.GetPort()); - RegDisablePredefinedCacheEx(); - scanner.WaitSerialDevices(parent_hwnd, setup.GetAutoReconnectTimeoutInSec()); - if (scanner.GetDevices().empty()) { - throw SimpleCom::SerialDeviceScanException(_T("Waiting for serial device"), _T("Serial device is not available")); - } - SimpleCom::debug::log(_T("Reconnect device found")); - } - else { - break; - } - } - } - catch (SimpleCom::WinAPIException& e) { - MessageBox(parent_hwnd, e.GetErrorText().c_str(), e.GetErrorCaption(), MB_OK | MB_ICONERROR); - return -4; - } - catch (SimpleCom::SerialDeviceScanException& e) { - MessageBox(parent_hwnd, e.GetErrorText(), e.GetErrorCaption(), MB_OK | MB_ICONERROR); - return -5; - } - - return 0; + return setup.IsBatchMode() ? DoBatchMode(device, &dcb) : DoInteractiveMode(device, &dcb, setup, parent_hwnd); } diff --git a/SimpleCom/SimpleCom.vcxproj b/SimpleCom/SimpleCom.vcxproj index f799c0c..e94bd85 100644 --- a/SimpleCom/SimpleCom.vcxproj +++ b/SimpleCom/SimpleCom.vcxproj @@ -175,6 +175,7 @@ + @@ -186,12 +187,15 @@ Create + + + @@ -201,6 +205,8 @@ + + diff --git a/SimpleCom/SimpleCom.vcxproj.filters b/SimpleCom/SimpleCom.vcxproj.filters index 7f22a33..642cadc 100644 --- a/SimpleCom/SimpleCom.vcxproj.filters +++ b/SimpleCom/SimpleCom.vcxproj.filters @@ -48,6 +48,15 @@ ソース ファイル + + ソース ファイル + + + ソース ファイル + + + ソース ファイル + @@ -89,6 +98,15 @@ ヘッダー ファイル + + ヘッダー ファイル + + + ヘッダー ファイル + + + ヘッダー ファイル + diff --git a/SimpleCom/TerminalRedirector.cpp b/SimpleCom/TerminalRedirector.cpp new file mode 100644 index 0000000..d6de9cd --- /dev/null +++ b/SimpleCom/TerminalRedirector.cpp @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2025, Yasumasa Suenaga + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ +#include "stdafx.h" +#include "TerminalRedirector.h" +#include "SerialPortWriter.h" +#include "LogWriter.h" +#include "debug.h" +#include "WinAPIException.h" +#include "../common/common.h" + +static constexpr int buf_sz = 256; + +static constexpr LPCTSTR CLEAR_CONSOLE_COMMAND = _T("\x1b[2J"); +static DWORD CLEAR_CONSOLE_COMMAND_LEN = static_cast(_tcslen(CLEAR_CONSOLE_COMMAND)); + + +/* + * Entry point for stdout redirector. + * stdout redirects serial (read op) to stdout. + */ +DWORD WINAPI StdOutRedirector(_In_ LPVOID lpParameter) { + SimpleCom::TStdOutRedirectorParam* param = reinterpret_cast(lpParameter); + + while (WaitForSingleObject(param->hTermEvent, 0) != WAIT_OBJECT_0) { + try { + + if (!ResetEvent(param->overlapped.hEvent)) { + throw SimpleCom::WinAPIException(GetLastError(), _T("ResetEvent for reading data from serial device")); + } + + DWORD event_mask = 0; + if (!WaitCommEvent(param->hSerial, &event_mask, ¶m->overlapped)) { + if (GetLastError() == ERROR_IO_PENDING) { + DWORD unused = 0; + if (!GetOverlappedResult(param->hSerial, ¶m->overlapped, &unused, TRUE)) { + throw SimpleCom::SerialAPIException(GetLastError(), _T("GetOverlappedResult for WaitCommEvent")); + } + } + else { + throw SimpleCom::SerialAPIException(GetLastError(), _T("WaitCommEvent")); + } + } + + if (event_mask & EV_RXCHAR) { + DWORD errors; + COMSTAT comstat = { 0 }; + if (!ClearCommError(param->hSerial, &errors, &comstat)) { + throw SimpleCom::SerialAPIException(GetLastError(), _T("ClearCommError")); + } + + char buf[buf_sz] = { 0 }; + DWORD nBytesRead = 0; + DWORD remainBytes = comstat.cbInQue; + while (remainBytes > 0) { + + if (!ReadFile(param->hSerial, buf, min(buf_sz, remainBytes), &nBytesRead, ¶m->overlapped)) { + if (GetLastError() == ERROR_IO_PENDING) { + if (!GetOverlappedResult(param->hSerial, ¶m->overlapped, &nBytesRead, FALSE)) { + throw SimpleCom::SerialAPIException(GetLastError(), _T("GetOverlappedResult for ReadFile")); + } + } + else { + throw SimpleCom::SerialAPIException(GetLastError(), _T("ReadFile from serial device")); + } + } + + if (nBytesRead > 0) { + DWORD nBytesWritten; + if (!WriteFile(param->hStdOut, buf, nBytesRead, &nBytesWritten, NULL)) { + throw SimpleCom::WinAPIException(GetLastError(), _T("WriteFile to stdout")); + } + if (param->logwriter != nullptr) { + param->logwriter->Write(buf, nBytesRead); + } + remainBytes -= nBytesRead; + } + + } + + } + } + catch (SimpleCom::WinAPIException& e) { + if (WaitForSingleObject(param->hTermEvent, 0) != WAIT_OBJECT_0) { + SetEvent(param->hTermEvent); + param->exception_handler(e); + break; + } + } + } + + return 0; +} + +/* + * Ask user whether terminate current serial session via dialog box. + * Return true if session should be closed (active close). + */ +static bool ShouldTerminate(HWND parent_hwnd, SimpleCom::SerialPortWriter& writer, const HANDLE hTermEvent) { + // Write all keys in the buffer before F1 + writer.WriteAsync(); + + if (MessageBox(parent_hwnd, _T("Do you want to leave from this serial session?"), _T("SimpleCom"), MB_YESNO | MB_ICONQUESTION) == IDYES) { + SetEvent(hTermEvent); + return true; + } + + return false; +} + +/* + * Write key code in KEY_EVENT_RECORD to send buffer. + */ +static void ProcessKeyEvents(const KEY_EVENT_RECORD keyevent, SimpleCom::SerialPortWriter& writer, SimpleCom::LogWriter *logwriter) { + + if (keyevent.wVirtualKeyCode == VK_F1) { + // F1 key should not be propagated to peripheral. + return; + } + + if (keyevent.bKeyDown && (keyevent.uChar.AsciiChar != '\0')) { + for (int send_idx = 0; send_idx < keyevent.wRepeatCount; send_idx++) { + writer.Put(keyevent.uChar.AsciiChar); + if(logwriter != nullptr) { + // Write key to log file if logging is enabled. + logwriter->Write(keyevent.uChar.AsciiChar); + } + } + } +} + +/* + * Entry point for stdin redirector. + * stdin redirects stdin to serial (write op). + * Return true if F1 key is pushed (active close). + */ +DWORD WINAPI StdInRedirector(_In_ LPVOID lpParameter) { + SimpleCom::TStdInRedirectorParam* param = reinterpret_cast(lpParameter); + INPUT_RECORD inputs[buf_sz]; + DWORD n_read; + + CONSOLE_SCREEN_BUFFER_INFO console_info = { 0 }; + CALL_WINAPI_WITH_DEBUGLOG(GetConsoleScreenBufferInfo(param->hStdOut, &console_info), TRUE, __FILE__, __LINE__) + COORD current_window_sz = console_info.dwSize; + + SimpleCom::SerialPortWriter writer(param->hSerial, buf_sz); + + try { + HANDLE waiters[] = { param->hStdIn, param->hTermEvent }; + COORD newConsoleSize; + bool isConsoleSizeUpdated = false; + + while (true) { + DWORD result = WaitForMultipleObjects(sizeof(waiters) / sizeof(HANDLE), waiters, FALSE, INFINITE); + if (result == WAIT_OBJECT_0) { // hStdIn + if (!ReadConsoleInput(param->hStdIn, inputs, sizeof(inputs) / sizeof(INPUT_RECORD), &n_read)) { + throw SimpleCom::WinAPIException(GetLastError()); + } + + for (DWORD idx = 0; idx < n_read; idx++) { + if (inputs[idx].EventType == KEY_EVENT) { + // Skip escape sequence of F1 (0x1B 0x4F 0x50) + // + // Input sequence on Windows (ENABLE_VIRTUAL_TERMINAL_INPUT): + // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#numpad--function-keys + if (idx + 2 < n_read && + (inputs[idx].Event.KeyEvent.wRepeatCount == 1 && inputs[idx].Event.KeyEvent.uChar.AsciiChar == '\x1b' /* ESC */) && + (inputs[idx + 1].Event.KeyEvent.wRepeatCount == 1 && inputs[idx + 1].Event.KeyEvent.uChar.AsciiChar == 'O') && + (inputs[idx + 2].Event.KeyEvent.wRepeatCount == 1 && inputs[idx + 2].Event.KeyEvent.uChar.AsciiChar == 'P')) { + idx += 2; + if (ShouldTerminate(param->parent_hwnd, writer, param->hTermEvent)) { + *param->reattachable = false; + CALL_WINAPI_WITH_DEBUGLOG(CancelIoEx(param->hSerial, nullptr), TRUE, __FILE__, __LINE__) + return 0; + } + else { + continue; + } + } + + ProcessKeyEvents(inputs[idx].Event.KeyEvent, writer, param->enableStdinLogging ? param->logwriter : nullptr); + } + else if ((inputs[idx].EventType == WINDOW_BUFFER_SIZE_EVENT) && param->useTTYResizer) { + if (memcmp(¤t_window_sz, &inputs[idx].Event.WindowBufferSizeEvent.dwSize, sizeof(COORD)) != 0) { + newConsoleSize = inputs[idx].Event.WindowBufferSizeEvent.dwSize; + isConsoleSizeUpdated = true; + } + } + + if (isConsoleSizeUpdated) { + DWORD numOfEvents; + if (GetNumberOfConsoleInputEvents(param->hStdIn, &numOfEvents) && (numOfEvents == 0)) { + char buf[RINGBUF_SZ]; + int len = snprintf(buf, sizeof(buf), "%c%d" RESIZER_SEPARATOR "%d%c", RESIZER_START_MARKER, newConsoleSize.Y, newConsoleSize.X, RESIZER_END_MARKER); + writer.PutData(buf, len); + isConsoleSizeUpdated = false; + current_window_sz = newConsoleSize; + } + } + } + + writer.WriteAsync(); + } + else if (result == (WAIT_OBJECT_0 + 1)) { // hTermEvent + break; + } + else { + throw SimpleCom::WinAPIException(GetLastError(), _T("WaitForMultipleObjects in StdInRedirector")); + } + } + } + catch (SimpleCom::WinAPIException& e) { + // Fire terminate event because other threads should be terminated immediately. + SetEvent(param->hTermEvent); + param->exception_handler(e); + } + + writer.Shutdown(); + return 0; +} + +SimpleCom::TerminalRedirector::TerminalRedirector(HANDLE hSerial, SimpleCom::LogWriter* logwriter, bool enableStdinLogging, bool useTTYResizer, HWND parent_hwnd) : + TerminalRedirectorBase(hSerial), + _hTermEvent(CreateEvent(NULL, TRUE, FALSE, NULL), _T("CreateEvent for thread termination")), + _hIoEvent(CreateEvent(NULL, TRUE, TRUE, NULL), _T("CreateEvent for reading from serial device")), + _exception_queue(), + _reattachable(true) +{ + TStringStream ss; + ss << "Current code page: " << GetConsoleCP(); + SimpleCom::debug::log(ss.str().c_str()); + + DWORD mode; + HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); + if (hStdIn == INVALID_HANDLE_VALUE) { + throw SimpleCom::WinAPIException(GetLastError(), _T("GetStdHandle(stdin)")); + } + CALL_WINAPI_WITH_DEBUGLOG(GetConsoleMode(hStdIn, &mode), TRUE, __FILE__, __LINE__) + mode &= ~ENABLE_PROCESSED_INPUT; + mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; + CALL_WINAPI_WITH_DEBUGLOG(SetConsoleMode(hStdIn, mode), TRUE, __FILE__, __LINE__) + + HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hStdOut == INVALID_HANDLE_VALUE) { + throw SimpleCom::WinAPIException(GetLastError(), _T("GetStdHandle(stdout)")); + } + CALL_WINAPI_WITH_DEBUGLOG(GetConsoleMode(hStdOut, &mode), TRUE, __FILE__, __LINE__); + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT; + CALL_WINAPI_WITH_DEBUGLOG(SetConsoleMode(hStdOut, mode), TRUE, __FILE__, __LINE__); + + _stdin_param = { + .hSerial = hSerial, + .hStdIn = hStdIn, + .hStdOut = hStdOut, + .enableStdinLogging = enableStdinLogging, + .logwriter = logwriter, + .useTTYResizer = useTTYResizer, + .parent_hwnd = parent_hwnd, + .hTermEvent = _hTermEvent.handle(), + .exception_handler = [&](const WinAPIException& e) { _exception_queue.push(e); }, + .reattachable = &_reattachable + }; + + _stdout_param = { + .hSerial = hSerial, + .hStdOut = hStdOut, + .overlapped = { .hEvent = _hIoEvent.handle() }, + .logwriter = logwriter, + .hTermEvent = _hTermEvent.handle(), + .exception_handler = [&](const WinAPIException& e) { _exception_queue.push(e); } + }; +} + +std::tuple SimpleCom::TerminalRedirector::GetStdInRedirector() { + return { &StdInRedirector, &_stdin_param }; +} + +std::tuple SimpleCom::TerminalRedirector::GetStdOutRedirector() { + return { &StdOutRedirector, &_stdout_param }; +} + +void SimpleCom::TerminalRedirector::StartRedirector(){ + // Clear console + WriteConsole(_stdout_param.hStdOut, CLEAR_CONSOLE_COMMAND, CLEAR_CONSOLE_COMMAND_LEN, nullptr, nullptr); + + TerminalRedirectorBase::StartRedirector(); +} + +bool SimpleCom::TerminalRedirector::Reattachable() { + return _reattachable; +} \ No newline at end of file diff --git a/SimpleCom/TerminalRedirector.h b/SimpleCom/TerminalRedirector.h new file mode 100644 index 0000000..4e3f93b --- /dev/null +++ b/SimpleCom/TerminalRedirector.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025, Yasumasa Suenaga + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ +#pragma once + +#include "stdafx.h" +#include "TerminalRedirectorBase.h" +#include "util.h" + +namespace SimpleCom +{ + typedef struct { + HANDLE hSerial; + HANDLE hStdOut; + OVERLAPPED overlapped; + SimpleCom::LogWriter* logwriter; + HANDLE hTermEvent; + std::function exception_handler; + } TStdOutRedirectorParam; + + typedef struct { + HANDLE hSerial; + HANDLE hStdIn; + HANDLE hStdOut; + bool enableStdinLogging; + SimpleCom::LogWriter* logwriter; + bool useTTYResizer; + HWND parent_hwnd; + HANDLE hTermEvent; + std::function exception_handler; + bool* reattachable; + } TStdInRedirectorParam; + + class TerminalRedirector : + public TerminalRedirectorBase + { + private: + HandleHandler _hTermEvent; + HandleHandler _hIoEvent; + concurrency::concurrent_queue _exception_queue; + bool _reattachable; + TStdInRedirectorParam _stdin_param; + TStdOutRedirectorParam _stdout_param; + + protected: + virtual std::tuple GetStdInRedirector() override; + virtual std::tuple GetStdOutRedirector() override; + + public: + TerminalRedirector(HANDLE hSerial, LogWriter* logwriter, bool enableStdinLogging, bool useTTYResizer, HWND parent_hwnd); + virtual ~TerminalRedirector() {}; + + inline concurrency::concurrent_queue & exception_queue() { + return _exception_queue; + } + + void StartRedirector() override; + bool Reattachable() override; + }; +} \ No newline at end of file diff --git a/SimpleCom/TerminalRedirectorBase.cpp b/SimpleCom/TerminalRedirectorBase.cpp new file mode 100644 index 0000000..c8a93f4 --- /dev/null +++ b/SimpleCom/TerminalRedirectorBase.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025, Yasumasa Suenaga + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include "stdafx.h" +#include "TerminalRedirectorBase.h" + + +void SimpleCom::TerminalRedirectorBase::StartRedirector() +{ + auto [StdInRedirector, stdin_param] = GetStdInRedirector(); + auto [StdOutRedirector, stdout_param] = GetStdOutRedirector(); + + _hThreadStdIn = CreateThread(NULL, 0, StdInRedirector, stdin_param, 0, NULL); + if (GetLastError() != ERROR_SUCCESS) { + throw SimpleCom::WinAPIException(GetLastError(), _T("CreateThread for StdInRedirector")); + } + + _hThreadStdOut = CreateThread(NULL, 0, StdOutRedirector, stdout_param, 0, NULL); + if (GetLastError() != ERROR_SUCCESS) { + throw SimpleCom::WinAPIException(GetLastError(), _T("CreateThread for StdOutRedirector")); + } +} + +void SimpleCom::TerminalRedirectorBase::AwaitTermination() +{ + HANDLE objs[] = { _hThreadStdIn, _hThreadStdOut }; + WaitForMultipleObjects(sizeof(objs) / sizeof(HANDLE), objs, TRUE, INFINITE); +} + +bool SimpleCom::TerminalRedirectorBase::Reattachable() +{ + return false; +} \ No newline at end of file diff --git a/SimpleCom/TerminalRedirectorBase.h b/SimpleCom/TerminalRedirectorBase.h new file mode 100644 index 0000000..2a80c75 --- /dev/null +++ b/SimpleCom/TerminalRedirectorBase.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025, Yasumasa Suenaga + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ +#pragma once + +#include "stdafx.h" +#include "LogWriter.h" +#include "WinAPIException.h" + + +namespace SimpleCom +{ + class TerminalRedirectorBase + { + protected: + HANDLE _hSerial; + HANDLE _hThreadStdIn; + HANDLE _hThreadStdOut; + + virtual std::tuple GetStdInRedirector() = 0; + virtual std::tuple GetStdOutRedirector() = 0; + + public: + TerminalRedirectorBase(HANDLE hSerial) : _hSerial(hSerial), _hThreadStdIn(INVALID_HANDLE_VALUE), _hThreadStdOut(INVALID_HANDLE_VALUE) {}; + virtual ~TerminalRedirectorBase() {}; + + virtual void StartRedirector(); + + // Returns true if the peripheral is reattachable. + virtual void AwaitTermination(); + + virtual bool Reattachable(); + }; +} \ No newline at end of file diff --git a/SimpleCom/stdafx.h b/SimpleCom/stdafx.h index 26e098c..a4430ef 100644 --- a/SimpleCom/stdafx.h +++ b/SimpleCom/stdafx.h @@ -26,6 +26,7 @@ #include #include #include +#include #ifdef _UNICODE typedef std::wstring TString; diff --git a/SimpleCom/util.h b/SimpleCom/util.h index 039b316..492862c 100644 --- a/SimpleCom/util.h +++ b/SimpleCom/util.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023, 2024, Yasumasa Suenaga + * Copyright (C) 2023, 2025, Yasumasa Suenaga * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -18,7 +18,7 @@ */ #pragma once -#include +#include "stdafx.h" #include "WinAPIException.h" /* diff --git a/SimpleComTest/SerialSetupTest.cpp b/SimpleComTest/SerialSetupTest.cpp index 8e82d0b..b35fd64 100644 --- a/SimpleComTest/SerialSetupTest.cpp +++ b/SimpleComTest/SerialSetupTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023, 2024, Yasumasa Suenaga + * Copyright (C) 2023, 2025, Yasumasa Suenaga * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -229,6 +229,7 @@ namespace SimpleComTest Assert::AreEqual(120, setup.GetAutoReconnectTimeoutInSec()); Assert::IsNull(setup.GetLogFile()); Assert::AreEqual(false, setup.IsEnableStdinLogging()); + Assert::AreEqual(false, setup.IsBatchMode()); } TEST_METHOD(ArgParserTest) @@ -342,5 +343,127 @@ namespace SimpleComTest Assert::AreEqual('\x13', dcb.XoffChar); } + TEST_METHOD(DialogWithoutPortValidatorTest) + { + SimpleCom::SerialSetup setup; + + LPCTSTR args[] = { + _T("SimpleCom.exe"), // argv[0] is an executable in main() + _T("--show-dialog"), + _T("--batch"), + _T("COM100") + }; + + auto test = [&] { setup.ParseArguments(sizeof(args) / sizeof(char*), args); }; + Assert::ExpectException(test); + } + + TEST_METHOD(StdInLoggingWithoutLoggingValidatorTest) + { + SimpleCom::SerialSetup setup; + + LPCTSTR args[] = { + _T("SimpleCom.exe"), // argv[0] is an executable in main() + _T("--stdin-logging"), + _T("COM100") + }; + + auto test = [&] { setup.ParseArguments(sizeof(args) / sizeof(char*), args); }; + Assert::ExpectException(test); + } + + TEST_METHOD(BatchWithoutPortValidatorTest) + { + SimpleCom::SerialSetup setup; + + LPCTSTR args[] = { + _T("SimpleCom.exe"), // argv[0] is an executable in main() + _T("--batch") + }; + + auto test = [&] { setup.ParseArguments(sizeof(args) / sizeof(char*), args); }; + Assert::ExpectException(test); + } + + TEST_METHOD(BatchWithDialogValidatorTest) + { + SimpleCom::SerialSetup setup; + + LPCTSTR args[] = { + _T("SimpleCom.exe"), // argv[0] is an executable in main() + _T("--batch"), + _T("--show-dialog"), + _T("COM100") + }; + + auto test = [&] { setup.ParseArguments(sizeof(args) / sizeof(char*), args); }; + Assert::ExpectException(test); + } + + TEST_METHOD(BatchWithTTYResizerValidatorTest) + { + SimpleCom::SerialSetup setup; + + LPCTSTR args[] = { + _T("SimpleCom.exe"), // argv[0] is an executable in main() + _T("--batch"), + _T("--tty-resizer"), + _T("COM100") + }; + + auto test = [&] { setup.ParseArguments(sizeof(args) / sizeof(char*), args); }; + Assert::ExpectException(test); + } + + TEST_METHOD(BatchWithAutoReconnectValidatorTest) + { + SimpleCom::SerialSetup setup; + + LPCTSTR args[] = { + _T("SimpleCom.exe"), // argv[0] is an executable in main() + _T("--batch"), + _T("--auto-reconnect"), + _T("COM100") + }; + + auto test = [&] { setup.ParseArguments(sizeof(args) / sizeof(char*), args); }; + Assert::ExpectException(test); + } + + TEST_METHOD(BatchWithLoggingValidatorTest) + { + SimpleCom::SerialSetup setup; + + LPCTSTR args[] = { + _T("SimpleCom.exe"), // argv[0] is an executable in main() + _T("--batch"), + _T("--log-file"), _T("silver-bullet.log"), + _T("COM100") + }; + + auto test = [&] { setup.ParseArguments(sizeof(args) / sizeof(char*), args); }; + Assert::ExpectException(test); + } + + TEST_METHOD(BatchValidatorTest) + { + SimpleCom::SerialSetup setup; + + LPCTSTR args[] = { + _T("SimpleCom.exe"), // argv[0] is an executable in main() + _T("--batch"), + _T("--baud-rate"), _T("9600"), + _T("--byte-size"), _T("1"), + _T("--parity"), _T("odd"), + _T("--stop-bits"), _T("2"), + _T("--flow-control"), _T("hardware"), + _T("--utf8"), + _T("--wait-serial-device"), _T("10"), + _T("COM100") + }; + setup.ParseArguments(sizeof(args) / sizeof(char*), args); + Assert::AreEqual(true, setup.IsBatchMode()); + } + }; }