mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-01 03:12:37 +08:00
491 lines
16 KiB
C++
491 lines
16 KiB
C++
#ifdef _WIN32
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include "terminal_io.h"
|
|
#include <windows.h>
|
|
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <fcntl.h>
|
|
#include <io.h>
|
|
#include <random>
|
|
#include <sddl.h>
|
|
#include <stdexcept>
|
|
#include <sys/stat.h>
|
|
#include <thread>
|
|
|
|
namespace crossdesk::cli {
|
|
std::atomic<int> 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<HANDLE>(value);
|
|
#else
|
|
return reinterpret_cast<HANDLE>(_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<int>(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<SHORT>(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<DWORD>(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<char *>(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_t>(size, available);
|
|
}
|
|
DWORD count = 0;
|
|
if (!ReadFile(handle, buffer, DWORD(std::min<size_t>(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_t>(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;
|
|
// ConPTY's console window is only a message window, and its font API
|
|
// describes the hidden console host, not the user's display terminal.
|
|
// Only trust native font metrics for a visible classic console.
|
|
HWND window = GetConsoleWindow();
|
|
CONSOLE_FONT_INFOEX font{};
|
|
font.cbSize = sizeof(font);
|
|
if (window && IsWindowVisible(window) &&
|
|
GetCurrentConsoleFontEx(handle, FALSE, &font) &&
|
|
font.dwFontSize.X > 0 && font.dwFontSize.X <= 1024 &&
|
|
font.dwFontSize.Y > 0 && font.dwFontSize.Y <= 1024) {
|
|
size.pixel_width = size.columns * font.dwFontSize.X;
|
|
size.pixel_height = size.rows * font.dwFontSize.Y;
|
|
}
|
|
}
|
|
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>()) {
|
|
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<State>(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
|