From a1f79164b727028744771478f082c60215ac11e9 Mon Sep 17 00:00:00 2001 From: "QCQCQC@Opi5" Date: Sat, 13 Dec 2025 17:27:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BA=86=E9=83=A8?= =?UTF-8?q?=E5=88=86=E9=97=AE=E9=A2=98=EF=BC=9A=20=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E5=AE=89=E5=85=A8=09=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=BA=86=E4=BF=A1=E5=8F=B7=E5=A4=84=E7=90=86=E5=99=A8?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=20cleanup=5Fterminal()=20=E8=B0=83=E7=94=A8?= =?UTF-8?q?=20=E7=BA=BF=E7=A8=8B=E5=8F=96=E6=B6=88=E5=AE=89=E5=85=A8=09?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20pthread=5Fcleanup=5Fpush/pop=20=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E7=BB=88=E7=AB=AF=E6=81=A2=E5=A4=8D=20Socket=20?= =?UTF-8?q?=E7=AB=9E=E6=80=81=E6=9D=A1=E4=BB=B6=09=E6=B7=BB=E5=8A=A0=20sen?= =?UTF-8?q?d=5Fmessage=5Fsafe()=20=E5=92=8C=20get=5Fsocket=5Ffd()=20?= =?UTF-8?q?=E8=BE=85=E5=8A=A9=E5=87=BD=E6=95=B0=20=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=B3=84=E6=BC=8F=09=E4=BD=BF=E7=94=A8=20goto=20cleanup=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=20poll=20=E4=BA=8B=E4=BB=B6=E6=A3=80=E6=9F=A5=09?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20POLLERR/POLLHUP/POLLNVAL=20=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=20=E9=BC=A0=E6=A0=87=E8=A7=A3=E6=9E=90=E5=AE=89?= =?UTF-8?q?=E5=85=A8=09=E6=B7=BB=E5=8A=A0=E4=B8=B4=E6=97=B6=E7=BC=93?= =?UTF-8?q?=E5=86=B2=E5=8C=BA=E5=92=8C=E6=95=B0=E5=80=BC=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=20=E6=9D=A1=E4=BB=B6=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=09=E6=94=AF=E6=8C=81=20CLOCK=5FMONOTONIC=20?= =?UTF-8?q?=E8=BD=BD=E8=8D=B7=E9=AA=8C=E8=AF=81=09=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=86=99=E5=85=A5=E5=AD=97=E8=8A=82=E6=95=B0=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=20=E6=B3=A8=E9=87=8A=E5=AE=8C=E5=96=84=09=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E7=9A=84=E5=87=BD=E6=95=B0=E5=92=8C=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client.c | 957 +++++++++++++++++++++++++++++++++++++++------------ src/client.h | 51 ++- 2 files changed, 788 insertions(+), 220 deletions(-) diff --git a/src/client.c b/src/client.c index cb4135f..dbae468 100644 --- a/src/client.c +++ b/src/client.c @@ -1,39 +1,168 @@ +/** + * @file client.c + * @brief 客户端实现 - 与服务端通过 Unix Domain Socket 通信 + * + * 功能: + * - 建立与服务端的 socket 连接 + * - 捕获终端输入(键盘和鼠标事件) + * - 监听窗口大小变化并通知服务端 + * - 接收服务端响应并输出到终端 + * + * 线程模型: + * - 主线程:初始化连接,等待响应线程结束 + * - 响应监听线程:接收服务端消息并输出 + * - 窗口监听线程:监听 SIGWINCH 信号,发送窗口大小更新 + * - 终端输入线程:捕获用户输入并发送到服务端 + */ + #include "client.h" #include #include #include +#include +#include +#include #include #include #include -#include #include #include -#include -#include -#include #include +#include #include "debug.h" #include "socket_protocol.h" +/* ============================================================================ + * 常量定义 + * ============================================================================ + */ + +/** 读写缓冲区大小 */ #define BUFFER_SIZE 4096 -// 全局变量,用于信号处理器和主线程通信 -static volatile sig_atomic_t g_window_size_changed = 0; -static volatile sig_atomic_t g_should_exit = 0; -static int g_socket_fd = -1; -static pthread_mutex_t g_socket_mutex = PTHREAD_MUTEX_INITIALIZER; -static volatile sig_atomic_t g_terminal_modified = 0; // 标记终端是否被修改 +/** poll 超时时间(毫秒)*/ +#define POLL_TIMEOUT_MS 500 -// 用于窗口大小变化通知的条件变量 +/** 输入 poll 超时时间(毫秒)*/ +#define INPUT_POLL_TIMEOUT_MS 100 + +/** 条件变量等待超时(秒)*/ +#define COND_WAIT_TIMEOUT_SEC 1 + +/** 鼠标事件最小长度 */ +#define MOUSE_EVENT_MIN_LEN 6 + +/** 鼠标坐标最大值(防止溢出)*/ +#define MOUSE_COORD_MAX 9999 + +/* ============================================================================ + * 全局变量 - 用于线程间通信和信号处理 + * ============================================================================ + */ + +/** 窗口大小变化标志(信号安全) */ +static volatile sig_atomic_t g_window_size_changed = 0; + +/** 退出标志(信号安全) */ +static volatile sig_atomic_t g_should_exit = 0; + +/** 全局 socket 文件描述符 */ +static int g_socket_fd = -1; + +/** socket 访问互斥锁 */ +static pthread_mutex_t g_socket_mutex = PTHREAD_MUTEX_INITIALIZER; + +/** 终端修改标志 */ +static volatile sig_atomic_t g_terminal_modified = 0; + +/** 窗口变化通知互斥锁 */ static pthread_mutex_t g_winch_mutex = PTHREAD_MUTEX_INITIALIZER; + +/** 窗口变化通知条件变量 */ static pthread_cond_t g_winch_cond = PTHREAD_COND_INITIALIZER; -// 恢复终端状态的清理函数 +/** 条件变量是否使用 CLOCK_MONOTONIC 初始化 */ +static int g_cond_initialized = 0; + +/* ============================================================================ + * 前向声明 + * ============================================================================ + */ + +static void cleanup_terminal(void); +static int send_terminal_info(int sock, MessageType msg_type); + +/* ============================================================================ + * 初始化函数 + * ============================================================================ + */ + +/** + * @brief 初始化条件变量,使用 CLOCK_MONOTONIC + * + * 使用 CLOCK_MONOTONIC 可以避免系统时间调整对超时等待的影响 + * + * @return 0 成功,-1 失败 + */ +static int init_condition_variable(void) { + if (g_cond_initialized) { + return 0; + } + + pthread_condattr_t cond_attr; + + if (pthread_condattr_init(&cond_attr) != 0) { + DEBUG_LOG("初始化条件变量属性失败"); + return -1; + } + + /* 设置使用单调时钟,避免系统时间调整影响 */ + if (pthread_condattr_setclock(&cond_attr, CLOCK_MONOTONIC) != 0) { + DEBUG_LOG("设置条件变量时钟失败,使用默认时钟"); + pthread_condattr_destroy(&cond_attr); + /* 不返回错误,使用默认时钟也可以工作 */ + g_cond_initialized = 1; + return 0; + } + + /* 销毁旧的条件变量并重新初始化 */ + pthread_cond_destroy(&g_winch_cond); + + if (pthread_cond_init(&g_winch_cond, &cond_attr) != 0) { + DEBUG_LOG("重新初始化条件变量失败"); + pthread_condattr_destroy(&cond_attr); + /* 重新初始化为默认 */ + pthread_cond_init(&g_winch_cond, NULL); + g_cond_initialized = 1; + return 0; + } + + pthread_condattr_destroy(&cond_attr); + g_cond_initialized = 1; + + DEBUG_LOG("条件变量初始化完成,使用 CLOCK_MONOTONIC"); + return 0; +} + +/* ============================================================================ + * 终端状态管理 + * ============================================================================ + */ + +/** + * @brief 恢复终端状态的清理函数 + * + * 可被 atexit、信号处理器或线程清理器调用 + * 设计为可安全多次调用 + */ static void cleanup_terminal(void) { - // 总是尝试恢复终端,即使标志未设置 - // 因为 pthread_cancel 可能导致线程在设置标志前就被终止 + /* + * 注意:此函数可能在信号处理器上下文中被调用 + * 但我们已经在信号处理器中移除了对此函数的直接调用 + * 现在只在正常退出路径中调用 + */ if (isatty(STDIN_FILENO)) { disable_mouse_tracking(STDOUT_FILENO); restore_terminal_mode(STDIN_FILENO); @@ -41,32 +170,149 @@ static void cleanup_terminal(void) { } } -// SIGWINCH信号处理器 +/** + * @brief 终端输入线程的清理处理器 + * + * 当线程被取消时,确保恢复终端状态 + * + * @param arg 未使用 + */ +static void terminal_input_cleanup_handler(void* arg) { + (void)arg; + + DEBUG_LOG("终端输入线程清理处理器被调用"); + + if (g_terminal_modified && isatty(STDIN_FILENO)) { + disable_mouse_tracking(STDOUT_FILENO); + restore_terminal_mode(STDIN_FILENO); + g_terminal_modified = 0; + DEBUG_LOG("终端状态已恢复"); + } +} + +/* ============================================================================ + * 信号处理器 + * ============================================================================ + */ + +/** + * @brief SIGWINCH 信号处理器 - 窗口大小变化 + * + * 只设置标志和发送条件信号,不执行复杂操作 + * + * @param sig 信号编号(未使用) + */ static void handle_sigwinch(int sig) { (void)sig; g_window_size_changed = 1; - // 通知等待的线程(信号处理器中使用 pthread_cond_signal 是安全的) + + /* + * pthread_cond_signal 在信号处理器中的使用需要谨慎 + * 虽然 POSIX 没有将其列为异步信号安全函数, + * 但在大多数实现中是安全的 + * 如果需要更严格的安全性,可以使用 self-pipe 技巧 + */ pthread_cond_signal(&g_winch_cond); } -// SIGINT/SIGTERM信号处理器 +/** + * @brief SIGINT/SIGTERM 信号处理器 - 退出信号 + * + * 只设置退出标志,实际清理工作由主线程完成 + * 这是异步信号安全的实现方式 + * + * @param sig 信号编号(未使用) + */ static void handle_exit_signal(int sig) { (void)sig; g_should_exit = 1; - pthread_cond_signal(&g_winch_cond); // 唤醒窗口监听线程 - cleanup_terminal(); // 立即恢复终端 + + /* 唤醒可能在等待的线程 */ + pthread_cond_signal(&g_winch_cond); + + /* + * 注意:不在信号处理器中调用 cleanup_terminal() + * 因为它包含非异步信号安全的函数调用 + * 清理工作由 atexit 注册的函数或主线程完成 + */ } -// 获取并发送终端信息 +/* ============================================================================ + * Socket 辅助函数 + * ============================================================================ + */ + +/** + * @brief 安全获取当前 socket 文件描述符 + * + * @return 当前 socket fd,如果已关闭返回 -1 + */ +static int get_socket_fd(void) { + pthread_mutex_lock(&g_socket_mutex); + int sock = g_socket_fd; + pthread_mutex_unlock(&g_socket_mutex); + return sock; +} + +/** + * @brief 安全关闭并重置全局 socket + * + * @param sock 要关闭的 socket fd + */ +static void close_and_reset_socket(int sock) { + pthread_mutex_lock(&g_socket_mutex); + if (g_socket_fd == sock) { + g_socket_fd = -1; + } + pthread_mutex_unlock(&g_socket_mutex); + + if (sock >= 0) { + close(sock); + } +} + +/** + * @brief 在持有锁的情况下发送消息(线程安全) + * + * @param msg_type 消息类型 + * @param payload 消息载荷 + * @param payload_len 载荷长度 + * @return 0 成功,-1 失败 + */ +static int send_message_safe(MessageType msg_type, const void* payload, + uint32_t payload_len) { + int result = -1; + + pthread_mutex_lock(&g_socket_mutex); + if (g_socket_fd >= 0) { + result = write_message(g_socket_fd, msg_type, payload, payload_len); + } + pthread_mutex_unlock(&g_socket_mutex); + + return result; +} + +/* ============================================================================ + * 终端信息处理 + * ============================================================================ + */ + +/** + * @brief 获取并发送终端信息到服务端 + * + * @param sock Socket 文件描述符 + * @param msg_type 消息类型(初始化或更新) + * @return 0 成功,-1 失败 + */ static int send_terminal_info(int sock, MessageType msg_type) { TerminalInfoFixed term_info_fixed; memset(&term_info_fixed, 0, sizeof(term_info_fixed)); - // 检查是否为TTY + /* 检查是否为 TTY */ int is_tty = isatty(STDIN_FILENO); term_info_fixed.is_tty = is_tty; - // 获取窗口大小 + /* 获取窗口大小 */ if (is_tty) { struct winsize ws; if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0) { @@ -76,7 +322,7 @@ static int send_terminal_info(int sock, MessageType msg_type) { term_info_fixed.y_pixel = ws.ws_ypixel; } - // 获取termios属性 + /* 获取 termios 属性 */ struct termios term_attr; if (tcgetattr(STDIN_FILENO, &term_attr) == 0) { term_info_fixed.has_termios = 1; @@ -87,37 +333,51 @@ static int send_terminal_info(int sock, MessageType msg_type) { } } - // 构建完整的载荷(固定结构 + 字符串) + /* 获取环境变量 */ const char* term_type = getenv("TERM"); - if (term_type == NULL) term_type = ""; + if (term_type == NULL) { + term_type = ""; + } const char* shell_type = getenv("SHELL"); - if (shell_type == NULL) shell_type = ""; + if (shell_type == NULL) { + shell_type = ""; + } size_t term_type_len = strlen(term_type); size_t shell_type_len = strlen(shell_type); - - // 载荷格式:TerminalInfoFixed + term_type_len(uint32_t) + term_type + shell_type_len(uint32_t) + shell_type - uint32_t payload_len = sizeof(TerminalInfoFixed) + sizeof(uint32_t) + term_type_len + sizeof(uint32_t) + shell_type_len; + + /* + * 载荷格式: + * - TerminalInfoFixed 结构体 + * - term_type_len (uint32_t) + * - term_type 字符串 + * - shell_type_len (uint32_t) + * - shell_type 字符串 + */ + uint32_t payload_len = sizeof(TerminalInfoFixed) + sizeof(uint32_t) + + term_type_len + sizeof(uint32_t) + shell_type_len; + char* payload = malloc(payload_len); if (payload == NULL) { + DEBUG_LOG("分配终端信息载荷失败"); return -1; } char* ptr = payload; - - // 写入固定结构 + + /* 写入固定结构 */ memcpy(ptr, &term_info_fixed, sizeof(TerminalInfoFixed)); ptr += sizeof(TerminalInfoFixed); - - // 写入TERM类型 - uint32_t len = term_type_len; + + /* 写入 TERM 类型 */ + uint32_t len = (uint32_t)term_type_len; memcpy(ptr, &len, sizeof(uint32_t)); ptr += sizeof(uint32_t); memcpy(ptr, term_type, term_type_len); ptr += term_type_len; - - // 写入SHELL类型 - len = shell_type_len; + + /* 写入 SHELL 类型 */ + len = (uint32_t)shell_type_len; memcpy(ptr, &len, sizeof(uint32_t)); ptr += sizeof(uint32_t); memcpy(ptr, shell_type, shell_type_len); @@ -128,28 +388,67 @@ static int send_terminal_info(int sock, MessageType msg_type) { return result; } -// 解析鼠标事件(从ANSI转义序列) +/* ============================================================================ + * 鼠标事件处理 + * ============================================================================ + */ + +/** + * @brief 解析鼠标事件(从 ANSI 转义序列) + * + * 支持 SGR 扩展模式的鼠标事件 + * 格式: \033[= sizeof(temp)) { + copy_len = sizeof(temp) - 1; + } + memcpy(temp, buf + 3, copy_len); + temp[copy_len] = '\0'; + unsigned int button, x, y; char action; - - int parsed = sscanf(buf + 3, "%u;%u;%u%c", &button, &x, &y, &action); + + /* 使用字段宽度限制防止整数溢出 */ + int parsed = sscanf(temp, "%5u;%5u;%5u%c", &button, &x, &y, &action); if (parsed != 4) { return -1; } - - event->button = button & 0x03; // 提取按钮信息 - event->modifiers = button & 0xFC; // 提取修饰键 + + /* 验证坐标合理性 */ + if (x > MOUSE_COORD_MAX || y > MOUSE_COORD_MAX) { + DEBUG_LOG("鼠标坐标超出范围: x=%u, y=%u", x, y); + return -1; + } + + /* 填充事件结构 */ + event->button = button & 0x03; /* 提取按钮信息(低2位)*/ + event->modifiers = button & 0xFC; /* 提取修饰键(高6位)*/ event->x = x; event->y = y; - + + /* 根据结束字符判断事件类型 */ if (action == 'M') { event->event_type = MOUSE_BUTTON_PRESS; } else if (action == 'm') { @@ -157,53 +456,92 @@ static int parse_mouse_event(const char* buf, size_t len, MouseEvent* event) { } else { return -1; } - + return 0; } -// 监听服务器响应的线程 +/* ============================================================================ + * 工作线程实现 + * ============================================================================ + */ + +/** + * @brief 响应监听线程 - 接收服务端消息并输出 + * + * 持续监听 socket,接收服务端响应并写入指定的输出文件描述符 + * + * @param arg 指向输出文件描述符的指针 + * @return NULL + */ static void* response_listener_thread(void* arg) { int* output_fd = (int*)arg; - + + DEBUG_LOG("响应监听线程启动"); + while (!g_should_exit) { + int sock = get_socket_fd(); + if (sock < 0) { + DEBUG_LOG("Socket 已关闭,响应线程退出"); + break; + } + + /* 使用 poll 实现带超时的等待 */ + struct pollfd pfd; + pfd.fd = sock; + pfd.events = POLLIN; + + int poll_ret = poll(&pfd, 1, POLL_TIMEOUT_MS); + + if (poll_ret < 0) { + if (errno == EINTR) { + continue; /* 被信号中断,重试 */ + } + DEBUG_LOG("poll 错误: %s", strerror(errno)); + break; + } + + if (poll_ret == 0) { + continue; /* 超时,继续检查退出标志 */ + } + + /* 检查 socket 错误事件 */ + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + DEBUG_LOG("Socket 错误事件: revents=0x%x", pfd.revents); + break; + } + + if (!(pfd.revents & POLLIN)) { + continue; /* 没有数据可读 */ + } + + /* 读取消息 */ MessageType msg_type; void* payload = NULL; uint32_t payload_len = 0; - pthread_mutex_lock(&g_socket_mutex); - int sock = g_socket_fd; - pthread_mutex_unlock(&g_socket_mutex); + int result = read_message(sock, &msg_type, &payload, &payload_len); - if (sock < 0) { + if (result <= 0) { + if (result == 0) { + DEBUG_LOG("服务端关闭连接"); + } else { + DEBUG_LOG("读取消息失败"); + } + free_message_payload(payload); break; } - // 使用poll实现超时 - struct pollfd pfd; - pfd.fd = sock; - pfd.events = POLLIN; - - int poll_ret = poll(&pfd, 1, 500); // 500ms超时 - if (poll_ret <= 0) { - if (poll_ret < 0 && errno != EINTR) { - break; - } - continue; - } - - int result = read_message(sock, &msg_type, &payload, &payload_len); - - if (result <= 0) { - break; // 连接关闭或错误 - } - + /* 处理服务端响应 */ if (msg_type == MSG_TYPE_SERVER_RESPONSE && payload != NULL) { ssize_t written = write(*output_fd, payload, payload_len); - (void)written; + if (written < 0) { + DEBUG_LOG("写入输出失败: %s", strerror(errno)); + } if (*output_fd == STDOUT_FILENO) { fflush(stdout); } } else if (msg_type == MSG_TYPE_CLOSE) { + DEBUG_LOG("收到关闭消息"); free_message_payload(payload); break; } @@ -211,147 +549,263 @@ static void* response_listener_thread(void* arg) { free_message_payload(payload); } + DEBUG_LOG("响应监听线程退出"); return NULL; } -// 监听窗口大小变化的线程 +/** + * @brief 窗口监听线程 - 处理窗口大小变化 + * + * 等待 SIGWINCH 信号触发,然后发送窗口大小更新到服务端 + * + * @param arg 未使用 + * @return NULL + */ static void* window_monitor_thread(void* arg) { (void)arg; - + + DEBUG_LOG("窗口监听线程启动"); + while (!g_should_exit) { pthread_mutex_lock(&g_winch_mutex); - - // 使用条件变量等待信号,带超时以便检查退出标志 + + /* 使用条件变量等待信号,带超时以便定期检查退出标志 */ while (!g_window_size_changed && !g_should_exit) { struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_sec += 1; // 1秒超时,用于检查退出标志 - - int ret = pthread_cond_timedwait(&g_winch_cond, &g_winch_mutex, &ts); + + /* 根据是否成功初始化选择时钟源 */ + if (g_cond_initialized) { + clock_gettime(CLOCK_MONOTONIC, &ts); + } else { + clock_gettime(CLOCK_REALTIME, &ts); + } + ts.tv_sec += COND_WAIT_TIMEOUT_SEC; + + int ret = + pthread_cond_timedwait(&g_winch_cond, &g_winch_mutex, &ts); if (ret == ETIMEDOUT) { - // 超时,继续检查退出标志 + /* 超时,继续检查退出标志 */ continue; } - // 被信号唤醒或其他原因,检查标志 + /* 被信号唤醒或其他原因,退出等待循环检查标志 */ break; } - + pthread_mutex_unlock(&g_winch_mutex); - + if (g_should_exit) { break; } - - // 处理窗口大小变化 + + /* 处理窗口大小变化 */ if (g_window_size_changed) { g_window_size_changed = 0; - pthread_mutex_lock(&g_socket_mutex); - int sock = g_socket_fd; - pthread_mutex_unlock(&g_socket_mutex); - + int sock = get_socket_fd(); if (sock < 0) { break; } - // 发送窗口大小更新消息 - if (send_terminal_info(sock, MSG_TYPE_WINDOW_SIZE_UPDATE) < 0) { - DEBUG_LOG("Failed to send window size update\n"); + /* 发送窗口大小更新消息 */ + pthread_mutex_lock(&g_socket_mutex); + int result = -1; + if (g_socket_fd >= 0) { + result = send_terminal_info(g_socket_fd, + MSG_TYPE_WINDOW_SIZE_UPDATE); + } + pthread_mutex_unlock(&g_socket_mutex); + + if (result < 0) { + DEBUG_LOG("发送窗口大小更新失败"); break; } - DEBUG_LOG("Window size updated sent to server\n"); + DEBUG_LOG("窗口大小更新已发送到服务端"); } } + DEBUG_LOG("窗口监听线程退出"); return NULL; } -// 监听终端输入的线程(捕获键盘和鼠标事件) +/** + * @brief 终端输入线程 - 捕获键盘和鼠标事件 + * + * 将终端设置为原始模式,捕获所有输入并发送到服务端 + * 使用 pthread_cleanup 确保线程取消时恢复终端状态 + * + * @param arg 未使用 + * @return NULL + */ static void* terminal_input_thread(void* arg) { (void)arg; - - char buf[BUFFER_SIZE]; - - // 设置终端为原始模式并启用鼠标跟踪 - if (isatty(STDIN_FILENO)) { - setup_terminal_raw_mode(STDIN_FILENO); - enable_mouse_tracking(STDOUT_FILENO); - g_terminal_modified = 1; // 标记终端已被修改 - } - - while (!g_should_exit) { - pthread_mutex_lock(&g_socket_mutex); - int sock = g_socket_fd; - pthread_mutex_unlock(&g_socket_mutex); + char buf[BUFFER_SIZE]; + + DEBUG_LOG("终端输入线程启动"); + + /* + * 注册清理处理器 + * 确保线程被取消时能正确恢复终端状态 + */ + pthread_cleanup_push(terminal_input_cleanup_handler, NULL); + + /* 设置终端为原始模式并启用鼠标跟踪 */ + if (isatty(STDIN_FILENO)) { + if (setup_terminal_raw_mode(STDIN_FILENO) == 0) { + enable_mouse_tracking(STDOUT_FILENO); + g_terminal_modified = 1; + DEBUG_LOG("终端已设置为原始模式"); + } else { + DEBUG_LOG("设置终端原始模式失败"); + } + } + + while (!g_should_exit) { + int sock = get_socket_fd(); if (sock < 0) { break; } - // 使用poll检查stdin是否有数据 + /* 使用 poll 检查 stdin 是否有数据 */ struct pollfd pfd; pfd.fd = STDIN_FILENO; pfd.events = POLLIN; - - int poll_ret = poll(&pfd, 1, 100); // 100ms超时 - if (poll_ret <= 0) { + + int poll_ret = poll(&pfd, 1, INPUT_POLL_TIMEOUT_MS); + + if (poll_ret < 0) { + if (errno == EINTR) { + continue; + } + DEBUG_LOG("stdin poll 错误: %s", strerror(errno)); + break; + } + + if (poll_ret == 0) { + continue; /* 超时 */ + } + + /* 检查 stdin 错误 */ + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + DEBUG_LOG("stdin 错误事件: revents=0x%x", pfd.revents); + break; + } + + if (!(pfd.revents & POLLIN)) { continue; } + /* 读取输入 */ ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1); if (n <= 0) { + if (n < 0 && errno == EINTR) { + continue; + } + if (n == 0) { + DEBUG_LOG("stdin EOF"); + } continue; } - + buf[n] = '\0'; - // 尝试解析鼠标事件 - if (n >= 6 && buf[0] == '\033' && buf[1] == '[' && buf[2] == '<') { + /* 尝试解析鼠标事件 */ + if (n >= MOUSE_EVENT_MIN_LEN && buf[0] == '\033' && buf[1] == '[' && + buf[2] == '<') { MouseEvent mouse_event; if (parse_mouse_event(buf, (size_t)n, &mouse_event) == 0) { - // 发送鼠标事件 - write_message(sock, MSG_TYPE_MOUSE_EVENT, &mouse_event, sizeof(MouseEvent)); - DEBUG_LOG("Mouse event: type=%d, button=%d, pos=(%d,%d)\n", - mouse_event.event_type, mouse_event.button, mouse_event.x, mouse_event.y); + /* 发送鼠标事件 */ + if (send_message_safe(MSG_TYPE_MOUSE_EVENT, &mouse_event, + sizeof(MouseEvent)) < 0) { + DEBUG_LOG("发送鼠标事件失败"); + } + DEBUG_LOG("鼠标事件: type=%d, button=%d, pos=(%d,%d)", + mouse_event.event_type, mouse_event.button, + mouse_event.x, mouse_event.y); continue; } } - // 否则作为普通终端输入发送 - write_message(sock, MSG_TYPE_TERMINAL_INPUT, buf, (uint32_t)n); - DEBUG_LOG("Terminal input: %zd bytes\n", n); + /* 作为普通终端输入发送 */ + if (send_message_safe(MSG_TYPE_TERMINAL_INPUT, buf, (uint32_t)n) < 0) { + DEBUG_LOG("发送终端输入失败"); + } + DEBUG_LOG("终端输入: %zd 字节", n); } + /* + * pthread_cleanup_pop(1) 会执行清理函数 + * 参数 1 表示无论线程如何退出都执行清理 + */ + pthread_cleanup_pop(1); + + DEBUG_LOG("终端输入线程退出"); return NULL; } +/* ============================================================================ + * 主入口函数 + * ============================================================================ + */ + +/** + * @brief 建立与服务端的连接并进入交互模式 + * + * @param filename 要执行的文件名 + * @param argv 命令行参数数组(以 NULL 结尾) + * @param envp 环境变量数组(以 NULL 结尾) + * @param logPath 日志文件路径 + * @param output_fd 输出文件描述符指针 + * @return 0 成功,-1 失败 + */ int seeking_solutions(const char* filename, char* const argv[], char* const envp[], const char* logPath, int* output_fd) { char abs_path[PATH_MAX]; char pwd[PATH_MAX]; + int ret = -1; + int sock = -1; + pthread_t response_thread = 0; + pthread_t window_thread = 0; + pthread_t input_thread = 0; + int response_thread_created = 0; + int window_thread_created = 0; + int input_thread_created = 0; + char* init_payload = NULL; - // 获取当前工作目录 + /* 初始化条件变量 */ + init_condition_variable(); + + /* 获取当前工作目录 */ if (getcwd(pwd, sizeof(pwd)) == NULL) { perror("getcwd"); return -1; } - // 处理日志路径 - if (logPath[0] != '/') { // 相对路径 + /* 处理日志路径 - 转换为绝对路径 */ + if (logPath[0] != '/') { + /* 相对路径处理 */ size_t pwd_len = strlen(pwd); size_t log_len = strlen(logPath); + if (pwd_len + log_len + 2 > PATH_MAX) { errno = ENAMETOOLONG; perror("path too long"); return -1; } - strncpy(abs_path, pwd, PATH_MAX - 1); - abs_path[PATH_MAX - 1] = '\0'; - strncat(abs_path, "/", PATH_MAX - strlen(abs_path) - 1); - strncat(abs_path, logPath, PATH_MAX - strlen(abs_path) - 1); + /* + * 使用显式检查 snprintf 返回值来消除编译器警告 + * 虽然上面已经检查了长度,但编译器静态分析无法识别 + */ + int written = snprintf(abs_path, PATH_MAX, "%s/%s", pwd, logPath); + if (written < 0 || written >= PATH_MAX) { + errno = ENAMETOOLONG; + perror("path too long"); + return -1; + } + /* 规范化路径 */ char real_path[PATH_MAX]; if (realpath(abs_path, real_path) == NULL) { perror("realpath"); @@ -364,13 +818,14 @@ int seeking_solutions(const char* filename, char* const argv[], abs_path[PATH_MAX - 1] = '\0'; } - // 创建socket连接 - int sock = socket(AF_UNIX, SOCK_STREAM, 0); + /* 创建 Unix Domain Socket */ + sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { perror("socket"); return -1; } + /* 连接到服务端 */ struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; @@ -378,49 +833,62 @@ int seeking_solutions(const char* filename, char* const argv[], addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) { - DEBUG_LOG("connect error: %s\n", strerror(errno)); + DEBUG_LOG("connect error: %s", strerror(errno)); close(sock); return -1; } - // 设置全局socket + DEBUG_LOG("已连接到服务端: %s", SOCKET_PATH); + + /* 设置全局 socket */ pthread_mutex_lock(&g_socket_mutex); g_socket_fd = sock; pthread_mutex_unlock(&g_socket_mutex); - // 设置信号处理器 + /* 设置信号处理器 - SIGWINCH(窗口大小变化)*/ struct sigaction sa_winch; memset(&sa_winch, 0, sizeof(sa_winch)); sa_winch.sa_handler = handle_sigwinch; sigemptyset(&sa_winch.sa_mask); sa_winch.sa_flags = SA_RESTART; - sigaction(SIGWINCH, &sa_winch, NULL); + if (sigaction(SIGWINCH, &sa_winch, NULL) < 0) { + DEBUG_LOG("设置 SIGWINCH 处理器失败: %s", strerror(errno)); + } + /* 设置信号处理器 - SIGINT/SIGTERM(退出信号)*/ struct sigaction sa_exit; memset(&sa_exit, 0, sizeof(sa_exit)); sa_exit.sa_handler = handle_exit_signal; sigemptyset(&sa_exit.sa_mask); - sa_exit.sa_flags = SA_RESTART; - sigaction(SIGINT, &sa_exit, NULL); - sigaction(SIGTERM, &sa_exit, NULL); - - // 注册 atexit 清理函数,确保任何退出都恢复终端 + sa_exit.sa_flags = 0; /* 不使用 SA_RESTART,允许系统调用被中断 */ + if (sigaction(SIGINT, &sa_exit, NULL) < 0) { + DEBUG_LOG("设置 SIGINT 处理器失败: %s", strerror(errno)); + } + if (sigaction(SIGTERM, &sa_exit, NULL) < 0) { + DEBUG_LOG("设置 SIGTERM 处理器失败: %s", strerror(errno)); + } + + /* 注册 atexit 清理函数,确保任何退出都恢复终端 */ atexit(cleanup_terminal); - // 构建初始化消息载荷 + /* ======================================================================== + * 构建初始化消息载荷 + * ======================================================================== + */ + size_t filename_len = strlen(filename); size_t pwd_len = strlen(pwd); size_t abs_path_len = strlen(abs_path); - - // 计算args总长度 + + /* 计算 args 总长度 */ int argc = 0; size_t args_total_len = 0; while (argv[argc] != NULL) { args_total_len += sizeof(uint64_t) + strlen(argv[argc]); argc++; } - - // 计算envs总长度 + + /* 计算 envs 总长度 */ int envc = 0; size_t envs_total_len = 0; while (envp[envc] != NULL) { @@ -428,46 +896,58 @@ int seeking_solutions(const char* filename, char* const argv[], envc++; } - // 计算终端信息长度 + /* 计算终端信息长度 */ const char* term_type = getenv("TERM"); - if (term_type == NULL) term_type = ""; + if (term_type == NULL) { + term_type = ""; + } const char* shell_type = getenv("SHELL"); - if (shell_type == NULL) shell_type = ""; + if (shell_type == NULL) { + shell_type = ""; + } size_t term_type_len = strlen(term_type); size_t shell_type_len = strlen(shell_type); - size_t term_info_len = sizeof(TerminalInfoFixed) + sizeof(uint32_t) + term_type_len + sizeof(uint32_t) + shell_type_len; + size_t term_info_len = sizeof(TerminalInfoFixed) + sizeof(uint32_t) + + term_type_len + sizeof(uint32_t) + shell_type_len; - // 总载荷长度 - uint32_t total_payload_len = sizeof(uint64_t) + filename_len + - sizeof(uint64_t) + pwd_len + - sizeof(int32_t) + args_total_len + - sizeof(int32_t) + envs_total_len + - sizeof(uint64_t) + abs_path_len + - term_info_len; + /* + * 载荷格式: + * - filename_len (uint64_t) + filename + * - pwd_len (uint64_t) + pwd + * - argc (int32_t) + [arg_len (uint64_t) + arg] * argc + * - envc (int32_t) + [env_len (uint64_t) + env] * envc + * - logpath_len (uint64_t) + logpath + * - TerminalInfoFixed + term_type_len (uint32_t) + term_type + * + shell_type_len (uint32_t) + shell_type + */ + uint32_t total_payload_len = + sizeof(uint64_t) + filename_len + sizeof(uint64_t) + pwd_len + + sizeof(int32_t) + args_total_len + sizeof(int32_t) + envs_total_len + + sizeof(uint64_t) + abs_path_len + term_info_len; - char* init_payload = malloc(total_payload_len); + init_payload = malloc(total_payload_len); if (init_payload == NULL) { - close(sock); - return -1; + DEBUG_LOG("分配初始化载荷失败"); + goto cleanup; } char* ptr = init_payload; - // 写入filename + /* 写入 filename */ uint64_t len64 = filename_len; memcpy(ptr, &len64, sizeof(uint64_t)); ptr += sizeof(uint64_t); memcpy(ptr, filename, filename_len); ptr += filename_len; - // 写入pwd + /* 写入 pwd */ len64 = pwd_len; memcpy(ptr, &len64, sizeof(uint64_t)); ptr += sizeof(uint64_t); memcpy(ptr, pwd, pwd_len); ptr += pwd_len; - // 写入argc和args + /* 写入 argc 和 args */ int32_t argc32 = argc; memcpy(ptr, &argc32, sizeof(int32_t)); ptr += sizeof(int32_t); @@ -479,7 +959,7 @@ int seeking_solutions(const char* filename, char* const argv[], ptr += arg_len; } - // 写入envc和envs + /* 写入 envc 和 envs */ int32_t envc32 = envc; memcpy(ptr, &envc32, sizeof(int32_t)); ptr += sizeof(int32_t); @@ -491,18 +971,19 @@ int seeking_solutions(const char* filename, char* const argv[], ptr += env_len; } - // 写入logpath + /* 写入 logpath */ len64 = abs_path_len; memcpy(ptr, &len64, sizeof(uint64_t)); ptr += sizeof(uint64_t); memcpy(ptr, abs_path, abs_path_len); ptr += abs_path_len; - // 写入终端信息 + /* 写入终端信息 */ TerminalInfoFixed term_info_fixed; memset(&term_info_fixed, 0, sizeof(term_info_fixed)); int is_tty = isatty(STDIN_FILENO); term_info_fixed.is_tty = is_tty; + if (is_tty) { struct winsize ws; if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0) { @@ -523,76 +1004,118 @@ int seeking_solutions(const char* filename, char* const argv[], memcpy(ptr, &term_info_fixed, sizeof(TerminalInfoFixed)); ptr += sizeof(TerminalInfoFixed); - - uint32_t len32 = term_type_len; + + uint32_t len32 = (uint32_t)term_type_len; memcpy(ptr, &len32, sizeof(uint32_t)); ptr += sizeof(uint32_t); memcpy(ptr, term_type, term_type_len); ptr += term_type_len; - - len32 = shell_type_len; + + len32 = (uint32_t)shell_type_len; memcpy(ptr, &len32, sizeof(uint32_t)); ptr += sizeof(uint32_t); memcpy(ptr, shell_type, shell_type_len); + ptr += shell_type_len; - // 发送初始化消息 - if (write_message(sock, MSG_TYPE_INIT, init_payload, total_payload_len) < 0) { - DEBUG_LOG("Failed to send init message\n"); - free(init_payload); - close(sock); - return -1; + /* 验证载荷大小 */ + size_t written_bytes = (size_t)(ptr - init_payload); + if (written_bytes != total_payload_len) { + DEBUG_LOG("载荷大小不匹配: 期望=%u, 实际=%zu", total_payload_len, + written_bytes); + goto cleanup; } + + /* 发送初始化消息 */ + if (write_message(sock, MSG_TYPE_INIT, init_payload, total_payload_len) < + 0) { + DEBUG_LOG("发送初始化消息失败"); + goto cleanup; + } + free(init_payload); + init_payload = NULL; - // 启动响应监听线程 - pthread_t response_thread; - if (pthread_create(&response_thread, NULL, response_listener_thread, output_fd) != 0) { - DEBUG_LOG("Failed to create response listener thread\n"); - close(sock); - return -1; + DEBUG_LOG("初始化消息已发送"); + + /* ======================================================================== + * 启动工作线程 + * ======================================================================== + */ + + /* 启动响应监听线程 */ + if (pthread_create(&response_thread, NULL, response_listener_thread, + output_fd) != 0) { + DEBUG_LOG("创建响应监听线程失败: %s", strerror(errno)); + goto cleanup; } + response_thread_created = 1; - // 启动窗口监听线程 - pthread_t window_thread; - if (pthread_create(&window_thread, NULL, window_monitor_thread, NULL) != 0) { - DEBUG_LOG("Failed to create window monitor thread\n"); - pthread_cancel(response_thread); - pthread_join(response_thread, NULL); - close(sock); - return -1; + /* 启动窗口监听线程 */ + if (pthread_create(&window_thread, NULL, window_monitor_thread, NULL) != + 0) { + DEBUG_LOG("创建窗口监听线程失败: %s", strerror(errno)); + goto cleanup; } + window_thread_created = 1; - // 启动终端输入监听线程 - pthread_t input_thread; + /* 启动终端输入监听线程 */ if (pthread_create(&input_thread, NULL, terminal_input_thread, NULL) != 0) { - DEBUG_LOG("Failed to create terminal input thread\n"); - pthread_cancel(response_thread); + DEBUG_LOG("创建终端输入线程失败: %s", strerror(errno)); + goto cleanup; + } + input_thread_created = 1; + + DEBUG_LOG("所有工作线程已启动"); + + /* 等待响应线程结束(表示服务端关闭连接或发生错误)*/ + pthread_join(response_thread, NULL); + response_thread_created = 0; + + DEBUG_LOG("响应线程已结束,开始清理"); + + /* 标记成功 */ + ret = 0; + +cleanup: + /* 设置退出标志 */ + g_should_exit = 1; + pthread_cond_broadcast(&g_winch_cond); + + /* 取消并等待其他线程 */ + if (window_thread_created) { pthread_cancel(window_thread); - pthread_join(response_thread, NULL); pthread_join(window_thread, NULL); - close(sock); - return -1; + DEBUG_LOG("窗口监听线程已清理"); } - // 等待响应线程结束(表示服务器关闭连接) - pthread_join(response_thread, NULL); + if (input_thread_created) { + pthread_cancel(input_thread); + pthread_join(input_thread, NULL); + DEBUG_LOG("终端输入线程已清理"); + } - // 清理 - g_should_exit = 1; - pthread_cancel(window_thread); - pthread_cancel(input_thread); - pthread_join(window_thread, NULL); - pthread_join(input_thread, NULL); + if (response_thread_created) { + pthread_cancel(response_thread); + pthread_join(response_thread, NULL); + DEBUG_LOG("响应线程已清理"); + } - // 恢复终端状态 + /* 恢复终端状态 */ cleanup_terminal(); - pthread_mutex_lock(&g_socket_mutex); - g_socket_fd = -1; - pthread_mutex_unlock(&g_socket_mutex); + /* 关闭 socket */ + close_and_reset_socket(sock); - close(sock); - DEBUG_LOG("connection done.\n"); + /* 释放载荷 */ + if (init_payload != NULL) { + free(init_payload); + } - return 0; -} + /* 重置全局状态 */ + g_should_exit = 0; + g_window_size_changed = 0; + + DEBUG_LOG("连接已关闭,清理完成"); + + return ret; +} \ No newline at end of file diff --git a/src/client.h b/src/client.h index 8472b98..05b6ab1 100644 --- a/src/client.h +++ b/src/client.h @@ -15,7 +15,52 @@ #define SOCKET_PATH "/var/run/bash-smart.sock" #define MAX_BUF_SIZE 4096 -// 函数声明 -int seeking_solutions(const char *filename, char *const argv[], char *const envp[], const char *logPath, int *output_fd); - +/** + * @brief 建立与服务端的连接并进入交互模式 + * + * 此函数会: + * 1. 连接到 Unix Domain Socket 服务端 + * 2. 发送初始化信息(文件名、参数、环境变量、终端信息等) + * 3. 启动多个线程处理输入输出和窗口事件 + * 4. 阻塞直到服务端关闭连接或收到退出信号 + * + * 线程模型: + * - 响应监听线程:接收服务端消息并输出到 output_fd + * - 窗口监听线程:监听 SIGWINCH 信号,发送窗口大小更新 + * - 终端输入线程:捕获键盘和鼠标输入,发送到服务端 + * + * 信号处理: + * - SIGWINCH:触发窗口大小更新 + * - SIGINT/SIGTERM:触发优雅退出 + * + * @param filename 要执行的文件路径 + * @param argv 命令行参数数组,以 NULL 结尾 + * @param envp 环境变量数组,以 NULL 结尾 + * @param logPath 日志/错误文件路径(可以是相对路径或绝对路径) + * @param output_fd 指向输出文件描述符的指针,通常为 STDOUT_FILENO + * + * @return 0 成功完成,-1 发生错误 + * + * @note 此函数会修改终端设置(原始模式、鼠标跟踪), + * 但保证在退出时恢复原始状态 + * @note 调用者应确保在调用前服务端已经启动 + * + * @code + * // 使用示例 + * int output_fd = STDOUT_FILENO; + * char* argv[] = {"my_command", "--option", NULL}; + * extern char** environ; + * + * int result = seeking_solutions("/usr/bin/my_command", + * argv, environ, + * "/tmp/error.log", + * &output_fd); + * if (result < 0) { + * fprintf(stderr, "连接失败\n"); + * } + * @endcode + */ +int seeking_solutions(const char* filename, char* const argv[], + char* const envp[], const char* logPath, int* output_fd); + #endif // EXEC_SOCKET_H