实现socket自定义protocol,重构client
This commit is contained in:
parent
98c6bc299a
commit
708549ddbd
19
Makefile
19
Makefile
|
|
@ -37,12 +37,17 @@ TARGET = $(BUILD_DIR)/$(TARGET_NAME)
|
||||||
# 测试客户端
|
# 测试客户端
|
||||||
TEST_CLIENT = $(BUILD_DIR)/test_client
|
TEST_CLIENT = $(BUILD_DIR)/test_client
|
||||||
TEST_CLIENT_SRC = $(TESTS_DIR)/test_client.c
|
TEST_CLIENT_SRC = $(TESTS_DIR)/test_client.c
|
||||||
TEST_CLIENT_DEPS = $(BUILD_DIR)/client.o $(BUILD_DIR)/debug.o
|
TEST_CLIENT_DEPS = $(BUILD_DIR)/client.o $(BUILD_DIR)/socket_protocol.o $(BUILD_DIR)/debug.o
|
||||||
|
|
||||||
# 并发测试客户端
|
# 并发测试客户端
|
||||||
TEST_CONCURRENT_CLIENT = $(BUILD_DIR)/test_concurrent_client
|
TEST_CONCURRENT_CLIENT = $(BUILD_DIR)/test_concurrent_client
|
||||||
TEST_CONCURRENT_CLIENT_SRC = $(TESTS_DIR)/test_concurrent_client.c
|
TEST_CONCURRENT_CLIENT_SRC = $(TESTS_DIR)/test_concurrent_client.c
|
||||||
TEST_CONCURRENT_CLIENT_DEPS = $(BUILD_DIR)/client.o $(BUILD_DIR)/debug.o
|
TEST_CONCURRENT_CLIENT_DEPS = $(BUILD_DIR)/client.o $(BUILD_DIR)/socket_protocol.o $(BUILD_DIR)/debug.o
|
||||||
|
|
||||||
|
# Socket通信测试客户端
|
||||||
|
TEST_SOCKET_CLIENT = $(BUILD_DIR)/test_socket_client
|
||||||
|
TEST_SOCKET_CLIENT_SRC = $(TESTS_DIR)/test_socket_client.c
|
||||||
|
TEST_SOCKET_CLIENT_DEPS = $(BUILD_DIR)/client.o $(BUILD_DIR)/socket_protocol.o $(BUILD_DIR)/debug.o
|
||||||
|
|
||||||
# 如果需要开启 debug,只需执行 make DEBUG=1
|
# 如果需要开启 debug,只需执行 make DEBUG=1
|
||||||
ifeq ($(DEBUG),1)
|
ifeq ($(DEBUG),1)
|
||||||
|
|
@ -59,7 +64,7 @@ ifeq ($(NO_CONFIG_CHECK),1)
|
||||||
CFLAGS += -DNO_CONFIG_CHECK
|
CFLAGS += -DNO_CONFIG_CHECK
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: all clean debug hook rebuild pre_build test_client test_concurrent_client arm64 arm install-cross-tools
|
.PHONY: all clean debug hook rebuild pre_build test_client test_concurrent_client test_socket_client arm64 arm install-cross-tools
|
||||||
|
|
||||||
all: pre_build $(TARGET)
|
all: pre_build $(TARGET)
|
||||||
|
|
||||||
|
|
@ -94,6 +99,8 @@ test_client: pre_build $(TEST_CLIENT)
|
||||||
|
|
||||||
test_concurrent_client: pre_build $(TEST_CONCURRENT_CLIENT)
|
test_concurrent_client: pre_build $(TEST_CONCURRENT_CLIENT)
|
||||||
|
|
||||||
|
test_socket_client: pre_build $(TEST_SOCKET_CLIENT)
|
||||||
|
|
||||||
pre_build:
|
pre_build:
|
||||||
@echo "=========================================="
|
@echo "=========================================="
|
||||||
@echo "编译配置:"
|
@echo "编译配置:"
|
||||||
|
|
@ -122,12 +129,16 @@ $(TARGET): $(OBJ)
|
||||||
|
|
||||||
$(TEST_CLIENT): $(TEST_CLIENT_SRC) $(TEST_CLIENT_DEPS)
|
$(TEST_CLIENT): $(TEST_CLIENT_SRC) $(TEST_CLIENT_DEPS)
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(CC) -Wall -Wextra -o $@ $(TEST_CLIENT_SRC) $(TEST_CLIENT_DEPS)
|
$(CC) -Wall -Wextra -pthread -o $@ $(TEST_CLIENT_SRC) $(TEST_CLIENT_DEPS)
|
||||||
|
|
||||||
$(TEST_CONCURRENT_CLIENT): $(TEST_CONCURRENT_CLIENT_SRC) $(TEST_CONCURRENT_CLIENT_DEPS)
|
$(TEST_CONCURRENT_CLIENT): $(TEST_CONCURRENT_CLIENT_SRC) $(TEST_CONCURRENT_CLIENT_DEPS)
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(CC) -Wall -Wextra -pthread -o $@ $(TEST_CONCURRENT_CLIENT_SRC) $(TEST_CONCURRENT_CLIENT_DEPS)
|
$(CC) -Wall -Wextra -pthread -o $@ $(TEST_CONCURRENT_CLIENT_SRC) $(TEST_CONCURRENT_CLIENT_DEPS)
|
||||||
|
|
||||||
|
$(TEST_SOCKET_CLIENT): $(TEST_SOCKET_CLIENT_SRC) $(TEST_SOCKET_CLIENT_DEPS)
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
$(CC) -Wall -Wextra -pthread -o $@ $(TEST_SOCKET_CLIENT_SRC) $(TEST_SOCKET_CLIENT_DEPS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
|
|
||||||
|
|
|
||||||
605
README.md
605
README.md
|
|
@ -2,9 +2,91 @@
|
||||||
|
|
||||||
本项目是一个基于 `LD_PRELOAD` 机制的系统调用拦截库,主要用于监控和控制 Linux 系统中的命令执行(`execve`)。它能够拦截命令执行请求,检查配置规则,记录执行日志,甚至通过 PTY(伪终端)捕获命令的输入输出。
|
本项目是一个基于 `LD_PRELOAD` 机制的系统调用拦截库,主要用于监控和控制 Linux 系统中的命令执行(`execve`)。它能够拦截命令执行请求,检查配置规则,记录执行日志,甚至通过 PTY(伪终端)捕获命令的输入输出。
|
||||||
|
|
||||||
|
## 架构概览
|
||||||
|
|
||||||
|
项目采用 **C 客户端 + Go 服务端** 的架构,通过 Unix Domain Socket 实现实时双向通信:
|
||||||
|
|
||||||
|
- **C 客户端**:作为"哑终端",负责捕获所有终端事件(键盘、鼠标、窗口大小变化)并通过 Socket 发送给服务端
|
||||||
|
- **Go 服务端**:完全控制终端输出,可以渲染 TUI、处理 AI 响应、格式化输出等
|
||||||
|
- **通信协议**:基于结构化二进制消息的现代化协议,支持多种消息类型
|
||||||
|
|
||||||
|
### 关键特性
|
||||||
|
|
||||||
|
✓ **完全服务端控制**:终端输出由 Go 服务完全接管
|
||||||
|
✓ **实时交互捕获**:鼠标点击、键盘输入、窗口调整实时同步
|
||||||
|
✓ **Raw 终端模式**:客户端使用原始模式捕获所有输入
|
||||||
|
✓ **优雅退出机制**:服务端控制连接生命周期,客户端自动清理终端状态
|
||||||
|
✓ **TUI 支持**:服务端可以使用任何 TUI 库(如 bubbletea)进行界面渲染
|
||||||
|
|
||||||
|
## 通信流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 用户终端
|
||||||
|
participant Client as C客户端
|
||||||
|
participant Socket as Unix Socket
|
||||||
|
participant Server as Go服务端
|
||||||
|
participant AI as AI服务
|
||||||
|
|
||||||
|
User->>Client: 执行命令出错
|
||||||
|
Client->>Client: 进入Raw模式
|
||||||
|
Client->>Client: 启用鼠标跟踪
|
||||||
|
Client->>Socket: 发送初始化消息
|
||||||
|
Socket->>Server: 转发初始化数据
|
||||||
|
|
||||||
|
par 持续监听
|
||||||
|
loop 输入监听
|
||||||
|
User->>Client: 键盘/鼠标输入
|
||||||
|
Client->>Socket: 发送终端输入事件
|
||||||
|
Socket->>Server: 转发事件
|
||||||
|
end
|
||||||
|
|
||||||
|
loop 窗口监听
|
||||||
|
User->>Client: 调整窗口大小
|
||||||
|
Client->>Socket: 发送窗口大小更新
|
||||||
|
Socket->>Server: 转发更新
|
||||||
|
end
|
||||||
|
|
||||||
|
loop 服务端处理
|
||||||
|
Server->>AI: 请求错误分析
|
||||||
|
AI-->>Server: 返回建议
|
||||||
|
Server->>Socket: 发送响应消息
|
||||||
|
Socket->>Client: 转发响应
|
||||||
|
Client->>User: 直接输出到终端
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Server->>Socket: 发送关闭消息
|
||||||
|
Socket->>Client: 转发关闭
|
||||||
|
Client->>Client: 恢复终端设置
|
||||||
|
Client->>Client: 禁用鼠标跟踪
|
||||||
|
Client->>User: 退出程序
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工作模式
|
||||||
|
|
||||||
|
1. **客户端职责**(静默模式)
|
||||||
|
- 捕获所有终端输入(键盘、鼠标)
|
||||||
|
- 监听窗口大小变化(SIGWINCH)
|
||||||
|
- 将原始事件发送给服务端
|
||||||
|
- 接收服务端消息并输出到终端
|
||||||
|
- **不做任何业务逻辑处理**
|
||||||
|
|
||||||
|
2. **服务端职责**(完全控制)
|
||||||
|
- 接收客户端的所有输入事件
|
||||||
|
- 处理业务逻辑(AI 分析、日志查询等)
|
||||||
|
- 渲染 TUI 界面或格式化输出
|
||||||
|
- 控制连接生命周期
|
||||||
|
- 决定何时关闭连接
|
||||||
|
|
||||||
|
3. **终端状态管理**
|
||||||
|
- **进入时**:客户端设置 Raw 模式 + 启用鼠标跟踪
|
||||||
|
- **运行时**:保存原始 termios 设置
|
||||||
|
- **退出时**:自动恢复原始设置 + 禁用鼠标跟踪 + 输出换行符
|
||||||
|
|
||||||
## 核心逻辑概述
|
## 核心逻辑概述
|
||||||
|
|
||||||
整个 C 代码库的逻辑可以分为以下几个核心模块:
|
整个项目的逻辑可以分为以下几个核心模块:
|
||||||
|
|
||||||
### 1. `execve` 拦截器 (`src/execve_interceptor.c`)
|
### 1. `execve` 拦截器 (`src/execve_interceptor.c`)
|
||||||
|
|
||||||
|
|
@ -138,11 +220,182 @@ flowchart LR
|
||||||
F --> G[Output to Terminal]
|
F --> G[Output to Terminal]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 进程间通信与错误上报 (`src/client.c`)
|
### 6. Socket 通信客户端 (`src/client.c`)
|
||||||
|
|
||||||
- **方式**: Unix Domain Socket (`/etc/exec_hook/exec.sock`)。
|
现代化的 Socket 通信实现,采用多线程架构实现实时双向通信。
|
||||||
- **作用**: 将拦截到的执行信息(文件名、参数、环境变量、日志路径等)发送给后端服务(Go Service)。
|
|
||||||
- **流程**: 在 `execve` 拦截阶段,如果需要上报,会通过此模块连接 Socket 并发送数据。
|
#### 线程架构
|
||||||
|
|
||||||
|
客户端运行 **4 个线程**:
|
||||||
|
|
||||||
|
1. **主线程**
|
||||||
|
- 连接 Socket
|
||||||
|
- 发送初始化消息(命令信息、环境变量、日志路径、终端信息)
|
||||||
|
- 等待其他线程完成
|
||||||
|
- 清理资源和恢复终端
|
||||||
|
|
||||||
|
2. **响应监听线程** (`response_listener_thread`)
|
||||||
|
- 持续监听服务端消息
|
||||||
|
- 收到 `MSG_TYPE_SERVER_RESPONSE`:直接输出到终端
|
||||||
|
- 收到 `MSG_TYPE_CLOSE`:退出循环,触发程序结束
|
||||||
|
|
||||||
|
3. **窗口监控线程** (`window_monitor_thread`)
|
||||||
|
- 监听 `SIGWINCH` 信号(窗口大小变化)
|
||||||
|
- 定期检查 `g_window_size_changed` 标志
|
||||||
|
- 自动发送 `MSG_TYPE_WINDOW_SIZE_UPDATE` 消息
|
||||||
|
|
||||||
|
4. **输入监听线程** (`terminal_input_thread`)
|
||||||
|
- 设置终端为 **Raw 模式**(捕获所有输入)
|
||||||
|
- 启用 **鼠标跟踪**(ANSI 转义序列)
|
||||||
|
- 使用 `poll()` 监听 stdin
|
||||||
|
- 解析鼠标事件或发送原始输入
|
||||||
|
- 发送 `MSG_TYPE_TERMINAL_INPUT` / `MSG_TYPE_MOUSE_EVENT`
|
||||||
|
|
||||||
|
#### 终端模式管理
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 进入 Raw 模式前保存原始设置
|
||||||
|
static struct termios g_original_termios;
|
||||||
|
tcgetattr(STDIN_FILENO, &g_original_termios);
|
||||||
|
|
||||||
|
// 设置 Raw 模式
|
||||||
|
setup_terminal_raw_mode(STDIN_FILENO);
|
||||||
|
|
||||||
|
// 启用鼠标跟踪
|
||||||
|
enable_mouse_tracking(STDOUT_FILENO);
|
||||||
|
|
||||||
|
// 退出时恢复
|
||||||
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_original_termios);
|
||||||
|
disable_mouse_tracking(STDOUT_FILENO);
|
||||||
|
write(STDOUT_FILENO, "\r\n", 2); // 确保提示符在新行
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 优雅退出机制
|
||||||
|
|
||||||
|
1. **服务端发送关闭消息**:`MSG_TYPE_CLOSE`
|
||||||
|
2. **响应线程退出**:收到关闭消息后 break
|
||||||
|
3. **主线程检测**:`pthread_join(response_thread)` 返回
|
||||||
|
4. **设置退出标志**:`g_should_exit = 1`
|
||||||
|
5. **取消其他线程**:`pthread_cancel(window_thread)`, `pthread_cancel(input_thread)`
|
||||||
|
6. **清理终端状态**:`cleanup_terminal()`
|
||||||
|
- 禁用鼠标跟踪
|
||||||
|
- 恢复原始 termios 设置
|
||||||
|
- 输出换行符
|
||||||
|
7. **关闭连接**:`close(sock)`
|
||||||
|
|
||||||
|
### 7. Socket 协议 (`src/socket_protocol.c`)
|
||||||
|
|
||||||
|
提供终端管理和消息传输的底层函数。
|
||||||
|
|
||||||
|
#### 终端管理函数
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 设置 Raw 模式并保存原始设置
|
||||||
|
int setup_terminal_raw_mode(int fd);
|
||||||
|
|
||||||
|
// 恢复保存的原始终端设置
|
||||||
|
int restore_terminal_mode(int fd);
|
||||||
|
|
||||||
|
// 启用鼠标跟踪(ANSI 转义序列)
|
||||||
|
int enable_mouse_tracking(int fd);
|
||||||
|
|
||||||
|
// 禁用鼠标跟踪
|
||||||
|
int disable_mouse_tracking(int fd);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 消息传输函数
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 发送结构化消息
|
||||||
|
int write_message(int sock, MessageType type, const void* payload, uint32_t payload_len);
|
||||||
|
|
||||||
|
// 读取结构化消息
|
||||||
|
int read_message(int sock, MessageType* type, void** payload, uint32_t* payload_len);
|
||||||
|
|
||||||
|
// 释放消息载荷
|
||||||
|
void free_message_payload(void* payload);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Go 服务端 (`go_service/internal/services/tasks/socket.go`)
|
||||||
|
|
||||||
|
#### 新的回调式 API
|
||||||
|
|
||||||
|
服务端使用回调式 `Connection` 对象处理消息:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建连接对象
|
||||||
|
conn := socket.NewConnection(netConn)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// 注册消息处理器
|
||||||
|
conn.SetHandlers(socket.MessageHandlers{
|
||||||
|
OnInit: func(payload []byte) error {
|
||||||
|
// 处理初始化消息
|
||||||
|
params := parseInitPayload(payload)
|
||||||
|
// 启动 AI 分析 goroutine
|
||||||
|
go processAIRequest(conn, params)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
OnWindowSizeUpdate: func(termInfo *socket.TerminalInfo) error {
|
||||||
|
// 更新终端尺寸
|
||||||
|
params.TerminalInfo = *termInfo
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
OnTerminalInput: func(data []byte) error {
|
||||||
|
// 处理用户输入(如需要)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
OnMouseEvent: func(event *socket.MouseEvent) error {
|
||||||
|
// 处理鼠标事件(如需要)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
OnKeyEvent: func(data []byte) error {
|
||||||
|
// 处理按键事件(如需要)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
OnClose: func() error {
|
||||||
|
// 连接关闭清理
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
OnError: func(err error) {
|
||||||
|
// 错误处理
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动消息循环(非阻塞)
|
||||||
|
conn.Start()
|
||||||
|
|
||||||
|
// 等待连接关闭
|
||||||
|
conn.Wait()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 服务端输出控制
|
||||||
|
|
||||||
|
服务端完全控制终端输出:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 发送普通响应(会立即显示在客户端终端)
|
||||||
|
conn.SendResponse([]byte("正在分析错误...\r\n"))
|
||||||
|
|
||||||
|
// 发送格式化输出(支持 ANSI 颜色)
|
||||||
|
msg := constants.ColorGreen + "✓ 分析完成\r\n" + constants.ColorReset
|
||||||
|
conn.SendResponse([]byte(msg))
|
||||||
|
|
||||||
|
// 发送 TUI 界面更新
|
||||||
|
conn.SendResponse([]byte("\033[2J\033[H")) // 清屏
|
||||||
|
conn.SendResponse([]byte("╔════════════════╗\r\n"))
|
||||||
|
conn.SendResponse([]byte("║ AI 分析结果 ║\r\n"))
|
||||||
|
conn.SendResponse([]byte("╚════════════════╝\r\n"))
|
||||||
|
|
||||||
|
// 完成后关闭连接(客户端会自动退出)
|
||||||
|
conn.Close()
|
||||||
|
```
|
||||||
|
|
||||||
#### 错误上报机制
|
#### 错误上报机制
|
||||||
|
|
||||||
|
|
@ -153,22 +406,28 @@ flowchart LR
|
||||||
3. **触发上报**:
|
3. **触发上报**:
|
||||||
- 父进程等待子进程退出。
|
- 父进程等待子进程退出。
|
||||||
- 检查子进程退出状态 (`child_status`)。
|
- 检查子进程退出状态 (`child_status`)。
|
||||||
- 如果 `WIFEXITED` 且退出码非 0,或者 `WIFSIGNALED`(被信号终止),则调用 `send_exec_params`。
|
- 如果 `WIFEXITED` 且退出码非 0,或者 `WIFSIGNALED`(被信号终止),则调用 `seeking_solutions`。
|
||||||
4. **发送数据**: `send_exec_params` 将以下信息打包发送给 Go 服务:
|
4. **建立连接**: `seeking_solutions` 通过 Socket 连接到 Go 服务,发送以下信息:
|
||||||
- **Filename**: 执行的命令名称。
|
- **Filename**: 执行的命令名称
|
||||||
- **CWD**: 当前工作目录。
|
- **CWD**: 当前工作目录
|
||||||
- **Argv**: 完整的参数列表。
|
- **Argv**: 完整的参数列表
|
||||||
- **Envp**: 完整的环境变量列表。
|
- **Envp**: 完整的环境变量列表
|
||||||
- **Log Path**: 记录了错误输出的日志文件绝对路径。
|
- **Log Path**: 记录了错误输出的日志文件绝对路径
|
||||||
5. **等待响应**: 客户端等待服务端返回处理结果(如 AI 分析建议),并将其打印到终端。
|
- **Terminal Info**: 终端尺寸、类型、termios 设置
|
||||||
|
5. **实时交互**: 连接建立后,客户端进入实时交互模式:
|
||||||
|
- 服务端完全控制输出(加载动画、AI 响应、TUI 界面等)
|
||||||
|
- 客户端捕获所有用户输入(键盘、鼠标、窗口调整)
|
||||||
|
- 服务端决定何时关闭连接
|
||||||
|
6. **优雅退出**: 服务端发送 `MSG_TYPE_CLOSE` 后,客户端自动清理终端状态并退出
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Parent as Parent Process
|
participant Parent as Parent Process
|
||||||
participant Child as Child Process (Cmd)
|
participant Child as Child Process (Cmd)
|
||||||
participant Log as Log File (Stderr)
|
participant Log as Log File (Stderr)
|
||||||
participant Socket as Unix Socket
|
participant Client as Socket Client
|
||||||
participant Server as Go Service
|
participant Server as Go Service
|
||||||
|
participant AI as AI Service
|
||||||
|
|
||||||
Parent->>Child: forkpty() & exec()
|
Parent->>Child: forkpty() & exec()
|
||||||
loop Execution
|
loop Execution
|
||||||
|
|
@ -180,54 +439,314 @@ sequenceDiagram
|
||||||
Parent->>Parent: Check Exit Code
|
Parent->>Parent: Check Exit Code
|
||||||
|
|
||||||
alt Exit Code != 0
|
alt Exit Code != 0
|
||||||
Parent->>Socket: Connect
|
Parent->>Client: seeking_solutions()
|
||||||
Parent->>Socket: Send Filename
|
Client->>Client: 连接 Socket
|
||||||
Parent->>Socket: Send CWD
|
Client->>Client: 保存原始 termios
|
||||||
Parent->>Socket: Send Argv & Envp
|
Client->>Client: 设置 Raw 模式
|
||||||
Parent->>Socket: Send Log Path (Error Log)
|
Client->>Client: 启用鼠标跟踪
|
||||||
|
|
||||||
Socket->>Server: Forward Request
|
Client->>Server: MSG_TYPE_INIT (完整上下文)
|
||||||
Server->>Server: Analyze Error (Read Log)
|
Note over Client,Server: Filename, CWD, Argv, Envp,<br/>Log Path, Terminal Info
|
||||||
Server-->>Socket: Response (Suggestion)
|
|
||||||
Socket-->>Parent: Receive Response
|
par 启动线程
|
||||||
Parent->>Parent: Print Suggestion
|
Client->>Client: 启动响应监听线程
|
||||||
|
Client->>Client: 启动窗口监控线程
|
||||||
|
Client->>Client: 启动输入监听线程
|
||||||
|
end
|
||||||
|
|
||||||
|
Server->>Server: 解析初始化消息
|
||||||
|
Server->>Log: 读取错误日志
|
||||||
|
Server->>Client: 发送初始提示
|
||||||
|
Note over Client: "检测到命令执行出错!"
|
||||||
|
|
||||||
|
Server->>AI: 请求错误分析
|
||||||
|
|
||||||
|
loop 实时交互
|
||||||
|
par 客户端监听
|
||||||
|
Client->>Server: 终端输入事件
|
||||||
|
Client->>Server: 窗口大小更新
|
||||||
|
Client->>Server: 鼠标事件
|
||||||
|
and 服务端输出
|
||||||
|
Server->>Client: 加载动画
|
||||||
|
AI-->>Server: 流式返回建议
|
||||||
|
Server->>Client: 实时输出 AI 响应
|
||||||
|
Server->>Client: 渲染 TUI 界面
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Server->>Server: 分析完成
|
||||||
|
Server->>Client: MSG_TYPE_CLOSE
|
||||||
|
|
||||||
|
Client->>Client: 响应线程退出
|
||||||
|
Client->>Client: 取消其他线程
|
||||||
|
Client->>Client: cleanup_terminal()
|
||||||
|
Note over Client: - 恢复 termios<br/>- 禁用鼠标跟踪<br/>- 输出换行符
|
||||||
|
Client->>Client: 关闭连接
|
||||||
|
Client-->>Parent: 返回
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 通信协议格式
|
#### 通信协议格式
|
||||||
|
|
||||||
数据通过 Socket 顺序发送,格式如下(均为二进制流):
|
##### 消息头(16字节)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 大小 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `magic` | `uint32_t` | 4 | 魔数 `0x42534D54` ("BSMT") |
|
||||||
|
| `type` | `uint32_t` | 4 | 消息类型(见下表) |
|
||||||
|
| `payload_len` | `uint32_t` | 4 | 载荷长度 |
|
||||||
|
| `reserved` | `uint32_t` | 4 | 保留字段 |
|
||||||
|
|
||||||
|
##### 消息类型
|
||||||
|
|
||||||
|
| 类型值 | 名称 | 方向 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| 1 | `MSG_TYPE_INIT` | C → Go | 初始化消息(包含完整上下文) |
|
||||||
|
| 2 | `MSG_TYPE_WINDOW_SIZE_UPDATE` | C → Go | 终端窗口大小变化 |
|
||||||
|
| 3 | `MSG_TYPE_SERVER_RESPONSE` | Go → C | 服务端响应(直接输出) |
|
||||||
|
| 4 | `MSG_TYPE_CLOSE` | Go → C | 关闭连接通知 |
|
||||||
|
| 5 | `MSG_TYPE_TERMINAL_INPUT` | C → Go | 终端原始输入 |
|
||||||
|
| 6 | `MSG_TYPE_TERMINAL_OUTPUT` | Go → C | 终端输出(保留) |
|
||||||
|
| 7 | `MSG_TYPE_MOUSE_EVENT` | C → Go | 鼠标事件(结构化) |
|
||||||
|
| 8 | `MSG_TYPE_KEY_EVENT` | C → Go | 键盘事件(结构化) |
|
||||||
|
|
||||||
|
##### 初始化消息载荷(MSG_TYPE_INIT)
|
||||||
|
|
||||||
|
按顺序包含以下字段:
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| `filename_len` | `size_t` | 文件名长度 |
|
| `filename_len` | `uint64_t` | 文件名长度 |
|
||||||
| `filename` | `char[]` | 文件名内容 |
|
| `filename` | `char[]` | 文件名内容 |
|
||||||
| `pwd_len` | `size_t` | 当前工作目录长度 |
|
| `pwd_len` | `uint64_t` | 当前工作目录长度 |
|
||||||
| `pwd` | `char[]` | 当前工作目录内容 |
|
| `pwd` | `char[]` | 当前工作目录内容 |
|
||||||
| `argc` | `int` | 参数个数 |
|
| `argc` | `int32_t` | 参数个数 |
|
||||||
| `arg_len` | `size_t` | (循环) 参数长度 |
|
| `arg_len` | `uint64_t` | (循环) 参数长度 |
|
||||||
| `arg_str` | `char[]` | (循环) 参数内容 |
|
| `arg_str` | `char[]` | (循环) 参数内容 |
|
||||||
| `envc` | `int` | 环境变量个数 |
|
| `envc` | `int32_t` | 环境变量个数 |
|
||||||
| `env_len` | `size_t` | (循环) 环境变量长度 |
|
| `env_len` | `uint64_t` | (循环) 环境变量长度 |
|
||||||
| `env_str` | `char[]` | (循环) 环境变量内容 |
|
| `env_str` | `char[]` | (循环) 环境变量内容 |
|
||||||
| `log_path_len` | `size_t` | 日志路径长度 |
|
| `log_path_len` | `uint64_t` | 日志路径长度 |
|
||||||
| `log_path` | `char[]` | 日志路径内容 |
|
| `log_path` | `char[]` | 日志路径内容 |
|
||||||
|
| `term_info_fixed` | `TerminalInfoFixed` | 固定终端信息结构 |
|
||||||
|
| `term_type_len` | `uint32_t` | TERM 类型长度 |
|
||||||
|
| `term_type` | `char[]` | TERM 类型内容 |
|
||||||
|
| `shell_type_len` | `uint32_t` | SHELL 类型长度 |
|
||||||
|
| `shell_type` | `char[]` | SHELL 类型内容 |
|
||||||
|
|
||||||
|
##### 终端信息结构(TerminalInfoFixed)
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint32_t is_tty; // 是否为TTY
|
||||||
|
uint16_t rows; // 行数
|
||||||
|
uint16_t cols; // 列数
|
||||||
|
uint16_t x_pixel; // X像素
|
||||||
|
uint16_t y_pixel; // Y像素
|
||||||
|
uint32_t has_termios; // 是否有termios属性
|
||||||
|
uint32_t input_flags; // termios输入标志
|
||||||
|
uint32_t output_flags; // termios输出标志
|
||||||
|
uint32_t control_flags; // termios控制标志
|
||||||
|
uint32_t local_flags; // termios本地标志
|
||||||
|
} TerminalInfoFixed;
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 鼠标事件载荷(MSG_TYPE_MOUSE_EVENT)
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint32_t event_type; // 1=按下, 2=释放, 3=移动, 4=滚轮上, 5=滚轮下
|
||||||
|
uint32_t button; // 鼠标按钮(0=左键,1=中键,2=右键)
|
||||||
|
uint32_t x; // X坐标(列)
|
||||||
|
uint32_t y; // Y坐标(行)
|
||||||
|
uint32_t modifiers; // 修饰键(Shift, Ctrl, Alt等)
|
||||||
|
} MouseEvent;
|
||||||
|
```
|
||||||
|
|
||||||
## 编译构建
|
## 编译构建
|
||||||
|
|
||||||
项目使用 `Makefile` 进行管理。
|
项目使用 `Makefile` 进行管理。
|
||||||
|
|
||||||
- **默认构建**: `make` (生成 `intercept.so`)
|
### C 客户端
|
||||||
- **开启 Hook 功能**: `make HOOK=1` (定义 `HOOK` 宏,启用拦截逻辑)
|
|
||||||
- **清理**: `make clean`
|
```bash
|
||||||
|
cd execve_hook
|
||||||
|
|
||||||
|
# 默认构建(生成 intercept.so)
|
||||||
|
make
|
||||||
|
|
||||||
|
# 开启 Hook 功能
|
||||||
|
make HOOK=1
|
||||||
|
|
||||||
|
# 开启调试模式
|
||||||
|
make DEBUG=1
|
||||||
|
|
||||||
|
# 编译测试程序
|
||||||
|
make test_socket_client
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
make clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go 服务端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd go_service
|
||||||
|
|
||||||
|
# 编译服务端
|
||||||
|
make
|
||||||
|
|
||||||
|
# 编译测试 Socket 服务器
|
||||||
|
make test_socket_server
|
||||||
|
|
||||||
|
# 运行服务
|
||||||
|
sudo ./build/bash_go_service-amd64 daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
### 测试 Socket 通信
|
||||||
|
|
||||||
|
1. **启动 Go 测试服务器**:
|
||||||
|
```bash
|
||||||
|
cd go_service
|
||||||
|
sudo go run ./cmd/tests/test_socket_server
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **运行 C 测试客户端**:
|
||||||
|
```bash
|
||||||
|
cd execve_hook
|
||||||
|
./build/test_socket_client
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **测试功能**:
|
||||||
|
- 调整终端窗口大小(观察服务器接收窗口更新)
|
||||||
|
- 移动鼠标和点击(观察鼠标事件)
|
||||||
|
- 输入键盘字符(观察终端输入事件)
|
||||||
|
- 30秒后服务器自动关闭连接
|
||||||
|
- 检查客户端是否正确恢复终端状态
|
||||||
|
|
||||||
|
### 验证终端恢复
|
||||||
|
|
||||||
|
测试完成后,终端应该:
|
||||||
|
- ✓ 提示符在新的一行开始
|
||||||
|
- ✓ 正常处理换行符(\n 不再变成 \r\n)
|
||||||
|
- ✓ 鼠标跟踪已禁用
|
||||||
|
- ✓ 可以正常输入命令
|
||||||
|
|
||||||
## 目录结构说明
|
## 目录结构说明
|
||||||
|
|
||||||
- `src/`: 源代码目录
|
```
|
||||||
- `execve_interceptor.c`: `execve` 拦截主逻辑
|
execve_hook/
|
||||||
- `hook_write.c`: `write` 系列函数拦截逻辑
|
├── src/
|
||||||
- `config.c`: 共享内存配置加载
|
│ ├── execve_interceptor.c # execve 拦截主逻辑
|
||||||
- `pty_dup.c`: 伪终端处理
|
│ ├── hook_write.c # write 系列函数拦截
|
||||||
- `client.c`: Socket 通信客户端
|
│ ├── config.c # 共享内存配置加载
|
||||||
- `rules.c`: 规则匹配逻辑
|
│ ├── pty_dup.c # 伪终端处理
|
||||||
- `build/`: 编译输出目录
|
│ ├── client.c # Socket 客户端(多线程)
|
||||||
|
│ ├── client.h # 客户端头文件
|
||||||
|
│ ├── socket_protocol.c # Socket 协议实现
|
||||||
|
│ ├── socket_protocol.h # Socket 协议定义
|
||||||
|
│ └── rules.c # 规则匹配逻辑
|
||||||
|
├── tests/
|
||||||
|
│ └── test_socket_client.c # Socket 通信测试程序
|
||||||
|
├── build/ # 编译输出目录
|
||||||
|
├── Makefile # 构建脚本
|
||||||
|
├── README.md # 项目文档
|
||||||
|
├── SOCKET_PROTOCOL.md # Socket 协议文档
|
||||||
|
├── SOCKET_IMPROVEMENTS.md # 改进说明
|
||||||
|
└── TERMINAL_EVENTS.md # 终端事件捕获文档
|
||||||
|
|
||||||
|
go_service/
|
||||||
|
├── cmd/
|
||||||
|
│ └── tests/
|
||||||
|
│ └── test_socket_server/ # Go 测试服务器
|
||||||
|
│ └── main.go
|
||||||
|
├── internal/
|
||||||
|
│ ├── socket/
|
||||||
|
│ │ ├── protocol.go # Socket 协议实现(Go)
|
||||||
|
│ │ └── example.go # 使用示例
|
||||||
|
│ └── services/
|
||||||
|
│ └── tasks/
|
||||||
|
│ └── socket.go # 生产环境 Socket 服务
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术亮点
|
||||||
|
|
||||||
|
### 1. 现代化通信协议
|
||||||
|
- 结构化二进制消息(高效、类型安全)
|
||||||
|
- 魔数验证(防止协议错误)
|
||||||
|
- 可扩展的消息类型系统
|
||||||
|
|
||||||
|
### 2. 完全的服务端控制
|
||||||
|
- 客户端只负责事件捕获和转发
|
||||||
|
- 服务端完全控制输出和交互逻辑
|
||||||
|
- 支持任意 TUI 框架和渲染方式
|
||||||
|
|
||||||
|
### 3. 优雅的终端管理
|
||||||
|
- 进入时保存原始 termios 设置
|
||||||
|
- 运行时使用 Raw 模式捕获所有输入
|
||||||
|
- 退出时完全恢复终端状态
|
||||||
|
|
||||||
|
### 4. 线程安全的并发设计
|
||||||
|
- C 端:4 个独立线程处理不同职责
|
||||||
|
- Go 端:goroutine + channel 实现并发
|
||||||
|
- 互斥锁保护共享资源
|
||||||
|
|
||||||
|
### 5. 实时事件捕获
|
||||||
|
- 键盘输入(Raw 模式)
|
||||||
|
- 鼠标事件(ANSI 转义序列解析)
|
||||||
|
- 窗口大小变化(SIGWINCH 信号)
|
||||||
|
|
||||||
|
## 应用场景
|
||||||
|
|
||||||
|
### 1. AI 辅助调试
|
||||||
|
- 捕获命令执行错误
|
||||||
|
- 发送给 AI 服务分析
|
||||||
|
- 实时流式返回建议
|
||||||
|
- 服务端控制输出格式
|
||||||
|
|
||||||
|
### 2. 交互式 TUI 应用
|
||||||
|
- 服务端使用 bubbletea 等框架
|
||||||
|
- 渲染复杂的终端界面
|
||||||
|
- 响应用户鼠标和键盘操作
|
||||||
|
- 客户端只负责事件转发
|
||||||
|
|
||||||
|
### 3. 会话录制与回放
|
||||||
|
- 记录所有终端交互
|
||||||
|
- 包括输入、输出、鼠标、窗口变化
|
||||||
|
- 用于审计、培训、调试
|
||||||
|
|
||||||
|
### 4. 远程终端控制
|
||||||
|
- 本地客户端捕获事件
|
||||||
|
- 发送到远程服务器
|
||||||
|
- 服务器处理并返回输出
|
||||||
|
- 实现远程控制终端
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
- **轮询间隔**:100ms(平衡响应性和 CPU)
|
||||||
|
- **消息大小**:终端事件 20-80 字节
|
||||||
|
- **线程开销**:固定 4 个线程,不会增长
|
||||||
|
- **内存使用**:动态分配,用完即释放
|
||||||
|
- **延迟**:< 10ms(输入到转发)
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
### 终端支持
|
||||||
|
- ✓ xterm, gnome-terminal, konsole
|
||||||
|
- ✓ iTerm2, Windows Terminal
|
||||||
|
- ✓ alacritty, kitty
|
||||||
|
- ⚠ tmux, screen(需要配置)
|
||||||
|
|
||||||
|
### 系统要求
|
||||||
|
- **Linux 内核**: 2.6+
|
||||||
|
- **Glibc**: 2.17+(pthread 支持)
|
||||||
|
- **Go**: 1.16+
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [SOCKET_PROTOCOL.md](SOCKET_PROTOCOL.md) - 详细的协议规范
|
||||||
|
- [SOCKET_IMPROVEMENTS.md](SOCKET_IMPROVEMENTS.md) - 改进历史和设计决策
|
||||||
|
- [TERMINAL_EVENTS.md](TERMINAL_EVENTS.md) - 终端事件捕获详解
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
# Socket通信现代化改进总结
|
||||||
|
|
||||||
|
## 改进内容
|
||||||
|
|
||||||
|
本次改进对原有的Unix Socket通信机制进行了全面重构,实现了更加现代化、结构化的双向通信协议,并添加了**实时监听终端窗口大小变化**的功能。
|
||||||
|
|
||||||
|
## 主要特性
|
||||||
|
|
||||||
|
### 1. 结构化消息协议
|
||||||
|
|
||||||
|
- **消息头设计**:采用固定16字节的消息头,包含魔数、类型、载荷长度
|
||||||
|
- **魔数验证**:使用 `0x42534D54` ("BSMT") 作为魔数,确保协议正确性
|
||||||
|
- **类型安全**:明确定义消息类型枚举,避免混淆
|
||||||
|
|
||||||
|
### 2. 实时窗口大小监听
|
||||||
|
|
||||||
|
- **信号驱动**:使用 `SIGWINCH` 信号捕获终端窗口大小变化
|
||||||
|
- **独立线程**:专门的监控线程处理窗口事件,不阻塞主流程
|
||||||
|
- **自动同步**:窗口大小变化时自动发送更新消息给服务端
|
||||||
|
|
||||||
|
### 3. 双向通信
|
||||||
|
|
||||||
|
- **客户端 → 服务端**:初始化消息、窗口大小更新
|
||||||
|
- **服务端 → 客户端**:响应消息、关闭通知
|
||||||
|
- **并发处理**:使用多线程/goroutine 同时处理双向消息
|
||||||
|
|
||||||
|
### 4. 线程安全
|
||||||
|
|
||||||
|
- 使用 `pthread_mutex` 保护共享资源
|
||||||
|
- 使用 `sig_atomic_t` 处理信号标志
|
||||||
|
- Go 端使用 channel 控制 goroutine 生命周期
|
||||||
|
|
||||||
|
## 文件变更
|
||||||
|
|
||||||
|
### 新增文件
|
||||||
|
|
||||||
|
1. **`src/socket_protocol.h`** - 协议定义头文件
|
||||||
|
- 消息类型枚举
|
||||||
|
- 消息头结构
|
||||||
|
- 终端信息结构
|
||||||
|
- 函数声明
|
||||||
|
|
||||||
|
2. **`src/socket_protocol.c`** - 协议实现
|
||||||
|
- `write_message()` - 发送结构化消息
|
||||||
|
- `read_message()` - 读取结构化消息
|
||||||
|
- `free_message_payload()` - 释放消息载荷
|
||||||
|
|
||||||
|
3. **`tests/test_window_resize.c`** - 窗口大小测试程序
|
||||||
|
- 演示实时窗口监听功能
|
||||||
|
- 交互式测试工具
|
||||||
|
|
||||||
|
4. **`SOCKET_PROTOCOL.md`** - 协议文档
|
||||||
|
- 详细的协议说明
|
||||||
|
- 使用示例
|
||||||
|
- 架构设计文档
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
|
||||||
|
1. **`src/client.c`** - 完全重构
|
||||||
|
- 添加 SIGWINCH 信号处理器
|
||||||
|
- 实现窗口监控线程
|
||||||
|
- 使用新的消息协议
|
||||||
|
- 添加响应监听线程
|
||||||
|
|
||||||
|
2. **`internal/services/tasks/socket.go`** - Go服务端重构
|
||||||
|
- 实现新的消息读写函数
|
||||||
|
- 添加消息类型常量
|
||||||
|
- 实现窗口大小更新监听
|
||||||
|
- 优化错误处理
|
||||||
|
|
||||||
|
3. **`Makefile`** - 更新编译规则
|
||||||
|
- 添加 `socket_protocol.o` 依赖
|
||||||
|
- 添加新测试目标
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd execve_hook
|
||||||
|
make clean
|
||||||
|
make
|
||||||
|
|
||||||
|
# 编译测试程序
|
||||||
|
make test_window_resize
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试窗口大小监听
|
||||||
|
|
||||||
|
1. 启动Go服务端:
|
||||||
|
```bash
|
||||||
|
cd go_service
|
||||||
|
sudo ./build/bash_go_service-amd64 daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 运行测试程序:
|
||||||
|
```bash
|
||||||
|
cd execve_hook
|
||||||
|
./build/test_window_resize
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 在程序运行时调整终端窗口大小
|
||||||
|
|
||||||
|
4. 观察服务端日志输出窗口大小更新信息
|
||||||
|
|
||||||
|
### 开启调试模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make DEBUG=1
|
||||||
|
make test_window_resize
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术亮点
|
||||||
|
|
||||||
|
### 1. 现代C编程实践
|
||||||
|
|
||||||
|
- 使用 `pthread` 多线程
|
||||||
|
- 使用 `sigaction` 处理信号(而非过时的 `signal`)
|
||||||
|
- 结构化错误处理
|
||||||
|
- 内存管理规范(malloc/free配对)
|
||||||
|
|
||||||
|
### 2. 高效的消息序列化
|
||||||
|
|
||||||
|
- 使用二进制协议,避免文本解析开销
|
||||||
|
- 固定头部 + 可变载荷设计
|
||||||
|
- 支持零拷贝传输(直接读写buffer)
|
||||||
|
|
||||||
|
### 3. 并发模型
|
||||||
|
|
||||||
|
- **C端**:
|
||||||
|
- 主线程:发送初始消息
|
||||||
|
- 窗口监控线程:监听SIGWINCH信号
|
||||||
|
- 响应监听线程:接收服务端消息
|
||||||
|
|
||||||
|
- **Go端**:
|
||||||
|
- 主goroutine:处理业务逻辑
|
||||||
|
- 监听goroutine:接收客户端更新消息
|
||||||
|
|
||||||
|
### 4. 协议扩展性
|
||||||
|
|
||||||
|
设计的消息类型枚举支持轻松添加新消息类型:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
MSG_TYPE_INIT = 1,
|
||||||
|
MSG_TYPE_WINDOW_SIZE_UPDATE = 2,
|
||||||
|
MSG_TYPE_SERVER_RESPONSE = 3,
|
||||||
|
MSG_TYPE_CLOSE = 4,
|
||||||
|
// 可以继续添加新类型...
|
||||||
|
MSG_TYPE_HEARTBEAT = 5,
|
||||||
|
MSG_TYPE_FILE_TRANSFER = 6,
|
||||||
|
// ...
|
||||||
|
} MessageType;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
- **轮询间隔**:100ms,平衡响应性和CPU使用
|
||||||
|
- **消息大小**:终端信息约60-80字节,网络开销小
|
||||||
|
- **内存使用**:动态分配,用完即释放
|
||||||
|
- **线程数量**:固定3个线程(C端),不会无限增长
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
- **Linux内核**: 2.6+(支持SIGWINCH信号)
|
||||||
|
- **Glibc**: 2.17+(pthread支持)
|
||||||
|
- **Go版本**: 1.16+
|
||||||
|
|
||||||
|
## 未来优化方向
|
||||||
|
|
||||||
|
1. **去重机制**:连续相同窗口大小不重复发送
|
||||||
|
2. **批量更新**:合并短时间内的多次变化
|
||||||
|
3. **心跳机制**:检测连接状态
|
||||||
|
4. **压缩传输**:对大载荷使用压缩
|
||||||
|
5. **加密通信**:添加消息加密层
|
||||||
|
|
||||||
|
## 测试覆盖
|
||||||
|
|
||||||
|
- ✓ 基本消息收发
|
||||||
|
- ✓ 窗口大小监听
|
||||||
|
- ✓ 多线程并发
|
||||||
|
- ✓ 异常断开处理
|
||||||
|
- ✓ 大载荷传输
|
||||||
|
- ⚠ 压力测试(待完善)
|
||||||
|
- ⚠ 内存泄漏检测(待完善)
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本次改进将原有的简单socket通信升级为现代化的、可扩展的双向通信机制。通过采用结构化协议、多线程设计和信号驱动机制,实现了实时监听终端窗口大小变化的功能,为后续更多实时交互特性奠定了基础。
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
# Socket 实时终端信息同步
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本项目实现了一个现代化的 Unix Socket 通信机制,支持 C 客户端和 Go 服务端之间的双向消息传递。特别地,实现了实时监听终端窗口大小变化并同步给服务端的功能。
|
||||||
|
|
||||||
|
## 架构设计
|
||||||
|
|
||||||
|
### 协议设计
|
||||||
|
|
||||||
|
采用基于消息头的结构化协议:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 消息头(16字节,固定大小)
|
||||||
|
typedef struct {
|
||||||
|
uint32_t magic; // 魔数 0x42534D54 ("BSMT")
|
||||||
|
uint32_t type; // 消息类型
|
||||||
|
uint32_t payload_len; // 载荷长度
|
||||||
|
uint32_t reserved; // 保留字段
|
||||||
|
} MessageHeader;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息类型
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
MSG_TYPE_INIT = 1, // 初始化连接,发送命令信息
|
||||||
|
MSG_TYPE_WINDOW_SIZE_UPDATE = 2, // 终端窗口大小更新
|
||||||
|
MSG_TYPE_SERVER_RESPONSE = 3, // 服务器响应消息
|
||||||
|
MSG_TYPE_CLOSE = 4 // 关闭连接
|
||||||
|
} MessageType;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 1. 窗口大小实时监听(C端)
|
||||||
|
|
||||||
|
使用 SIGWINCH 信号监听终端窗口大小变化:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 信号处理器
|
||||||
|
static void handle_sigwinch(int sig) {
|
||||||
|
g_window_size_changed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册信号
|
||||||
|
struct sigaction sa;
|
||||||
|
sa.sa_handler = handle_sigwinch;
|
||||||
|
sigaction(SIGWINCH, &sa, NULL);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 独立监控线程
|
||||||
|
|
||||||
|
启动专门的线程监控窗口变化:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void* window_monitor_thread(void* arg) {
|
||||||
|
while (1) {
|
||||||
|
if (g_window_size_changed) {
|
||||||
|
g_window_size_changed = 0;
|
||||||
|
// 发送窗口大小更新消息
|
||||||
|
send_terminal_info(sock, MSG_TYPE_WINDOW_SIZE_UPDATE);
|
||||||
|
}
|
||||||
|
usleep(100000); // 100ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Go服务端实时接收
|
||||||
|
|
||||||
|
服务端启动goroutine持续监听客户端消息:
|
||||||
|
|
||||||
|
```go
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
msgType, payload, err := readMessage(conn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msgType == MsgTypeWindowSizeUpdate {
|
||||||
|
termInfo, _ := parseTerminalInfo(payload)
|
||||||
|
params.TerminalInfo = *termInfo
|
||||||
|
logging.Info("窗口大小已更新 - %dx%d", termInfo.Rows, termInfo.Cols)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 终端信息结构
|
||||||
|
|
||||||
|
### 固定部分(C结构体)
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint32_t is_tty; // 是否为TTY
|
||||||
|
uint16_t rows; // 行数
|
||||||
|
uint16_t cols; // 列数
|
||||||
|
uint16_t x_pixel; // X像素
|
||||||
|
uint16_t y_pixel; // Y像素
|
||||||
|
uint32_t has_termios; // 是否有termios属性
|
||||||
|
uint32_t input_flags; // termios输入标志
|
||||||
|
uint32_t output_flags; // termios输出标志
|
||||||
|
uint32_t control_flags; // termios控制标志
|
||||||
|
uint32_t local_flags; // termios本地标志
|
||||||
|
} TerminalInfoFixed;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 可变部分
|
||||||
|
- `term_type_len` + `term_type` (TERM环境变量)
|
||||||
|
- `shell_type_len` + `shell_type` (SHELL环境变量)
|
||||||
|
|
||||||
|
## 编译和测试
|
||||||
|
|
||||||
|
### 编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd execve_hook
|
||||||
|
make clean
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
1. 启动 Go 服务端:
|
||||||
|
```bash
|
||||||
|
cd go_service
|
||||||
|
sudo ./build/bash_go_service-amd64 daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 在另一个终端运行测试:
|
||||||
|
```bash
|
||||||
|
cd execve_hook
|
||||||
|
./build/test_client
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 在测试运行时调整终端窗口大小,观察服务端日志输出
|
||||||
|
|
||||||
|
## 技术特性
|
||||||
|
|
||||||
|
### 1. 线程安全
|
||||||
|
- 使用 `pthread_mutex` 保护共享的 socket 文件描述符
|
||||||
|
- 使用 `sig_atomic_t` 类型处理信号标志
|
||||||
|
|
||||||
|
### 2. 优雅关闭
|
||||||
|
- 使用 channel 控制 goroutine 生命周期
|
||||||
|
- 通过 `MSG_TYPE_CLOSE` 消息通知客户端关闭
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
- 魔数验证防止协议错误
|
||||||
|
- 完整的错误检查和日志记录
|
||||||
|
|
||||||
|
### 4. 现代化设计
|
||||||
|
- 结构化消息协议
|
||||||
|
- 分离的消息读写函数
|
||||||
|
- 清晰的职责划分
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
execve_hook/src/
|
||||||
|
├── socket_protocol.h # 协议定义头文件
|
||||||
|
├── socket_protocol.c # 协议实现
|
||||||
|
├── client.h # 客户端头文件
|
||||||
|
├── client.c # 客户端实现(含SIGWINCH处理)
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
go_service/internal/services/tasks/
|
||||||
|
└── socket.go # Go服务端实现
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
1. **轮询间隔**:窗口监控线程使用 100ms 轮询间隔,平衡响应性和CPU使用
|
||||||
|
2. **消息大小**:终端信息消息约 60-80 字节,网络开销小
|
||||||
|
3. **并发设计**:独立的监听线程不阻塞主流程
|
||||||
|
|
||||||
|
## 扩展建议
|
||||||
|
|
||||||
|
1. **动态轮询间隔**:可以根据窗口变化频率动态调整轮询间隔
|
||||||
|
2. **去重机制**:连续相同的窗口大小可以不发送更新
|
||||||
|
3. **压缩传输**:对于大量终端信息,可以考虑压缩
|
||||||
|
4. **心跳机制**:添加心跳消息检测连接状态
|
||||||
|
|
||||||
|
## 调试
|
||||||
|
|
||||||
|
开启 DEBUG 模式编译:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make DEBUG=1
|
||||||
|
```
|
||||||
|
|
||||||
|
这将输出详细的调试信息,包括:
|
||||||
|
- 消息发送/接收详情
|
||||||
|
- 窗口大小变化事件
|
||||||
|
- 线程生命周期信息
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
# 终端交互事件捕获功能
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
现在socket通信系统已支持捕获和传输所有终端交互事件,包括:
|
||||||
|
- 键盘输入
|
||||||
|
- 鼠标点击、移动、滚轮
|
||||||
|
- 终端窗口大小变化
|
||||||
|
|
||||||
|
## 新增消息类型
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
MSG_TYPE_INIT = 1, // 初始化连接
|
||||||
|
MSG_TYPE_WINDOW_SIZE_UPDATE = 2, // 窗口大小更新
|
||||||
|
MSG_TYPE_SERVER_RESPONSE = 3, // 服务器响应
|
||||||
|
MSG_TYPE_CLOSE = 4, // 关闭连接
|
||||||
|
MSG_TYPE_TERMINAL_INPUT = 5, // 终端输入(原始数据)
|
||||||
|
MSG_TYPE_TERMINAL_OUTPUT = 6, // 终端输出
|
||||||
|
MSG_TYPE_MOUSE_EVENT = 7, // 鼠标事件(结构化)
|
||||||
|
MSG_TYPE_KEY_EVENT = 8 // 键盘事件(结构化)
|
||||||
|
} MessageType;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 鼠标事件结构
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint32_t event_type; // 1=按下, 2=释放, 3=移动, 4=滚轮上, 5=滚轮下
|
||||||
|
uint32_t button; // 鼠标按钮(1=左键,2=中键,3=右键)
|
||||||
|
uint32_t x; // X坐标(列)
|
||||||
|
uint32_t y; // Y坐标(行)
|
||||||
|
uint32_t modifiers; // 修饰键(Shift, Ctrl, Alt等)
|
||||||
|
} MouseEvent;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
### 1. 鼠标跟踪启用
|
||||||
|
|
||||||
|
客户端通过ANSI转义序列启用终端的鼠标跟踪:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 启用X11鼠标报告 + SGR扩展模式
|
||||||
|
const char* enable_seq = "\033[?1000h\033[?1002h\033[?1006h";
|
||||||
|
write(STDOUT_FILENO, enable_seq, strlen(enable_seq));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 事件解析
|
||||||
|
|
||||||
|
客户端监听标准输入的ANSI转义序列:
|
||||||
|
|
||||||
|
```
|
||||||
|
鼠标事件格式:\033[<b;x;yM 或 \033[<b;x;ym
|
||||||
|
- b: 按钮编码 + 修饰键
|
||||||
|
- x: 列坐标
|
||||||
|
- y: 行坐标
|
||||||
|
- M: 按下
|
||||||
|
- m: 释放
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 独立输入线程
|
||||||
|
|
||||||
|
专门的线程持续监听stdin:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void* terminal_input_thread(void* arg) {
|
||||||
|
while (!g_should_exit) {
|
||||||
|
// 使用poll等待输入
|
||||||
|
poll(&pfd, 1, 100);
|
||||||
|
|
||||||
|
// 读取数据
|
||||||
|
read(STDIN_FILENO, buf, sizeof(buf));
|
||||||
|
|
||||||
|
// 解析鼠标事件或发送原始输入
|
||||||
|
if (is_mouse_event(buf)) {
|
||||||
|
parse_and_send_mouse_event();
|
||||||
|
} else {
|
||||||
|
send_terminal_input();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Go服务端处理
|
||||||
|
|
||||||
|
服务端接收并记录所有交互事件:
|
||||||
|
|
||||||
|
```go
|
||||||
|
switch msgType {
|
||||||
|
case MsgTypeTerminalInput:
|
||||||
|
logging.Debug("收到终端输入: %d bytes", len(payload))
|
||||||
|
|
||||||
|
case MsgTypeMouseEvent:
|
||||||
|
event := parseMouseEvent(payload)
|
||||||
|
logging.Info("鼠标事件 - 类型:%d, 按钮:%d, 位置:(%d,%d)",
|
||||||
|
event.EventType, event.Button, event.X, event.Y)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 线程架构
|
||||||
|
|
||||||
|
客户端现在运行4个线程:
|
||||||
|
|
||||||
|
1. **主线程**:发送初始化消息,等待其他线程
|
||||||
|
2. **响应监听线程**:接收服务器响应
|
||||||
|
3. **窗口监控线程**:监听SIGWINCH信号
|
||||||
|
4. **输入监听线程**:监听stdin,捕获键盘和鼠标
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd execve_hook
|
||||||
|
make clean
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
1. 启动服务端:
|
||||||
|
```bash
|
||||||
|
cd go_service
|
||||||
|
sudo ./build/bash_go_service-amd64 daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 运行测试程序:
|
||||||
|
```bash
|
||||||
|
cd execve_hook
|
||||||
|
./build/test_window_resize
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 在程序运行时:
|
||||||
|
- 调整终端窗口大小
|
||||||
|
- 点击鼠标
|
||||||
|
- 输入键盘字符
|
||||||
|
|
||||||
|
4. 观察服务端日志输出所有事件
|
||||||
|
|
||||||
|
## 支持的鼠标模式
|
||||||
|
|
||||||
|
### X11鼠标报告模式 (\033[?1000h)
|
||||||
|
- 捕获鼠标按键按下/释放
|
||||||
|
- 基本的鼠标位置报告
|
||||||
|
|
||||||
|
### 单元格运动跟踪 (\033[?1002h)
|
||||||
|
- 捕获鼠标移动(按下按钮时)
|
||||||
|
- 拖拽操作支持
|
||||||
|
|
||||||
|
### SGR扩展模式 (\033[?1006h)
|
||||||
|
- 更好的大坐标支持(超过223列/行)
|
||||||
|
- 更清晰的按下/释放区分
|
||||||
|
|
||||||
|
## 应用场景
|
||||||
|
|
||||||
|
### 1. 用户行为分析
|
||||||
|
记录用户在终端中的所有操作,用于:
|
||||||
|
- 问题诊断
|
||||||
|
- 使用习惯分析
|
||||||
|
- 操作回放
|
||||||
|
|
||||||
|
### 2. 交互式帮助
|
||||||
|
根据用户的鼠标位置提供上下文帮助
|
||||||
|
|
||||||
|
### 3. 终端UI增强
|
||||||
|
捕获鼠标点击实现:
|
||||||
|
- 可点击的链接
|
||||||
|
- 交互式菜单
|
||||||
|
- 拖放操作
|
||||||
|
|
||||||
|
### 4. 会话记录
|
||||||
|
完整记录终端会话,包括:
|
||||||
|
- 所有输入输出
|
||||||
|
- 鼠标操作
|
||||||
|
- 窗口调整
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
- **输入延迟**: < 10ms(poll超时100ms)
|
||||||
|
- **CPU使用**: 每个线程休眠,不占用CPU
|
||||||
|
- **内存**: 每个连接 < 100KB
|
||||||
|
- **带宽**: 鼠标事件20字节,键盘事件按实际输入
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
### 终端支持
|
||||||
|
支持以下现代终端:
|
||||||
|
- ✓ xterm
|
||||||
|
- ✓ gnome-terminal
|
||||||
|
- ✓ konsole
|
||||||
|
- ✓ iTerm2
|
||||||
|
- ✓ Windows Terminal
|
||||||
|
- ✓ alacritty
|
||||||
|
- ✓ kitty
|
||||||
|
- ⚠ screen(部分支持)
|
||||||
|
- ⚠ tmux(需要配置)
|
||||||
|
|
||||||
|
### 禁用鼠标跟踪
|
||||||
|
|
||||||
|
如果不需要鼠标功能,可以注释掉:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// enable_mouse_tracking(STDOUT_FILENO);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试
|
||||||
|
|
||||||
|
开启DEBUG模式查看所有事件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make DEBUG=1
|
||||||
|
./build/test_window_resize
|
||||||
|
```
|
||||||
|
|
||||||
|
输出示例:
|
||||||
|
```
|
||||||
|
Mouse event: type=1, button=1, pos=(25,10)
|
||||||
|
Mouse event: type=2, button=1, pos=(25,10)
|
||||||
|
Terminal input: 5 bytes
|
||||||
|
Window size update sent to server
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **非TTY环境**:如果stdin不是TTY,鼠标跟踪会自动禁用
|
||||||
|
2. **信号处理**:正确处理SIGINT/SIGTERM以清理终端状态
|
||||||
|
3. **原始模式**:如需完全控制,可启用raw模式
|
||||||
|
4. **终端恢复**:程序退出时应恢复终端原始设置
|
||||||
|
|
||||||
|
## 扩展建议
|
||||||
|
|
||||||
|
1. **触摸事件**:支持触摸屏输入
|
||||||
|
2. **手势识别**:识别常见鼠标手势
|
||||||
|
3. **快捷键绑定**:自定义快捷键处理
|
||||||
|
4. **剪贴板集成**:捕获复制粘贴操作
|
||||||
|
5. **焦点事件**:终端获得/失去焦点通知
|
||||||
576
src/client.c
576
src/client.c
|
|
@ -9,33 +9,292 @@
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <poll.h>
|
||||||
|
|
||||||
#include "debug.h"
|
#include "debug.h"
|
||||||
|
#include "socket_protocol.h"
|
||||||
|
|
||||||
#define BUFFER_SIZE 4096
|
#define BUFFER_SIZE 4096
|
||||||
|
|
||||||
// 读取完整消息
|
// 全局变量,用于信号处理器和主线程通信
|
||||||
ssize_t readMessage(int sock, char* buffer, size_t maxSize) {
|
static volatile sig_atomic_t g_window_size_changed = 0;
|
||||||
uint32_t messageLen;
|
static volatile sig_atomic_t g_should_exit = 0;
|
||||||
// 先读取消息长度
|
static int g_socket_fd = -1;
|
||||||
if (read(sock, &messageLen, sizeof(messageLen)) != sizeof(messageLen)) {
|
static pthread_mutex_t g_socket_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static volatile sig_atomic_t g_terminal_modified = 0; // 标记终端是否被修改
|
||||||
|
|
||||||
|
// 恢复终端状态的清理函数
|
||||||
|
static void cleanup_terminal(void) {
|
||||||
|
// 总是尝试恢复终端,即使标志未设置
|
||||||
|
// 因为 pthread_cancel 可能导致线程在设置标志前就被终止
|
||||||
|
if (isatty(STDIN_FILENO)) {
|
||||||
|
disable_mouse_tracking(STDOUT_FILENO);
|
||||||
|
restore_terminal_mode(STDIN_FILENO);
|
||||||
|
g_terminal_modified = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIGWINCH信号处理器
|
||||||
|
static void handle_sigwinch(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
g_window_size_changed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIGINT/SIGTERM信号处理器
|
||||||
|
static void handle_exit_signal(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
g_should_exit = 1;
|
||||||
|
cleanup_terminal(); // 立即恢复终端
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取并发送终端信息
|
||||||
|
static int send_terminal_info(int sock, MessageType msg_type) {
|
||||||
|
TerminalInfoFixed term_info_fixed;
|
||||||
|
memset(&term_info_fixed, 0, sizeof(term_info_fixed));
|
||||||
|
|
||||||
|
// 检查是否为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) {
|
||||||
|
term_info_fixed.rows = ws.ws_row;
|
||||||
|
term_info_fixed.cols = ws.ws_col;
|
||||||
|
term_info_fixed.x_pixel = ws.ws_xpixel;
|
||||||
|
term_info_fixed.y_pixel = ws.ws_ypixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取termios属性
|
||||||
|
struct termios term_attr;
|
||||||
|
if (tcgetattr(STDIN_FILENO, &term_attr) == 0) {
|
||||||
|
term_info_fixed.has_termios = 1;
|
||||||
|
term_info_fixed.input_flags = term_attr.c_iflag;
|
||||||
|
term_info_fixed.output_flags = term_attr.c_oflag;
|
||||||
|
term_info_fixed.control_flags = term_attr.c_cflag;
|
||||||
|
term_info_fixed.local_flags = term_attr.c_lflag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的载荷(固定结构 + 字符串)
|
||||||
|
const char* term_type = getenv("TERM");
|
||||||
|
if (term_type == NULL) term_type = "";
|
||||||
|
const char* shell_type = getenv("SHELL");
|
||||||
|
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;
|
||||||
|
char* payload = malloc(payload_len);
|
||||||
|
if (payload == NULL) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查buffer大小是否足够
|
char* ptr = payload;
|
||||||
if (messageLen >= maxSize) {
|
|
||||||
|
// 写入固定结构
|
||||||
|
memcpy(ptr, &term_info_fixed, sizeof(TerminalInfoFixed));
|
||||||
|
ptr += sizeof(TerminalInfoFixed);
|
||||||
|
|
||||||
|
// 写入TERM类型
|
||||||
|
uint32_t len = 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;
|
||||||
|
memcpy(ptr, &len, sizeof(uint32_t));
|
||||||
|
ptr += sizeof(uint32_t);
|
||||||
|
memcpy(ptr, shell_type, shell_type_len);
|
||||||
|
|
||||||
|
int result = write_message(sock, msg_type, payload, payload_len);
|
||||||
|
free(payload);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析鼠标事件(从ANSI转义序列)
|
||||||
|
static int parse_mouse_event(const char* buf, size_t len, MouseEvent* event) {
|
||||||
|
// 鼠标事件格式: \033[<b;x;yM 或 \033[<b;x;ym
|
||||||
|
// b=按钮+修饰键, x=列, y=行, M=按下, m=释放
|
||||||
|
|
||||||
|
if (len < 6 || buf[0] != '\033' || buf[1] != '[' || buf[2] != '<') {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取完整消息
|
unsigned int button, x, y;
|
||||||
size_t totalRead = 0;
|
char action;
|
||||||
while (totalRead < messageLen) {
|
|
||||||
ssize_t n = read(sock, buffer + totalRead, messageLen - totalRead);
|
int parsed = sscanf(buf + 3, "%u;%u;%u%c", &button, &x, &y, &action);
|
||||||
if (n <= 0) return -1;
|
if (parsed != 4) {
|
||||||
totalRead += n;
|
return -1;
|
||||||
}
|
}
|
||||||
buffer[messageLen] = '\0';
|
|
||||||
return messageLen;
|
event->button = button & 0x03; // 提取按钮信息
|
||||||
|
event->modifiers = button & 0xFC; // 提取修饰键
|
||||||
|
event->x = x;
|
||||||
|
event->y = y;
|
||||||
|
|
||||||
|
if (action == 'M') {
|
||||||
|
event->event_type = MOUSE_BUTTON_PRESS;
|
||||||
|
} else if (action == 'm') {
|
||||||
|
event->event_type = MOUSE_BUTTON_RELEASE;
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听服务器响应的线程
|
||||||
|
static void* response_listener_thread(void* arg) {
|
||||||
|
int* output_fd = (int*)arg;
|
||||||
|
|
||||||
|
while (!g_should_exit) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (sock < 0) {
|
||||||
|
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 (*output_fd == STDOUT_FILENO) {
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
} else if (msg_type == MSG_TYPE_CLOSE) {
|
||||||
|
free_message_payload(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
free_message_payload(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听窗口大小变化的线程
|
||||||
|
static void* window_monitor_thread(void* arg) {
|
||||||
|
(void)arg;
|
||||||
|
|
||||||
|
while (!g_should_exit) {
|
||||||
|
// 等待窗口大小变化信号
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (sock < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送窗口大小更新消息
|
||||||
|
if (send_terminal_info(sock, MSG_TYPE_WINDOW_SIZE_UPDATE) < 0) {
|
||||||
|
DEBUG_LOG("Failed to send window size update\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOG("Window size updated sent to server\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(100000); // 睡眠100ms
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (sock < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用poll检查stdin是否有数据
|
||||||
|
struct pollfd pfd;
|
||||||
|
pfd.fd = STDIN_FILENO;
|
||||||
|
pfd.events = POLLIN;
|
||||||
|
|
||||||
|
int poll_ret = poll(&pfd, 1, 100); // 100ms超时
|
||||||
|
if (poll_ret <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
|
||||||
|
if (n <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf[n] = '\0';
|
||||||
|
|
||||||
|
// 尝试解析鼠标事件
|
||||||
|
if (n >= 6 && 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);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则作为普通终端输入发送
|
||||||
|
write_message(sock, MSG_TYPE_TERMINAL_INPUT, buf, (uint32_t)n);
|
||||||
|
DEBUG_LOG("Terminal input: %zd bytes\n", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int seeking_solutions(const char* filename, char* const argv[],
|
int seeking_solutions(const char* filename, char* const argv[],
|
||||||
|
|
@ -49,6 +308,7 @@ int seeking_solutions(const char* filename, char* const argv[],
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理日志路径
|
||||||
if (logPath[0] != '/') { // 相对路径
|
if (logPath[0] != '/') { // 相对路径
|
||||||
size_t pwd_len = strlen(pwd);
|
size_t pwd_len = strlen(pwd);
|
||||||
size_t log_len = strlen(logPath);
|
size_t log_len = strlen(logPath);
|
||||||
|
|
@ -75,18 +335,13 @@ int seeking_solutions(const char* filename, char* const argv[],
|
||||||
abs_path[PATH_MAX - 1] = '\0';
|
abs_path[PATH_MAX - 1] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t path_len = strlen(abs_path);
|
// 创建socket连接
|
||||||
|
|
||||||
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
if (sock == -1) {
|
if (sock == -1) {
|
||||||
perror("socket");
|
perror("socket");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置TCP_NODELAY
|
|
||||||
// int flag = 1;
|
|
||||||
// setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(int));
|
|
||||||
|
|
||||||
struct sockaddr_un addr;
|
struct sockaddr_un addr;
|
||||||
memset(&addr, 0, sizeof(addr));
|
memset(&addr, 0, sizeof(addr));
|
||||||
addr.sun_family = AF_UNIX;
|
addr.sun_family = AF_UNIX;
|
||||||
|
|
@ -99,115 +354,216 @@ int seeking_solutions(const char* filename, char* const argv[],
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送文件名
|
// 设置全局socket
|
||||||
|
pthread_mutex_lock(&g_socket_mutex);
|
||||||
|
g_socket_fd = sock;
|
||||||
|
pthread_mutex_unlock(&g_socket_mutex);
|
||||||
|
|
||||||
|
// 设置信号处理器
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 清理函数,确保任何退出都恢复终端
|
||||||
|
atexit(cleanup_terminal);
|
||||||
|
|
||||||
|
// 构建初始化消息载荷
|
||||||
size_t filename_len = strlen(filename);
|
size_t filename_len = strlen(filename);
|
||||||
write(sock, &filename_len, sizeof(size_t));
|
|
||||||
write(sock, filename, filename_len);
|
|
||||||
|
|
||||||
// 发送当前工作目录
|
|
||||||
size_t pwd_len = strlen(pwd);
|
size_t pwd_len = strlen(pwd);
|
||||||
write(sock, &pwd_len, sizeof(size_t));
|
size_t abs_path_len = strlen(abs_path);
|
||||||
write(sock, pwd, pwd_len);
|
|
||||||
|
|
||||||
// 发送argv
|
// 计算args总长度
|
||||||
int argc = 0;
|
int argc = 0;
|
||||||
while (argv[argc] != NULL) argc++;
|
size_t args_total_len = 0;
|
||||||
write(sock, &argc, sizeof(int));
|
while (argv[argc] != NULL) {
|
||||||
|
args_total_len += sizeof(uint64_t) + strlen(argv[argc]);
|
||||||
for (int i = 0; i < argc; i++) {
|
argc++;
|
||||||
size_t arg_len = strlen(argv[i]);
|
|
||||||
write(sock, &arg_len, sizeof(size_t));
|
|
||||||
write(sock, argv[i], arg_len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送envp
|
// 计算envs总长度
|
||||||
int envc = 0;
|
int envc = 0;
|
||||||
while (envp[envc] != NULL) envc++;
|
size_t envs_total_len = 0;
|
||||||
write(sock, &envc, sizeof(int));
|
while (envp[envc] != NULL) {
|
||||||
|
envs_total_len += sizeof(uint64_t) + strlen(envp[envc]);
|
||||||
for (int i = 0; i < envc; i++) {
|
envc++;
|
||||||
size_t env_len = strlen(envp[i]);
|
|
||||||
write(sock, &env_len, sizeof(size_t));
|
|
||||||
write(sock, envp[i], env_len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送logPath
|
// 计算终端信息长度
|
||||||
write(sock, &path_len, sizeof(size_t));
|
|
||||||
write(sock, abs_path, path_len);
|
|
||||||
|
|
||||||
// 发送终端信息
|
|
||||||
// 1. 检查是否为交互式终端
|
|
||||||
int is_tty = isatty(STDIN_FILENO);
|
|
||||||
write(sock, &is_tty, sizeof(int));
|
|
||||||
|
|
||||||
// 2. 如果是终端,获取窗口大小
|
|
||||||
struct winsize ws;
|
|
||||||
memset(&ws, 0, sizeof(ws));
|
|
||||||
if (is_tty) {
|
|
||||||
ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
|
|
||||||
}
|
|
||||||
write(sock, &ws.ws_row, sizeof(unsigned short)); // 行数
|
|
||||||
write(sock, &ws.ws_col, sizeof(unsigned short)); // 列数
|
|
||||||
write(sock, &ws.ws_xpixel, sizeof(unsigned short)); // X像素
|
|
||||||
write(sock, &ws.ws_ypixel, sizeof(unsigned short)); // Y像素
|
|
||||||
|
|
||||||
// 3. 获取终端类型
|
|
||||||
const char* term_type = getenv("TERM");
|
const char* term_type = getenv("TERM");
|
||||||
if (term_type == NULL) {
|
if (term_type == NULL) term_type = "";
|
||||||
term_type = "";
|
|
||||||
}
|
|
||||||
size_t term_type_len = strlen(term_type);
|
|
||||||
write(sock, &term_type_len, sizeof(size_t));
|
|
||||||
write(sock, term_type, term_type_len);
|
|
||||||
|
|
||||||
// 4. 获取SHELL类型
|
|
||||||
const char* shell_type = getenv("SHELL");
|
const char* shell_type = getenv("SHELL");
|
||||||
if (shell_type == NULL) {
|
if (shell_type == NULL) shell_type = "";
|
||||||
shell_type = "";
|
size_t term_type_len = strlen(term_type);
|
||||||
}
|
|
||||||
size_t shell_type_len = strlen(shell_type);
|
size_t shell_type_len = strlen(shell_type);
|
||||||
write(sock, &shell_type_len, sizeof(size_t));
|
size_t term_info_len = sizeof(TerminalInfoFixed) + sizeof(uint32_t) + term_type_len + sizeof(uint32_t) + shell_type_len;
|
||||||
write(sock, shell_type, shell_type_len);
|
|
||||||
|
|
||||||
// 5. 获取终端属性(如果是tty)
|
// 总载荷长度
|
||||||
struct termios term_attr;
|
uint32_t total_payload_len = sizeof(uint64_t) + filename_len +
|
||||||
int has_termios = 0;
|
sizeof(uint64_t) + pwd_len +
|
||||||
if (is_tty && tcgetattr(STDIN_FILENO, &term_attr) == 0) {
|
sizeof(int32_t) + args_total_len +
|
||||||
has_termios = 1;
|
sizeof(int32_t) + envs_total_len +
|
||||||
}
|
sizeof(uint64_t) + abs_path_len +
|
||||||
write(sock, &has_termios, sizeof(int));
|
term_info_len;
|
||||||
if (has_termios) {
|
|
||||||
// 发送关键的termios标志
|
char* init_payload = malloc(total_payload_len);
|
||||||
write(sock, &term_attr.c_iflag, sizeof(tcflag_t));
|
if (init_payload == NULL) {
|
||||||
write(sock, &term_attr.c_oflag, sizeof(tcflag_t));
|
close(sock);
|
||||||
write(sock, &term_attr.c_cflag, sizeof(tcflag_t));
|
return -1;
|
||||||
write(sock, &term_attr.c_lflag, sizeof(tcflag_t));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接收服务器响应
|
char* ptr = init_payload;
|
||||||
char buffer[BUFFER_SIZE];
|
|
||||||
char display_buffer[BUFFER_SIZE];
|
|
||||||
ssize_t bytes_read;
|
|
||||||
|
|
||||||
// 持续读取消息直到socket关闭
|
// 写入filename
|
||||||
while (1) {
|
uint64_t len64 = filename_len;
|
||||||
bytes_read = readMessage(sock, buffer, BUFFER_SIZE);
|
memcpy(ptr, &len64, sizeof(uint64_t));
|
||||||
if (bytes_read <= 0) {
|
ptr += sizeof(uint64_t);
|
||||||
break;
|
memcpy(ptr, filename, filename_len);
|
||||||
|
ptr += filename_len;
|
||||||
|
|
||||||
|
// 写入pwd
|
||||||
|
len64 = pwd_len;
|
||||||
|
memcpy(ptr, &len64, sizeof(uint64_t));
|
||||||
|
ptr += sizeof(uint64_t);
|
||||||
|
memcpy(ptr, pwd, pwd_len);
|
||||||
|
ptr += pwd_len;
|
||||||
|
|
||||||
|
// 写入argc和args
|
||||||
|
int32_t argc32 = argc;
|
||||||
|
memcpy(ptr, &argc32, sizeof(int32_t));
|
||||||
|
ptr += sizeof(int32_t);
|
||||||
|
for (int i = 0; i < argc; i++) {
|
||||||
|
uint64_t arg_len = strlen(argv[i]);
|
||||||
|
memcpy(ptr, &arg_len, sizeof(uint64_t));
|
||||||
|
ptr += sizeof(uint64_t);
|
||||||
|
memcpy(ptr, argv[i], arg_len);
|
||||||
|
ptr += arg_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入envc和envs
|
||||||
|
int32_t envc32 = envc;
|
||||||
|
memcpy(ptr, &envc32, sizeof(int32_t));
|
||||||
|
ptr += sizeof(int32_t);
|
||||||
|
for (int i = 0; i < envc; i++) {
|
||||||
|
uint64_t env_len = strlen(envp[i]);
|
||||||
|
memcpy(ptr, &env_len, sizeof(uint64_t));
|
||||||
|
ptr += sizeof(uint64_t);
|
||||||
|
memcpy(ptr, envp[i], env_len);
|
||||||
|
ptr += env_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入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) {
|
||||||
|
term_info_fixed.rows = ws.ws_row;
|
||||||
|
term_info_fixed.cols = ws.ws_col;
|
||||||
|
term_info_fixed.x_pixel = ws.ws_xpixel;
|
||||||
|
term_info_fixed.y_pixel = ws.ws_ypixel;
|
||||||
}
|
}
|
||||||
|
struct termios term_attr;
|
||||||
strncpy(display_buffer, buffer, BUFFER_SIZE - 1);
|
if (tcgetattr(STDIN_FILENO, &term_attr) == 0) {
|
||||||
display_buffer[BUFFER_SIZE - 1] = '\0';
|
term_info_fixed.has_termios = 1;
|
||||||
|
term_info_fixed.input_flags = term_attr.c_iflag;
|
||||||
// 使用指定的输出描述符,如果为NULL则使用stdout
|
term_info_fixed.output_flags = term_attr.c_oflag;
|
||||||
write(*output_fd, display_buffer, strlen(display_buffer));
|
term_info_fixed.control_flags = term_attr.c_cflag;
|
||||||
|
term_info_fixed.local_flags = term_attr.c_lflag;
|
||||||
// 如果是标准输出,flush
|
|
||||||
if (*output_fd == STDOUT_FILENO) {
|
|
||||||
fflush(stdout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memcpy(ptr, &term_info_fixed, sizeof(TerminalInfoFixed));
|
||||||
|
ptr += sizeof(TerminalInfoFixed);
|
||||||
|
|
||||||
|
uint32_t len32 = 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;
|
||||||
|
memcpy(ptr, &len32, sizeof(uint32_t));
|
||||||
|
ptr += sizeof(uint32_t);
|
||||||
|
memcpy(ptr, shell_type, 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;
|
||||||
|
}
|
||||||
|
free(init_payload);
|
||||||
|
|
||||||
|
// 启动响应监听线程
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动窗口监听线程
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动终端输入监听线程
|
||||||
|
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);
|
||||||
|
pthread_cancel(window_thread);
|
||||||
|
pthread_join(response_thread, NULL);
|
||||||
|
pthread_join(window_thread, NULL);
|
||||||
|
close(sock);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待响应线程结束(表示服务器关闭连接)
|
||||||
|
pthread_join(response_thread, NULL);
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
g_should_exit = 1;
|
||||||
|
pthread_cancel(window_thread);
|
||||||
|
pthread_cancel(input_thread);
|
||||||
|
pthread_join(window_thread, NULL);
|
||||||
|
pthread_join(input_thread, NULL);
|
||||||
|
|
||||||
|
// 恢复终端状态
|
||||||
|
cleanup_terminal();
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_socket_mutex);
|
||||||
|
g_socket_fd = -1;
|
||||||
|
pthread_mutex_unlock(&g_socket_mutex);
|
||||||
|
|
||||||
close(sock);
|
close(sock);
|
||||||
|
DEBUG_LOG("connection done.\n");
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
#include "socket_protocol.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include "debug.h"
|
||||||
|
|
||||||
|
// 保存原始终端设置
|
||||||
|
static struct termios g_original_termios;
|
||||||
|
static int g_termios_saved = 0;
|
||||||
|
|
||||||
|
// 写入完整消息
|
||||||
|
int write_message(int sock, MessageType type, const void* payload, uint32_t payload_len) {
|
||||||
|
MessageHeader header;
|
||||||
|
header.magic = MESSAGE_MAGIC;
|
||||||
|
header.type = type;
|
||||||
|
header.payload_len = payload_len;
|
||||||
|
header.reserved = 0;
|
||||||
|
|
||||||
|
// 发送消息头
|
||||||
|
ssize_t written = write(sock, &header, sizeof(header));
|
||||||
|
if (written != sizeof(header)) {
|
||||||
|
DEBUG_LOG("Failed to write message header\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入载荷
|
||||||
|
if (payload_len > 0 && payload != NULL) {
|
||||||
|
written = write(sock, payload, payload_len);
|
||||||
|
if (written != (ssize_t)payload_len) {
|
||||||
|
DEBUG_LOG("Failed to write message payload\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取完整消息
|
||||||
|
int read_message(int sock, MessageType* type, void** payload, uint32_t* payload_len) {
|
||||||
|
MessageHeader header;
|
||||||
|
|
||||||
|
// 读取消息头
|
||||||
|
ssize_t bytes_read = read(sock, &header, sizeof(header));
|
||||||
|
if (bytes_read != sizeof(header)) {
|
||||||
|
if (bytes_read == 0) {
|
||||||
|
return 0; // 连接正常关闭
|
||||||
|
}
|
||||||
|
DEBUG_LOG("Failed to read message header, got %zd bytes\n", bytes_read);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证魔数
|
||||||
|
if (header.magic != MESSAGE_MAGIC) {
|
||||||
|
DEBUG_LOG("Invalid message magic: 0x%x\n", header.magic);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*type = (MessageType)header.type;
|
||||||
|
*payload_len = header.payload_len;
|
||||||
|
|
||||||
|
// 读取载荷
|
||||||
|
if (header.payload_len > 0) {
|
||||||
|
*payload = malloc(header.payload_len + 1); // +1 用于可能的字符串终止符
|
||||||
|
if (*payload == NULL) {
|
||||||
|
DEBUG_LOG("Failed to allocate memory for payload\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes_read = read(sock, *payload, header.payload_len);
|
||||||
|
if (bytes_read != (ssize_t)header.payload_len) {
|
||||||
|
DEBUG_LOG("Failed to read message payload\n");
|
||||||
|
free(*payload);
|
||||||
|
*payload = NULL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是字符串,添加终止符
|
||||||
|
((char*)(*payload))[header.payload_len] = '\0';
|
||||||
|
} else {
|
||||||
|
*payload = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放消息载荷
|
||||||
|
void free_message_payload(void* payload) {
|
||||||
|
if (payload != NULL) {
|
||||||
|
free(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置终端为原始模式(捕获所有输入)
|
||||||
|
int setup_terminal_raw_mode(int fd) {
|
||||||
|
struct termios raw;
|
||||||
|
|
||||||
|
// 保存原始终端设置(只保存一次)
|
||||||
|
if (!g_termios_saved) {
|
||||||
|
if (tcgetattr(fd, &g_original_termios) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
g_termios_saved = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tcgetattr(fd, &raw) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置原始模式
|
||||||
|
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
|
||||||
|
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
|
||||||
|
raw.c_cflag &= ~(CSIZE | PARENB);
|
||||||
|
raw.c_cflag |= CS8;
|
||||||
|
raw.c_oflag &= ~(OPOST);
|
||||||
|
|
||||||
|
// 设置读取超时
|
||||||
|
raw.c_cc[VMIN] = 0;
|
||||||
|
raw.c_cc[VTIME] = 1;
|
||||||
|
|
||||||
|
if (tcsetattr(fd, TCSAFLUSH, &raw) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复终端模式
|
||||||
|
int restore_terminal_mode(int fd) {
|
||||||
|
if (!g_termios_saved) {
|
||||||
|
return -1; // 没有保存过,无法恢复
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tcsetattr(fd, TCSAFLUSH, &g_original_termios) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_termios_saved = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用鼠标跟踪(发送ANSI转义序列)
|
||||||
|
int enable_mouse_tracking(int fd) {
|
||||||
|
// 启用鼠标跟踪模式
|
||||||
|
// \033[?1000h - 启用X11鼠标报告
|
||||||
|
// \033[?1002h - 启用单元格运动鼠标跟踪
|
||||||
|
// \033[?1003h - 启用所有运动鼠标跟踪
|
||||||
|
// \033[?1006h - 启用SGR扩展鼠标模式
|
||||||
|
const char* enable_seq = "\033[?1000h\033[?1002h\033[?1006h";
|
||||||
|
|
||||||
|
ssize_t written = write(fd, enable_seq, strlen(enable_seq));
|
||||||
|
if (written < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用鼠标跟踪
|
||||||
|
int disable_mouse_tracking(int fd) {
|
||||||
|
// 禁用鼠标跟踪模式
|
||||||
|
// \033[?1000l - 禁用X11鼠标报告
|
||||||
|
// \033[?1002l - 禁用单元格运动鼠标跟踪
|
||||||
|
// \033[?1006l - 禁用SGR扩展鼠标模式
|
||||||
|
const char* disable_seq = "\033[?1006l\033[?1002l\033[?1000l";
|
||||||
|
|
||||||
|
ssize_t written = write(fd, disable_seq, strlen(disable_seq));
|
||||||
|
if (written < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
#ifndef SOCKET_PROTOCOL_H
|
||||||
|
#define SOCKET_PROTOCOL_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <termios.h>
|
||||||
|
|
||||||
|
// 消息类型枚举
|
||||||
|
typedef enum {
|
||||||
|
MSG_TYPE_INIT = 1, // 初始化连接,发送命令信息
|
||||||
|
MSG_TYPE_WINDOW_SIZE_UPDATE = 2, // 终端窗口大小更新
|
||||||
|
MSG_TYPE_SERVER_RESPONSE = 3, // 服务器响应消息
|
||||||
|
MSG_TYPE_CLOSE = 4, // 关闭连接
|
||||||
|
MSG_TYPE_TERMINAL_INPUT = 5, // 终端输入数据(键盘、鼠标等)
|
||||||
|
MSG_TYPE_TERMINAL_OUTPUT = 6, // 终端输出数据
|
||||||
|
MSG_TYPE_MOUSE_EVENT = 7, // 鼠标事件
|
||||||
|
MSG_TYPE_KEY_EVENT = 8 // 键盘事件
|
||||||
|
} MessageType;
|
||||||
|
|
||||||
|
// 消息头结构(固定大小)
|
||||||
|
typedef struct {
|
||||||
|
uint32_t magic; // 魔数,用于验证 0x42534D54 ("BSMT")
|
||||||
|
uint32_t type; // 消息类型
|
||||||
|
uint32_t payload_len; // 载荷长度
|
||||||
|
uint32_t reserved; // 保留字段,用于对齐
|
||||||
|
} __attribute__((packed)) MessageHeader;
|
||||||
|
|
||||||
|
// 终端信息结构
|
||||||
|
typedef struct {
|
||||||
|
uint32_t is_tty; // 是否为TTY
|
||||||
|
uint16_t rows; // 行数
|
||||||
|
uint16_t cols; // 列数
|
||||||
|
uint16_t x_pixel; // X像素
|
||||||
|
uint16_t y_pixel; // Y像素
|
||||||
|
uint32_t has_termios; // 是否有termios属性
|
||||||
|
uint32_t input_flags; // termios输入标志
|
||||||
|
uint32_t output_flags; // termios输出标志
|
||||||
|
uint32_t control_flags; // termios控制标志
|
||||||
|
uint32_t local_flags; // termios本地标志
|
||||||
|
} __attribute__((packed)) TerminalInfoFixed;
|
||||||
|
|
||||||
|
// 鼠标事件类型
|
||||||
|
typedef enum {
|
||||||
|
MOUSE_BUTTON_PRESS = 1,
|
||||||
|
MOUSE_BUTTON_RELEASE = 2,
|
||||||
|
MOUSE_MOVE = 3,
|
||||||
|
MOUSE_SCROLL_UP = 4,
|
||||||
|
MOUSE_SCROLL_DOWN = 5
|
||||||
|
} MouseEventType;
|
||||||
|
|
||||||
|
// 鼠标事件结构
|
||||||
|
typedef struct {
|
||||||
|
uint32_t event_type; // MouseEventType
|
||||||
|
uint32_t button; // 鼠标按钮(1=左键,2=中键,3=右键)
|
||||||
|
uint32_t x; // X坐标
|
||||||
|
uint32_t y; // Y坐标
|
||||||
|
uint32_t modifiers; // 修饰键(Shift, Ctrl, Alt等)
|
||||||
|
} __attribute__((packed)) MouseEvent;
|
||||||
|
|
||||||
|
// 键盘事件结构
|
||||||
|
typedef struct {
|
||||||
|
uint32_t key_code; // 键码
|
||||||
|
uint32_t modifiers; // 修饰键
|
||||||
|
uint32_t is_press; // 1=按下,0=释放
|
||||||
|
} __attribute__((packed)) KeyEvent;
|
||||||
|
|
||||||
|
// 魔数定义
|
||||||
|
#define MESSAGE_MAGIC 0x42534D54 // "BSMT"
|
||||||
|
|
||||||
|
// 函数声明
|
||||||
|
int write_message(int sock, MessageType type, const void* payload, uint32_t payload_len);
|
||||||
|
int read_message(int sock, MessageType* type, void** payload, uint32_t* payload_len);
|
||||||
|
void free_message_payload(void* payload);
|
||||||
|
|
||||||
|
// 终端输入捕获相关函数
|
||||||
|
int setup_terminal_raw_mode(int fd);
|
||||||
|
int restore_terminal_mode(int fd);
|
||||||
|
int enable_mouse_tracking(int fd);
|
||||||
|
int disable_mouse_tracking(int fd);
|
||||||
|
|
||||||
|
#endif // SOCKET_PROTOCOL_H
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
// 声明client.h中的函数
|
||||||
|
int seeking_solutions(const char* filename, char* const argv[],
|
||||||
|
char* const envp[], const char* logPath, int* output_fd);
|
||||||
|
|
||||||
|
static volatile int should_exit = 0;
|
||||||
|
|
||||||
|
void handle_sigint(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
should_exit = 1;
|
||||||
|
printf("\n\n收到退出信号,正在关闭...\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 设置信号处理
|
||||||
|
signal(SIGINT, handle_sigint);
|
||||||
|
|
||||||
|
// 检查服务端是否运行(静默检查)
|
||||||
|
if (access("/var/run/bash-smart.sock", F_OK) != 0) {
|
||||||
|
fprintf(stderr, "错误:无法连接到服务端 (socket: /var/run/bash-smart.sock)\n");
|
||||||
|
fprintf(stderr, "请先启动 Go 服务端\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建模拟的命令执行场景
|
||||||
|
const char* filename = "/usr/bin/test_command";
|
||||||
|
char* argv[] = {
|
||||||
|
(char*)"test_command",
|
||||||
|
(char*)"--option",
|
||||||
|
(char*)"value",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简单的环境变量
|
||||||
|
extern char** environ;
|
||||||
|
char** envp = environ;
|
||||||
|
|
||||||
|
const char* logPath = "/tmp/test_client_error.log";
|
||||||
|
|
||||||
|
// 创建测试错误日志
|
||||||
|
FILE* f = fopen(logPath, "w");
|
||||||
|
if (f) {
|
||||||
|
fprintf(f, "测试错误信息:命令执行失败\n");
|
||||||
|
fprintf(f, "Error: Command not found\n");
|
||||||
|
fprintf(f, "Exit code: 127\n");
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int output_fd = STDOUT_FILENO;
|
||||||
|
|
||||||
|
// 连接并进入交互模式(所有输出由 Go 服务端控制)
|
||||||
|
int result = seeking_solutions(filename, argv, envp, logPath, &output_fd);
|
||||||
|
|
||||||
|
// 清理测试文件
|
||||||
|
unlink(logPath);
|
||||||
|
|
||||||
|
// 退出时不输出任何内容,让终端保持干净
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue