#ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include "terminal_io.h" #include #include #include #include #include #include #include #include #include #include #include namespace crossdesk::cli { std::atomic interrupted{0}; namespace { int console_output = -1; std::string input_bytes; wchar_t high_surrogate = 0; DWORD mouse_buttons = 0; HANDLE Handle(int fd) { #ifdef _MSC_VER // A missing optional credential fd should be an error response, not a CRT // invalid-parameter process termination. Keep the handler thread-local. auto previous = _set_thread_local_invalid_parameter_handler( [](const wchar_t *, const wchar_t *, const wchar_t *, unsigned, uintptr_t) {}); auto value = _get_osfhandle(fd); _set_thread_local_invalid_parameter_handler(previous); return reinterpret_cast(value); #else return reinterpret_cast(_get_osfhandle(fd)); #endif } void Signal(int value) { interrupted.store(value, std::memory_order_relaxed); } BOOL WINAPI ConsoleSignal(DWORD value) { if (value == CTRL_C_EVENT || value == CTRL_BREAK_EVENT) { interrupted.store(SIGINT, std::memory_order_relaxed); return TRUE; } return FALSE; } std::string Utf8(const std::wstring &value) { if (value.empty()) return {}; int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(), int(value.size()), nullptr, 0, nullptr, nullptr); if (!size) throw std::runtime_error("invalid Unicode input"); std::string result(size, '\0'); WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(), int(value.size()), result.data(), size, nullptr, nullptr); return result; } std::wstring Wide(const std::string &value) { int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), int(value.size()), nullptr, 0); if (!size) throw std::runtime_error("invalid Unicode path"); std::wstring result(size, L'\0'); MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), int(value.size()), result.data(), size); return result; } void Character(wchar_t c) { if (c >= 0xd800 && c <= 0xdbff) { high_surrogate = c; return; } if (c >= 0xdc00 && c <= 0xdfff) { if (high_surrogate) input_bytes += Utf8(std::wstring{high_surrogate, c}); } else if (c) { input_bytes += Utf8(std::wstring(1, c)); } high_surrogate = 0; } void Key(const KEY_EVENT_RECORD &key) { if (!key.bKeyDown) return; const char *sequence = nullptr; switch (key.wVirtualKeyCode) { case VK_UP: sequence = "\033[A"; break; case VK_DOWN: sequence = "\033[B"; break; case VK_RIGHT: sequence = "\033[C"; break; case VK_LEFT: sequence = "\033[D"; break; case VK_HOME: sequence = "\033[H"; break; case VK_END: sequence = "\033[F"; break; case VK_DELETE: sequence = "\033[3~"; break; case VK_PRIOR: sequence = "\033[5~"; break; case VK_NEXT: sequence = "\033[6~"; break; case VK_F1: sequence = "\033OP"; break; case VK_F2: sequence = "\033OQ"; break; case VK_F3: sequence = "\033OR"; break; case VK_F4: sequence = "\033OS"; break; } for (int i = 0; i < std::min(key.wRepeatCount, 64); ++i) { if (sequence) input_bytes += sequence; else { // Right Alt + Ctrl is AltGr, not an Alt shortcut. bool alt = key.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); bool ctrl = key.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); if (alt && !ctrl && key.uChar.UnicodeChar) input_bytes += '\033'; Character(key.uChar.UnicodeChar); } } } void Mouse(const MOUSE_EVENT_RECORD &mouse) { CONSOLE_SCREEN_BUFFER_INFO info{}; if (console_output < 0 || !GetConsoleScreenBufferInfo(Handle(console_output), &info)) return; int x = mouse.dwMousePosition.X - info.srWindow.Left + 1; int y = mouse.dwMousePosition.Y - info.srWindow.Top + 1; if (x < 1 || y < 1) return; auto emit = [&](int button, bool release = false) { input_bytes += "\033[<" + std::to_string(button) + ";" + std::to_string(x) + ";" + std::to_string(y) + (release ? "m" : "M"); }; DWORD buttons = mouse.dwButtonState & 0xffff; const DWORD masks[] = {FROM_LEFT_1ST_BUTTON_PRESSED, FROM_LEFT_2ND_BUTTON_PRESSED, RIGHTMOST_BUTTON_PRESSED}; for (int i = 0; i < 3; ++i) if ((buttons & masks[i]) != (mouse_buttons & masks[i])) emit(i, !(buttons & masks[i])); if (mouse.dwEventFlags & (MOUSE_WHEELED | MOUSE_HWHEELED)) { int delta = static_cast(HIWORD(mouse.dwButtonState)); emit(64 + ((mouse.dwEventFlags & MOUSE_HWHEELED) ? 2 : 0) + (delta < 0)); } else if (mouse.dwEventFlags & MOUSE_MOVED) { emit(32 + ((buttons & masks[0]) ? 0 : (buttons & masks[1]) ? 1 : (buttons & masks[2]) ? 2 : 3)); } mouse_buttons = buttons; } bool Ready(int fd) { if (fd < 0) return false; HANDLE handle = Handle(fd); if (handle == INVALID_HANDLE_VALUE) throw std::runtime_error("input descriptor unavailable"); DWORD mode = 0; if (GetConsoleMode(handle, &mode)) { DWORD count = 0; return !input_bytes.empty() || (GetNumberOfConsoleInputEvents(handle, &count) && count); } if (GetFileType(handle) == FILE_TYPE_PIPE) { DWORD available = 0; if (PeekNamedPipe(handle, nullptr, 0, nullptr, &available, nullptr)) return available != 0; if (GetLastError() == ERROR_BROKEN_PIPE) return true; throw std::runtime_error("cannot inspect input pipe"); } return true; } } // namespace void InitializeIo() { // Preserve LF-delimited JSON and terminal escape bytes through CRT streams. for (int fd : {0, 1, 2}) _setmode(fd, _O_BINARY); std::signal(SIGINT, Signal); std::signal(SIGTERM, Signal); SetConsoleCtrlHandler(ConsoleSignal, TRUE); } const char *Platform() { return "windows"; } bool IsTty(int fd) { DWORD mode; return GetConsoleMode(Handle(fd), &mode) != 0; } std::ptrdiff_t Read(int fd, void *buffer, size_t size) { if (!size) return 0; HANDLE handle = Handle(fd); DWORD mode; if (GetConsoleMode(handle, &mode)) { if (input_bytes.empty()) { DWORD count = 0, read = 0; if (!GetNumberOfConsoleInputEvents(handle, &count)) { errno = EIO; return -1; } INPUT_RECORD records[128]; if (count && ReadConsoleInputW(handle, records, std::min(count, 128), &read)) for (DWORD i = 0; i < read; ++i) { if (records[i].EventType == KEY_EVENT) Key(records[i].Event.KeyEvent); else if (records[i].EventType == MOUSE_EVENT) Mouse(records[i].Event.MouseEvent); else if (records[i].EventType == FOCUS_EVENT && !records[i].Event.FocusEvent.bSetFocus) input_bytes += "\033[O"; } } if (input_bytes.empty()) { errno = EAGAIN; return -1; } size_t count = std::min(size, input_bytes.size()); std::copy_n(input_bytes.data(), count, static_cast(buffer)); input_bytes.erase(0, count); return count; } if (GetFileType(handle) == FILE_TYPE_PIPE) { DWORD available = 0; if (!PeekNamedPipe(handle, nullptr, 0, nullptr, &available, nullptr)) { if (GetLastError() == ERROR_BROKEN_PIPE) return 0; errno = EIO; return -1; } if (!available) { errno = EAGAIN; return -1; } size = std::min(size, available); } DWORD count = 0; if (!ReadFile(handle, buffer, DWORD(std::min(size, 16384)), &count, nullptr)) { if (GetLastError() == ERROR_BROKEN_PIPE) return 0; errno = EIO; return -1; } return count; } std::ptrdiff_t Write(int fd, const void *buffer, size_t size) { DWORD count = 0; if (!WriteFile(Handle(fd), buffer, DWORD(std::min(size, 16384)), &count, nullptr)) { errno = GetLastError() == ERROR_NO_DATA ? EPIPE : EIO; return -1; } if (!count && size) { errno = EAGAIN; return -1; } return count; } void Close(int fd) { _close(fd); } int Duplicate(int fd) { return _dup(fd); } int Redirect(int from, int to) { return _dup2(from, to); } int OpenLog(const std::string &path) { return _wopen(Wide(path).c_str(), _O_CREAT | _O_WRONLY | _O_APPEND | _O_BINARY, _S_IREAD | _S_IWRITE); } bool WaitReadable(int fd, int timeout_ms) { auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms); do { if (Ready(fd)) return true; if (interrupted) return false; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } while (std::chrono::steady_clock::now() < until); return false; } void WaitIo(int input_fd, int, int timeout_ms) { WaitReadable(input_fd, timeout_ms); } TerminalSize GetTerminalSize(int) { TerminalSize size; HANDLE handle = console_output >= 0 ? Handle(console_output) : CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); CONSOLE_SCREEN_BUFFER_INFO info{}; if (GetConsoleScreenBufferInfo(handle, &info)) { size.columns = info.srWindow.Right - info.srWindow.Left + 1; size.rows = info.srWindow.Bottom - info.srWindow.Top + 1; } if (console_output < 0 && handle != INVALID_HANDLE_VALUE) CloseHandle(handle); return size; } struct NonBlocking::State { HANDLE handle; DWORD mode = 0; bool restore = false; }; NonBlocking::NonBlocking(int fd, bool output) : state_(std::make_unique()) { state_->handle = Handle(fd); // Reads use PeekNamedPipe; output pipes additionally require bounded writes. if (output && GetFileType(state_->handle) == FILE_TYPE_PIPE) { DWORD flags = 0; if (!GetNamedPipeHandleStateW(state_->handle, &state_->mode, nullptr, nullptr, nullptr, nullptr, 0)) throw std::runtime_error("cannot read pipe mode"); flags = state_->mode | PIPE_NOWAIT; if (!SetNamedPipeHandleState(state_->handle, &flags, nullptr, nullptr)) throw std::runtime_error("cannot configure nonblocking pipe"); state_->restore = true; } } NonBlocking::~NonBlocking() { if (state_->restore) SetNamedPipeHandleState(state_->handle, &state_->mode, nullptr, nullptr); } struct RawTerminal::State { HANDLE input, output; DWORD input_mode, output_mode; UINT input_cp, output_cp; State(HANDLE in, HANDLE out, DWORD in_mode, DWORD out_mode) : input(in), output(out), input_mode(in_mode), output_mode(out_mode), input_cp(GetConsoleCP()), output_cp(GetConsoleOutputCP()) {} State(const State &) = delete; State &operator=(const State &) = delete; ~State() { SetConsoleMode(input, input_mode); SetConsoleMode(output, output_mode); SetConsoleCP(input_cp); SetConsoleOutputCP(output_cp); console_output = -1; input_bytes.clear(); high_surrogate = 0; mouse_buttons = 0; } }; RawTerminal::RawTerminal(int input, int output) { DWORD in_mode = 0, out_mode = 0; if (!GetConsoleMode(Handle(input), &in_mode) || !GetConsoleMode(Handle(output), &out_mode)) throw std::runtime_error( "view requires a Windows console or ConPTY terminal"); state_ = std::make_unique(Handle(input), Handle(output), in_mode, out_mode); DWORD mode = (in_mode & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE | ENABLE_VIRTUAL_TERMINAL_INPUT)) | ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT; if (!SetConsoleMode(Handle(input), mode) || !SetConsoleMode(Handle(output), out_mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) || !SetConsoleCP(CP_UTF8) || !SetConsoleOutputCP(CP_UTF8)) throw std::runtime_error( "terminal requires Windows 10 or newer with VT support"); console_output = output; } RawTerminal::~RawTerminal() = default; std::string Password() { HANDLE input = CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); HANDLE output = CreateFileW(L"CONOUT$", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); struct Restore { HANDLE input, output; DWORD mode = 0; bool active = false; ~Restore() { if (active) { SetConsoleMode(input, mode); DWORD count; WriteConsoleW(output, L"\r\n", 2, &count, nullptr); } if (input != INVALID_HANDLE_VALUE) CloseHandle(input); if (output != INVALID_HANDLE_VALUE) CloseHandle(output); } } restore{input, output}; if (input == INVALID_HANDLE_VALUE || output == INVALID_HANDLE_VALUE || !GetConsoleMode(input, &restore.mode)) throw std::runtime_error("password requires an interactive console"); if (!SetConsoleMode( input, (restore.mode & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT)) | ENABLE_PROCESSED_INPUT)) throw std::runtime_error("cannot disable password echo"); restore.active = true; DWORD count; WriteConsoleW(output, L"Remote password: ", 17, &count, nullptr); std::wstring value; while (!interrupted) { if (WaitForSingleObject(input, 100) != WAIT_OBJECT_0) continue; INPUT_RECORD record{}; if (!ReadConsoleInputW(input, &record, 1, &count)) break; if (!count || record.EventType != KEY_EVENT || !record.Event.KeyEvent.bKeyDown) continue; wchar_t c = record.Event.KeyEvent.uChar.UnicodeChar; if (c == L'\r') { auto result = Utf8(value); if (result.size() <= 256) return result; break; } if (c == L'\b') { if (!value.empty()) { wchar_t last = value.back(); value.pop_back(); if (last >= 0xdc00 && last <= 0xdfff && !value.empty()) value.pop_back(); } } else if (c >= 32) { value += c; if (value.size() > 256) break; } } throw std::runtime_error("password entry cancelled or too long"); } std::string Logs() { wchar_t root[MAX_PATH + 1]; DWORD size = GetTempPathW(MAX_PATH + 1, root); if (!size || size > MAX_PATH) throw std::runtime_error("cannot locate temporary directory"); PSECURITY_DESCRIPTOR descriptor = nullptr; if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;OW)", SDDL_REVISION_1, &descriptor, nullptr)) throw std::runtime_error("cannot secure log directory"); struct Release { PSECURITY_DESCRIPTOR p; ~Release() { LocalFree(p); } } release{descriptor}; SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), descriptor, FALSE}; std::random_device random; for (int attempt = 0; attempt < 10; ++attempt) { std::wstring path = std::wstring(root) + L"crossdesk-cli-" + std::to_wstring(random()) + L"-" + std::to_wstring(random()); if (CreateDirectoryW(path.c_str(), &security)) return Utf8(path); if (GetLastError() != ERROR_ALREADY_EXISTS) break; } throw std::runtime_error("cannot create private log directory"); } } // namespace crossdesk::cli #endif