fix: 修复LD_PRELOAD bash -c执行时无法回显的bug, 修复窗口大小改变时无法同步子进程的bug

This commit is contained in:
QCQCQC@Debian 2025-12-05 17:13:20 +08:00
parent 3be4ba6e4b
commit cf33cff0a5
7 changed files with 291 additions and 138 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
build/ build/
output.txt output.txt
logs/

View File

@ -29,6 +29,11 @@ ifeq ($(HOOK),1)
CFLAGS += -DHOOK CFLAGS += -DHOOK
endif endif
# 如果跳过检查,只需执行 make NO_CONFIG_CHECK=1
ifeq ($(NO_CONFIG_CHECK),1)
CFLAGS += -DNO_CONFIG_CHECK
endif
.PHONY: all clean debug hook rebuild pre_build .PHONY: all clean debug hook rebuild pre_build
all: pre_build $(TARGET) $(HOOK_TARGET) all: pre_build $(TARGET) $(HOOK_TARGET)
@ -40,6 +45,9 @@ endif
ifeq ($(HOOK),1) ifeq ($(HOOK),1)
@echo "Building with hook flags..." @echo "Building with hook flags..."
endif endif
ifeq ($(NO_CONFIG_CHECK),1)
@echo "Building with NO_CONFIG_CHECK defined..."
endif
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)

316
README.md
View File

@ -1,131 +1,233 @@
# execve 拦截器 # Execve Hook Project
## 简介 本项目是一个基于 `LD_PRELOAD` 机制的系统调用拦截库,主要用于监控和控制 Linux 系统中的命令执行(`execve`)。它能够拦截命令执行请求,检查配置规则,记录执行日志,甚至通过 PTY伪终端捕获命令的输入输出。
本项目是一个通过动态链接库预加载 (LD\_PRELOAD) 方式实现的 `execve` 系统调用拦截器。它可以根据预定义的规则,在程序执行指定的命令时进行警告、阻止或记录操作。 ## 核心逻辑概述
**主要功能:** 整个 C 代码库的逻辑可以分为以下几个核心模块:
* **命令拦截与告警:** 当执行配置中指定的命令时,可以显示警告信息并询问用户是否继续执行。 ### 1. `execve` 拦截器 (`src/execve_interceptor.c`)
* **命令阻止:** 可以完全阻止执行配置中指定的命令。
* **参数匹配:** 支持基于命令参数进行更细粒度的规则匹配。
* **配置热加载:** 能够检测到配置文件修改并自动重新加载规则。
* **日志记录:** 记录所有通过 `execve` 执行的命令及其参数。
* **输出重定向与错误检测:** 将执行命令的标准输出和错误输出记录到日志文件,并能在检测到错误时发出提示。
* **仅拦截终端 Shell:** 默认只在终端 Shell (如 bash, zsh, fish, sh) 中拦截 `execve` 调用,避免影响其他程序。
* **功能开关:** 可以通过配置文件中的 `enabled` 字段全局启用或禁用拦截功能。
## 安装 这是项目的核心入口。
1. **前提条件:** - **拦截机制**: 通过 `LD_PRELOAD` 预加载动态库,覆盖系统默认的 `execve` 函数。
* 安装 `gcc` 等编译工具。 - **主要流程**:
* 安装 `json-c` 库 (开发版本)。在 Debian/Ubuntu 上可以使用 `sudo apt-get install libjson-c-dev` 安装,在 CentOS/RHEL 上可以使用 `sudo yum install json-c-devel` 安装。 1. **环境清理**: 在调用前移除 `LD_PRELOAD` 环境变量,防止对子进程造成非预期的递归影响。
2. **配置加载**: 调用 `config.c` 从共享内存中加载配置。
3. **规则匹配**: 使用 `rules.c` 检查当前执行的命令是否命中特定规则。
4. **行为决策**: 根据规则决定是否需要拦截、记录日志或通过 PTY 执行。
5. **执行原函数**: 最终调用原始的 `execve` 系统调用执行目标程序。
2. **编译:** ```mermaid
```bash sequenceDiagram
gcc -shared -fPIC execve_interceptor.c -o execve_interceptor.so -ldl -ljson-c participant User as User/Shell
``` participant Hook as execve_interceptor
* 确保你的代码文件名为 `execve_interceptor.c` participant Config as Config (Shm)
participant Rules as Rules Engine
participant PTY as PTY/IO Handler
participant Sys as Original execve
3. **创建配置和日志目录 (如果不存在):** User->>Hook: execve(filename, argv, envp)
```bash Hook->>Hook: Remove LD_PRELOAD
mkdir -p config logs Hook->>Hook: dlsym(RTLD_NEXT, "execve")
```
4. **创建配置文件:** Hook->>Config: load_config()
* 在 `config` 目录下创建 `execve_rules.json` 文件,并按照以下格式配置规则: Config-->>Hook: ConfigData*
```json alt Not Terminal Shell or Config Disabled
{ Hook->>Sys: orig_execve(...)
"enabled": true, end
"rules": [
{
"cmd": "nvidia-smi",
"type": "warn",
"msg": "在沐曦环境下请执行mx-smi"
},
{
"cmd": "rm",
"type": "error",
"msg": "Error: rm command is forbidden"
},
{
"cmd": "pip",
"type": "warn",
"msg": "使用pip安装torch时请注意使用厂家支持版本",
"args": ["install", "torch"]
}
]
}
```
* `enabled`: 布尔值,用于启用或禁用整个拦截器功能。 loop Check Rules
* `rules`: 一个 JSON 数组,包含多个规则对象。 Hook->>Rules: args_match(argv, rule)
* `cmd`: 要拦截的命令名称。 Rules-->>Hook: Match Result
* `type`: 拦截类型,可以是 `"warn"` (警告) 或 `"error"` (阻止)。 end
* `msg`: 当规则匹配时显示的消息。
* `args` (可选): 一个字符串数组,指定需要匹配的命令参数。只有当所有指定的参数都存在时,规则才匹配。
## 配置 alt Rule Type: SKIP
Hook->>Sys: orig_execve(...)
* **`config/execve_rules.json`:** 这是主要的配置文件,用于定义拦截规则。你可以根据需要添加、修改或删除规则。 else Rule Type: WARN
* **`logs/execve.log`:** 记录所有通过 `execve` 执行的命令及其参数。 Hook->>User: Print Warning
* **`logs/execve_out.log`:** 记录被拦截命令的标准输出和错误输出。 User->>Hook: Confirm (Y/N)
alt No
## 使用 Hook->>User: Exit
end
要使用该拦截器,你需要通过 `LD_PRELOAD` 环境变量在执行命令前加载编译好的动态链接库。 else Rule Type: ERROR
Hook->>User: Print Error
**示例:** Hook->>User: Exit
else Rule Type: LOG/DEFAULT (Match Found)
```bash Hook->>PTY: dupIO(filename, argv, envp)
export LD_PRELOAD=$(pwd)/execve_interceptor.so PTY->>PTY: forkpty()
nvidia-smi par Child Process
rm test.txt PTY->>Sys: orig_execve(...)
pip install torch and Parent Process
PTY->>PTY: handle_io() (Capture Output)
PTY->>User: Forward Output
end
else No Match
Hook->>Sys: orig_execve(...)
end
``` ```
## 前置依赖: ### 2. 配置管理 (`src/config.c`)
- jsonc - **共享内存**: 配置数据不直接从文件读取而是从共享内存Shared Memory中加载。
- **机制**: 这意味着有一个外部进程(通常是配套的 Go 服务)负责解析配置文件(如 JSON/YAML并将结构化数据写入共享内存C 代码只需高效读取即可。
```bash ### 3. 规则引擎 (`src/rules.c`)
# Ubuntu/Debian
sudo apt-get install libjson-c-dev
# CentOS/RHEL - **功能**: 负责解析和匹配命令执行规则。
sudo yum install json-c-devel - **匹配逻辑**: 检查命令名称(`filename`)以及参数列表(`argv`)是否符合预定义的条件。
### 4. I/O 捕获与 PTY (`src/pty_dup.c`)
- **目的**: 为了完整记录交互式命令的输入输出Session Recording
- **实现**:
- 使用 `forkpty` 创建一个新的伪终端会话。
- 在子进程中执行目标命令。
- 父进程负责在主终端和伪终端之间转发数据(`stdin`, `stdout`, `stderr`)。
- 单独处理 `stderr` 管道以区分标准输出和错误输出。
- 处理信号(如 `SIGINT`, `SIGCHLD`)以确保子进程正确退出。
```mermaid
flowchart TD
A[dupIO Called] --> B{forkpty}
B -- Error --> C[Exit]
B -- Child Process --> D[Setup Signals]
D --> E[Close Pipe Read End]
E --> F[Dup2 Stderr -> Pipe Write End]
F --> G[Return to execve_interceptor]
G --> H[Call orig_execve]
B -- Parent Process --> I[Close Pipe Write End]
I --> J[Get Log File Paths]
J --> K[handle_io Loop]
subgraph IO_Loop [IO Handling Loop]
L{Select/Poll}
L -- Stdin Data --> M[Write to Master PTY]
L -- Master PTY Data --> N[Read Output]
N --> O[Write to Stdout]
N --> P[Write to Log File]
L -- Stderr Pipe Data --> Q[Read Stderr]
Q --> R[Write to Stderr]
Q --> S[Write to Log File]
end
K --> IO_Loop
IO_Loop -- Child Exit --> T[Check Exit Status]
T -- Error Code --> U[Send Params to Socket]
T -- Success --> V[Exit Parent]
``` ```
## 编译: ### 5. Write 调用拦截 (`src/hook_write.c`)
- cd execve_hook && make 这是一个独立的模块(编译为 `hook_write.so`),用于更细粒度的输出捕获。
## 测试: - **拦截对象**: `write`, `writev`, `fwrite`, `puts`, `printf` 等标准输出函数。
- **功能**: 将进程写入 `stdout``stderr` 的内容同时写入到一个指定的日志文件中。
- ./test_bash.sh - **用途**: 即使不使用 PTY也能捕获程序的输出内容。
## 配置文件格式:
```json
[
{
"cmd": "nvidia-smi", // 将匹配的命令
"type": "warn", // 如果为warn则会提示是否继续执行
"msg": "在沐曦环境下请执行mx-smi" // 提示信息
},
{
"cmd": "rm", // 匹配的命令
"type": "error", // 如果为error则会拦截命令执行
"msg": "Error: rm command is forbidden" // 提示信息
},
{
"cmd": "pip", // 匹配的命令
"type": "warn",
"msg": "使用pip安装torch时请注意使用厂家支持版本",
"args": ["install", "torch"] //当存在args则这里的参数必须全部存在
}
]
```mermaid
flowchart LR
A[Application] -->|Calls write/printf| B(Hook Function)
B --> C{Log FD Open?}
C -- Yes --> D[Write to Log File]
C -- No --> E[Skip Log]
D --> F[Call Original Function]
E --> F
F --> G[Output to Terminal]
``` ```
### 6. 进程间通信与错误上报 (`src/client.c`)
- **方式**: Unix Domain Socket (`/etc/exec_hook/exec.sock`)。
- **作用**: 将拦截到的执行信息文件名、参数、环境变量、日志路径等发送给后端服务Go Service
- **流程**: 在 `execve` 拦截阶段,如果需要上报,会通过此模块连接 Socket 并发送数据。
#### 错误上报机制
当被拦截的命令执行失败(退出码非 0 或被信号终止)时,系统会触发错误上报流程。
1. **执行与捕获**: `pty_dup.c` 中的 `handle_io` 循环负责实时捕获子进程的 `stdout``stderr`
2. **日志记录**: 捕获到的 `stderr` 内容会被实时写入到特定的日志文件中(由 `GET_LOG_FILE` 宏定义路径)。
3. **触发上报**:
- 父进程等待子进程退出。
- 检查子进程退出状态 (`child_status`)。
- 如果 `WIFEXITED` 且退出码非 0或者 `WIFSIGNALED`(被信号终止),则调用 `send_exec_params`
4. **发送数据**: `send_exec_params` 将以下信息打包发送给 Go 服务:
- **Filename**: 执行的命令名称。
- **CWD**: 当前工作目录。
- **Argv**: 完整的参数列表。
- **Envp**: 完整的环境变量列表。
- **Log Path**: 记录了错误输出的日志文件绝对路径。
5. **等待响应**: 客户端等待服务端返回处理结果(如 AI 分析建议),并将其打印到终端。
```mermaid
sequenceDiagram
participant Parent as Parent Process
participant Child as Child Process (Cmd)
participant Log as Log File (Stderr)
participant Socket as Unix Socket
participant Server as Go Service
Parent->>Child: forkpty() & exec()
loop Execution
Child->>Parent: Stderr Output (Pipe)
Parent->>Log: Write Stderr Content
end
Child-->>Parent: Exit (Status != 0)
Parent->>Parent: Check Exit Code
alt Exit Code != 0
Parent->>Socket: Connect
Parent->>Socket: Send Filename
Parent->>Socket: Send CWD
Parent->>Socket: Send Argv & Envp
Parent->>Socket: Send Log Path (Error Log)
Socket->>Server: Forward Request
Server->>Server: Analyze Error (Read Log)
Server-->>Socket: Response (Suggestion)
Socket-->>Parent: Receive Response
Parent->>Parent: Print Suggestion
end
```
#### 通信协议格式
数据通过 Socket 顺序发送,格式如下(均为二进制流):
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `filename_len` | `size_t` | 文件名长度 |
| `filename` | `char[]` | 文件名内容 |
| `pwd_len` | `size_t` | 当前工作目录长度 |
| `pwd` | `char[]` | 当前工作目录内容 |
| `argc` | `int` | 参数个数 |
| `arg_len` | `size_t` | (循环) 参数长度 |
| `arg_str` | `char[]` | (循环) 参数内容 |
| `envc` | `int` | 环境变量个数 |
| `env_len` | `size_t` | (循环) 环境变量长度 |
| `env_str` | `char[]` | (循环) 环境变量内容 |
| `log_path_len` | `size_t` | 日志路径长度 |
| `log_path` | `char[]` | 日志路径内容 |
## 编译构建
项目使用 `Makefile` 进行管理。
- **默认构建**: `make` (生成 `intercept.so`)
- **开启 Hook 功能**: `make HOOK=1` (定义 `HOOK` 宏,启用拦截逻辑)
- **清理**: `make clean`
## 目录结构说明
- `src/`: 源代码目录
- `execve_interceptor.c`: `execve` 拦截主逻辑
- `hook_write.c`: `write` 系列函数拦截逻辑
- `config.c`: 共享内存配置加载
- `pty_dup.c`: 伪终端处理
- `client.c`: Socket 通信客户端
- `rules.c`: 规则匹配逻辑
- `build/`: 编译输出目录

View File

@ -124,6 +124,7 @@ int enhance_execve(const char *filename, char *const argv[],
#endif #endif
} }
#ifndef NO_CONFIG_CHECK
// Current configuration information // Current configuration information
DEBUG_LOG("Current Config rule count : %d", shared_config->rule_count); DEBUG_LOG("Current Config rule count : %d", shared_config->rule_count);
@ -216,6 +217,27 @@ int enhance_execve(const char *filename, char *const argv[],
#endif #endif
} }
#else
DEBUG_LOG("NO_CONFIG_CHECK defined, skipping config checks.");
#endif
// 只有当标准输入和标准输出都是终端时,才启用 PTY
// 1. 如果 stdin 不是终端(如管道输入),不需要 PTY
// 2. 如果 stdout 不是终端(如重定向到文件),使用 PTY 会导致输出包含颜色代码等控制字符,破坏文件内容
if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
DEBUG_LOG("Not a tty (stdin or stdout), skipping PTY setup.");
#ifdef HOOK
if (orig_execve) {
orig_execve(filename, argv, envp);
perror("orig_execve failed");
exit(1);
}
#endif
execvp(filename, argv);
perror("execvp failed");
exit(1);
}
write_log(filename, argv); write_log(filename, argv);
// Duplicate stdout and stderr to the log file // Duplicate stdout and stderr to the log file

View File

@ -30,7 +30,7 @@ __attribute__((destructor)) void cleanup_shared_memory() {
// shared_config = NULL; // shared_config = NULL;
// } // }
#ifdef DEBUG #ifdef DEBUG
// print_stacktrace(); print_stacktrace();
#endif #endif
// Note: We don't delete the shared memory segment here, as it might be // Note: We don't delete the shared memory segment here, as it might be
// used by other processes. A separate mechanism would be needed to manage // used by other processes. A separate mechanism would be needed to manage

View File

@ -11,6 +11,17 @@
FILE *log_file = NULL; FILE *log_file = NULL;
pid_t child_pid; pid_t child_pid;
int child_status = -1; int child_status = -1;
static int pty_master_fd = -1;
void handle_sigwinch(int sig) {
(void)sig;
struct winsize win;
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &win) != -1) {
if (pty_master_fd != -1) {
ioctl(pty_master_fd, TIOCSWINSZ, &win);
}
}
}
// void dupIO() { // void dupIO() {
// pid_t pid; // pid_t pid;
@ -73,6 +84,7 @@ void dupIO(const char *filename, char *const argv[], char *const envp[]) {
signal(SIGINT, handle_sigint); signal(SIGINT, handle_sigint);
signal(SIGCHLD, handle_sigchld); signal(SIGCHLD, handle_sigchld);
signal(SIGWINCH, handle_sigwinch);
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &win) < 0) { if (ioctl(STDIN_FILENO, TIOCGWINSZ, &win) < 0) {
perror("ioctl TIOCGWINSZ failed"); perror("ioctl TIOCGWINSZ failed");
@ -83,6 +95,10 @@ void dupIO(const char *filename, char *const argv[], char *const envp[]) {
DEBUG_LOG("forkpty result is: %d.", pid); DEBUG_LOG("forkpty result is: %d.", pid);
child_pid = pid; child_pid = pid;
if (pid > 0) {
pty_master_fd = master;
}
if (pid < 0) { if (pid < 0) {
perror("forkpty failed"); perror("forkpty failed");
exit(1); exit(1);

View File

@ -17,27 +17,31 @@ void setup_termios(struct termios *term) {
// 控制模式标志 // 控制模式标志
term->c_cflag = CS8 | CREAD; // 8位字符,启用接收器 term->c_cflag = CS8 | CREAD; // 8位字符,启用接收器
// 本地模式标志 - 禁用特殊输入处理 // 本地模式标志 - 启用规范模式、信号处理和回显
term->c_lflag = ISIG | IEXTEN; // 仅保留信号处理和扩展功能 term->c_lflag = ICANON | ECHO | ISIG | IEXTEN; // 启用规范模式、回显、信号处理和扩展功能
// 控制字符 // 控制字符
term->c_cc[VINTR] = 0x03; // Ctrl-C term->c_cc[VINTR] = 0x03; // Ctrl-C (中断)
term->c_cc[VQUIT] = 0x1c; // Ctrl-反斜杠 term->c_cc[VQUIT] = 0x1c; // Ctrl-\ (退出)
term->c_cc[VERASE] = 0x7f; // Backspace term->c_cc[VERASE] = 0x7f; // Backspace (擦除)
term->c_cc[VKILL] = 0x15; // Ctrl-U term->c_cc[VKILL] = 0x15; // Ctrl-U (清除行)
term->c_cc[VEOF] = 0x04; // Ctrl-D term->c_cc[VEOF] = 0x04; // Ctrl-D (文件结束)
term->c_cc[VTIME] = 0; term->c_cc[VEOL] = 0; // 行结束字符
term->c_cc[VMIN] = 1; term->c_cc[VEOL2] = 0; // 行结束字符2
term->c_cc[VSWTC] = 0; term->c_cc[VSTART] = 0x11; // Ctrl-Q (开始)
term->c_cc[VSTART] = 0x11; // Ctrl-Q term->c_cc[VSTOP] = 0x13; // Ctrl-S (停止)
term->c_cc[VSTOP] = 0x13; // Ctrl-S term->c_cc[VSUSP] = 0x1a; // Ctrl-Z (暂停)
term->c_cc[VSUSP] = 0x1a; // Ctrl-Z // term->c_cc[VDSUSP] = 0x19; // Ctrl-Y (延迟暂停)
term->c_cc[VEOL] = 0; term->c_cc[VREPRINT] = 0x12; // Ctrl-R (重打印)
term->c_cc[VREPRINT] = 0x12; // Ctrl-R term->c_cc[VDISCARD] = 0x0f; // Ctrl-O (丢弃)
term->c_cc[VDISCARD] = 0x0f; // Ctrl-O term->c_cc[VWERASE] = 0x17; // Ctrl-W (擦除词)
term->c_cc[VWERASE] = 0x17; // Ctrl-W term->c_cc[VLNEXT] = 0x16; // Ctrl-V (字面值下一个)
term->c_cc[VLNEXT] = 0x16; // Ctrl-V term->c_cc[VMIN] = 1; // 规范模式:最少读取1个字符
term->c_cc[VEOL2] = 0; term->c_cc[VTIME] = 0; // 规范模式:无超时
#ifdef VSWTC
term->c_cc[VSWTC] = 0; // 切换字符(通常未使用)
#endif
// 设置输入输出速度 // 设置输入输出速度
cfsetispeed(term, B38400); cfsetispeed(term, B38400);
@ -83,7 +87,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log,
} }
while (1) { while (1) {
DEBUG_LOG("poll....."); // DEBUG_LOG("poll.....");
int ret = poll(fds, 3, 100); // 修改为监控3个fd int ret = poll(fds, 3, 100); // 修改为监控3个fd
if (ret < 0) { if (ret < 0) {
if (errno == EINTR) continue; if (errno == EINTR) continue;
@ -92,7 +96,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log,
} }
// 优先处理 PTY 输出,确保缓冲区中的数据被完全读出 // 优先处理 PTY 输出,确保缓冲区中的数据被完全读出
DEBUG_LOG("Handling pty output..."); // DEBUG_LOG("Handling pty output...");
if (fds[1].revents & (POLLIN | POLLHUP)) { if (fds[1].revents & (POLLIN | POLLHUP)) {
ssize_t n = read(master_fd, buffer, sizeof(buffer)); ssize_t n = read(master_fd, buffer, sizeof(buffer));
if (n > 0) { if (n > 0) {
@ -104,7 +108,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log,
// 处理stderr // 处理stderr
// 处理stderr // 处理stderr
DEBUG_LOG("Handling stderr output..."); // DEBUG_LOG("Handling stderr output...");
if (fds[2].revents & (POLLIN | POLLHUP)) { if (fds[2].revents & (POLLIN | POLLHUP)) {
ssize_t n = read(stderr_fd, buffer, sizeof(buffer)); ssize_t n = read(stderr_fd, buffer, sizeof(buffer));
if (n > 0) { if (n > 0) {
@ -123,9 +127,9 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log,
} }
// 输出带颜色的错误信息 // 输出带颜色的错误信息
write(STDERR_FILENO, "\033[31m", 5); // 设置红色 // write(STDERR_FILENO, "\033[31m", 5); // 设置红色
write(STDERR_FILENO, converted, conv_len); write(STDERR_FILENO, converted, conv_len);
write(STDERR_FILENO, "\033[0m", 4); // 重置颜色 // write(STDERR_FILENO, "\033[0m", 4); // 重置颜色
// 写入日志 // 写入日志
write(stderr_log_fd, converted, conv_len); write(stderr_log_fd, converted, conv_len);
@ -133,12 +137,12 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log,
} }
// 检查子进程状态 // 检查子进程状态
DEBUG_LOG("Checking child status: %d", child_status); // DEBUG_LOG("Checking child status: %d", child_status);
if (child_status != -1) { if (child_status != -1) {
// 再次尝试读取可能残留的输出 // 再次尝试读取可能残留的输出
while (1) { while (1) {
ssize_t n = read(master_fd, buffer, sizeof(buffer)); ssize_t n = read(master_fd, buffer, sizeof(buffer));
DEBUG_LOG("Read n is: %ld", n); // DEBUG_LOG("Read n is: %ld", n);
if (n < 0) { if (n < 0) {
// 读取错误处理 // 读取错误处理
break; break;
@ -164,7 +168,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log,
} }
} }
DEBUG_LOG("fflush."); // DEBUG_LOG("fflush.");
// 确保所有输出都已刷新 // 确保所有输出都已刷新
fflush(stdout); fflush(stdout);
fflush(stderr); fflush(stderr);
@ -173,7 +177,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log,
} }
// 处理标准输入 // 处理标准输入
DEBUG_LOG("Handling stdin"); // DEBUG_LOG("Handling stdin");
if (fds[0].revents & POLLIN) { if (fds[0].revents & POLLIN) {
ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer)); ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
if (n <= 0) break; if (n <= 0) break;