From a3611d9fc1128513f1237ea20924c8faba9d0d7a Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sun, 3 May 2026 09:30:46 +0200 Subject: [PATCH] Feature: Add terminal support for macOS client with shared PTYHandler --- common/PTYHandler.h | 270 ++++++++++++++++++++++++++++++++++++++++++++ linux/main.cpp | 184 +----------------------------- macos/main.mm | 24 ++++ 3 files changed, 296 insertions(+), 182 deletions(-) create mode 100644 common/PTYHandler.h diff --git a/common/PTYHandler.h b/common/PTYHandler.h new file mode 100644 index 0000000..8c275c6 --- /dev/null +++ b/common/PTYHandler.h @@ -0,0 +1,270 @@ +/** + * PTYHandler.h - Unix Pseudo Terminal Handler + * + * This file provides pseudo terminal (PTY) functionality for remote shell access. + * + * PLATFORM SUPPORT: + * - Linux: Supported + * - macOS: Supported + * - Windows: NOT SUPPORTED (Windows uses different terminal APIs) + * + * USAGE: + * #include "common/PTYHandler.h" + * + * PTYHandler* handler = new PTYHandler(clientObject); + * clientObject->setManagerCallBack(handler, ...); + */ + +#pragma once + +#if defined(_WIN32) || defined(_WIN64) +#error "PTYHandler.h is not supported on Windows. Use Windows ConPTY or other APIs instead." +#endif + +// Platform-specific includes +#ifdef __APPLE__ +#include // macOS: openpty() +#else +#include // Linux: openpty() +#endif + +// Common Unix includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "commands.h" +#include "../client/IOCPClient.h" + +/** + * PTYHandler - Pseudo Terminal Handler + * + * Manages a pseudo terminal for remote shell access. + * Inherits from IOCPManager to integrate with the IOCP client framework. + */ +class PTYHandler : public IOCPManager +{ +public: + // Non-copyable, non-movable (owns system resources) + PTYHandler(const PTYHandler&) = delete; + PTYHandler& operator=(const PTYHandler&) = delete; + PTYHandler(PTYHandler&&) = delete; + PTYHandler& operator=(PTYHandler&&) = delete; + + PTYHandler(IOCPClient* client) : m_client(client), m_running(false), m_master_fd(-1), m_slave_fd(-1), m_child_pid(-1) + { + if (!client) { + throw std::invalid_argument("IOCPClient pointer cannot be null"); + } + + // Create pseudo terminal pair + if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) { + throw std::runtime_error("Failed to create pseudo terminal"); + } + + // Set master fd to non-blocking mode + int flags = fcntl(m_master_fd, F_GETFL, 0); + fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK); + + // Start shell process + startShell(); + } + + ~PTYHandler() + { + m_running = false; + if (m_readThread.joinable()) { + m_readThread.join(); + } + if (m_master_fd >= 0) { + close(m_master_fd); + } + if (m_slave_fd >= 0) { + close(m_slave_fd); + } + if (m_child_pid > 0) { + // Check if child is still running before killing + int status; + pid_t result = waitpid(m_child_pid, &status, WNOHANG); + if (result == 0) { + // Child still running, terminate it + kill(m_child_pid, SIGTERM); + waitpid(m_child_pid, nullptr, 0); + } + // If result == m_child_pid, child already exited and was reaped + // If result == -1, child was already reaped elsewhere + } + } + + // Start the PTY read thread + void Start() + { + bool expected = false; + if (!m_running.compare_exchange_strong(expected, true)) return; + m_readThread = std::thread(&PTYHandler::readFromPTY, this); + } + + // Handle incoming data from server + virtual VOID OnReceive(PBYTE data, ULONG size) override + { + if (size && data[0] == COMMAND_NEXT) { + Start(); + return; + } + + // Handle terminal resize command + if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) { + short cols, rows; + memcpy(&cols, data + 1, sizeof(short)); + memcpy(&rows, data + 3, sizeof(short)); + SetWindowSize(cols, rows); + return; + } + + // Write data to PTY + if (size > 0) { + ssize_t total = 0; + while (total < (ssize_t)size) { + ssize_t written = write(m_master_fd, (char*)data + total, size - total); + if (written == -1) { + if (errno == EAGAIN || errno == EINTR) continue; + break; + } + total += written; + } + } + } + + // Set terminal window size + void SetWindowSize(int cols, int rows) + { + struct winsize ws; + ws.ws_col = cols; + ws.ws_row = rows; + ws.ws_xpixel = 0; + ws.ws_ypixel = 0; + + if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) { + return; + } + + // Send SIGWINCH to child process to notify window size change + if (m_child_pid > 0) { + kill(m_child_pid, SIGWINCH); + } + } + +private: + int m_master_fd; + int m_slave_fd; + IOCPClient* m_client; + std::thread m_readThread; + std::atomic m_running; + pid_t m_child_pid; + + void startShell() + { + m_child_pid = fork(); + if (m_child_pid == -1) { + close(m_master_fd); + close(m_slave_fd); + throw std::runtime_error("Failed to fork shell process"); + } + + if (m_child_pid == 0) { + // Child process + setsid(); // Create new session, become session leader + + // Set slave PTY as controlling terminal (required for Ctrl+C to work) + // This must be done after setsid() and before dup2() + ioctl(m_slave_fd, TIOCSCTTY, 0); + + // Redirect stdin/stdout/stderr to slave PTY + dup2(m_slave_fd, STDIN_FILENO); + dup2(m_slave_fd, STDOUT_FILENO); + dup2(m_slave_fd, STDERR_FILENO); + close(m_master_fd); + close(m_slave_fd); + + // Set terminal environment for xterm.js compatibility + setenv("TERM", "xterm-256color", 1); + setenv("COLORTERM", "truecolor", 1); + +#ifdef __APPLE__ + // macOS locale settings + setenv("LANG", "en_US.UTF-8", 1); + setenv("LC_ALL", "en_US.UTF-8", 1); + + // Try zsh first (macOS default), fallback to bash + if (access("/bin/zsh", X_OK) == 0) { + execl("/bin/zsh", "zsh", "-i", nullptr); + } + execl("/bin/bash", "bash", "-i", nullptr); +#else + // Linux locale settings (C.UTF-8 is most portable) + setenv("LANG", "C.UTF-8", 1); + setenv("LC_ALL", "C.UTF-8", 1); + + // Start interactive bash + execl("/bin/bash", "bash", "-i", nullptr); +#endif + _exit(1); + } + } + + void readFromPTY() + { + char buffer[4096]; + while (m_running) { + // Check if child process has exited + int status; + pid_t result = waitpid(m_child_pid, &status, WNOHANG); + if (result == m_child_pid) { + // Shell exited, send close notification + if (m_client) { + BYTE closeToken = TOKEN_TERMINAL_CLOSE; + m_client->Send2Server((char*)&closeToken, 1); + } + m_running = false; + break; + } + + ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1); + if (bytes_read > 0) { + if (m_client) { + m_client->Send2Server(buffer, bytes_read); + } + } else if (bytes_read == 0) { + // EOF - PTY closed + if (m_client) { + BYTE closeToken = TOKEN_TERMINAL_CLOSE; + m_client->Send2Server((char*)&closeToken, 1); + } + m_running = false; + break; + } else if (bytes_read == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + usleep(10000); // 10ms + } else if (errno == EIO) { + // EIO typically means PTY slave closed (shell exited) + if (m_client) { + BYTE closeToken = TOKEN_TERMINAL_CLOSE; + m_client->Send2Server((char*)&closeToken, 1); + } + m_running = false; + break; + } else { + break; + } + } + } + } +}; diff --git a/linux/main.cpp b/linux/main.cpp index 5fa5b83..292cce3 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include "common/PTYHandler.h" #include #include #include @@ -338,187 +338,7 @@ struct RttEstimator { RttEstimator g_rttEstimator; int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新 -// 伪终端处理类:继承自IOCPManager. -class PTYHandler : public IOCPManager -{ -public: - PTYHandler(IOCPClient* client) : m_client(client), m_running(false) - { - if (!client) { - throw std::invalid_argument("IOCPClient pointer cannot be null"); - } - - // 创建伪终端 - if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) { - throw std::runtime_error("Failed to create pseudo terminal"); - } - - // 设置伪终端为非阻塞模式 - int flags = fcntl(m_master_fd, F_GETFL, 0); - fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK); - - // 启动 Shell 进程 - startShell(); - } - - ~PTYHandler() - { - m_running = false; - if (m_readThread.joinable()) m_readThread.join(); - close(m_master_fd); - close(m_slave_fd); - if (m_child_pid > 0) { - kill(m_child_pid, SIGTERM); - waitpid(m_child_pid, nullptr, 0); - } - } - - // 启动读取线程 - void Start() - { - bool expected = false; - if (!m_running.compare_exchange_strong(expected, true)) return; - m_readThread = std::thread(&PTYHandler::readFromPTY, this); - } - - virtual VOID OnReceive(PBYTE data, ULONG size) - { - if (size && data[0] == COMMAND_NEXT) { - Start(); - return; - } - // 处理终端尺寸调整命令 - if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) { - int cols = *(short*)(data + 1); - int rows = *(short*)(data + 3); - SetWindowSize(cols, rows); - return; - } - std::string s((char*)data, size); - Mprintf("%s", s.c_str()); - if (size > 0) { - ssize_t total = 0; - while (total < (ssize_t)size) { - ssize_t written = write(m_master_fd, (char*)data + total, size - total); - if (written == -1) { - if (errno == EAGAIN || errno == EINTR) continue; - Mprintf("OnReceive: write error %d\n", errno); - break; - } - total += written; - } - } - } - - // 设置终端窗口尺寸 - void SetWindowSize(int cols, int rows) - { - struct winsize ws; - ws.ws_col = cols; - ws.ws_row = rows; - ws.ws_xpixel = 0; - ws.ws_ypixel = 0; - - if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) { - Mprintf("SetWindowSize: ioctl failed %d\n", errno); - } else { - // 发送 SIGWINCH 给子进程,通知其窗口大小已改变 - if (m_child_pid > 0) { - kill(m_child_pid, SIGWINCH); - } - Mprintf("SetWindowSize: %dx%d\n", cols, rows); - } - } -private: - int m_master_fd, m_slave_fd; - IOCPClient* m_client; - std::thread m_readThread; - std::atomic m_running; - pid_t m_child_pid; - - void startShell() - { - m_child_pid = fork(); - if (m_child_pid == -1) { - close(m_master_fd); - close(m_slave_fd); - throw std::runtime_error("Failed to fork shell process"); - } - if (m_child_pid == 0) { // 子进程 - setsid(); // 创建新的会话 - dup2(m_slave_fd, STDIN_FILENO); - dup2(m_slave_fd, STDOUT_FILENO); - dup2(m_slave_fd, STDERR_FILENO); - close(m_master_fd); - close(m_slave_fd); - - // 设置完整终端支持(xterm.js 终端仿真) - setenv("TERM", "xterm-256color", 1); - setenv("COLORTERM", "truecolor", 1); - // 使用 C.UTF-8 是最通用的 UTF-8 locale,几乎所有 Linux 都支持 - setenv("LANG", "C.UTF-8", 1); - setenv("LC_ALL", "C.UTF-8", 1); - - // 启动交互式 Bash - execl("/bin/bash", "bash", "-i", nullptr); - exit(1); - } - } - - void readFromPTY() - { - char buffer[4096]; - while (m_running) { - // 检查子进程是否已退出 - int status; - pid_t result = waitpid(m_child_pid, &status, WNOHANG); - if (result == m_child_pid) { - // Shell 已退出,发送关闭通知 - Mprintf("readFromPTY: shell exited (status=%d)\n", WEXITSTATUS(status)); - if (m_client) { - BYTE closeToken = TOKEN_TERMINAL_CLOSE; - m_client->Send2Server((char*)&closeToken, 1); - } - m_running = false; - break; - } - - ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1); - if (bytes_read > 0) { - if (m_client) { - buffer[bytes_read] = '\0'; - Mprintf("%s", buffer); - m_client->Send2Server(buffer, bytes_read); - } - } else if (bytes_read == 0) { - // EOF - PTY 已关闭 - Mprintf("readFromPTY: EOF (shell closed)\n"); - if (m_client) { - BYTE closeToken = TOKEN_TERMINAL_CLOSE; - m_client->Send2Server((char*)&closeToken, 1); - } - m_running = false; - break; - } else if (bytes_read == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - usleep(10000); - } else if (errno == EIO) { - // EIO 通常表示 PTY slave 已关闭(shell 退出) - Mprintf("readFromPTY: EIO (shell closed)\n"); - if (m_client) { - BYTE closeToken = TOKEN_TERMINAL_CLOSE; - m_client->Send2Server((char*)&closeToken, 1); - } - m_running = false; - break; - } else { - Mprintf("readFromPTY: read error %d\n", errno); - break; - } - } - } - } -}; +// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS) void* ShellworkingThread(void* param) { diff --git a/macos/main.mm b/macos/main.mm index 75003e7..13bc188 100644 --- a/macos/main.mm +++ b/macos/main.mm @@ -22,6 +22,7 @@ #import "ScreenHandler.h" #import "InputHandler.h" #import "SystemManager.h" +#import "common/PTYHandler.h" // Global state static std::atomic g_running(true); @@ -615,6 +616,28 @@ struct RttEstimator { RttEstimator g_rttEstimator; int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整 +void* ShellworkingThread(void* param) +{ + try { + std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); + void* clientAddr = ClientObject.get(); + NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr); + if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { + std::unique_ptr handler(new PTYHandler(ClientObject.get())); + ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); + BYTE bToken = TOKEN_TERMINAL_START; + ClientObject->Send2Server((char*)&bToken, 1); + NSLog(@">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START", clientAddr); + while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) + Sleep(1000); + } + NSLog(@">>> Leave ShellworkingThread [%p]", clientAddr); + } catch (const std::exception& e) { + NSLog(@"*** ShellworkingThread exception: %s ***", e.what()); + } + return NULL; +} + void* ScreenworkingThread(void* param) { try { @@ -651,6 +674,7 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength) g_bExit = S_CLIENT_EXIT; g_running = false; // Stop main loop to prevent reconnection } else if (szBuffer[0] == COMMAND_SHELL) { + std::thread(ShellworkingThread, nullptr).detach(); Mprintf("** [%p] Received 'SHELL' command ***\n", user); } else if (szBuffer[0] == COMMAND_SCREEN_SPY) { std::thread(ScreenworkingThread, nullptr).detach();