Feature: Add terminal support for macOS client with shared PTYHandler
This commit is contained in:
270
common/PTYHandler.h
Normal file
270
common/PTYHandler.h
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
184
linux/main.cpp
184
linux/main.cpp
@@ -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)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#import "ScreenHandler.h"
|
||||
#import "InputHandler.h"
|
||||
#import "SystemManager.h"
|
||||
#import "common/PTYHandler.h"
|
||||
|
||||
// Global state
|
||||
static std::atomic<bool> 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<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)
|
||||
{
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user