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

View File

@@ -14,7 +14,7 @@
#include <csignal>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pty.h>
#include "common/PTYHandler.h"
#include <iostream>
#include <stdexcept>
#include <cstdio>
@@ -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<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;
}
}
}
}
};
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
void* ShellworkingThread(void* param)
{