popen是Workerman实现WebSSH最轻量的进程启动方式,因其单resource特性可直接接入event loop的onReadable监听,而proc_open的三通道资源无法被libevent/select混合监听;虽不处理ANSI语义,但通过script伪终端、非阻塞fwrite、stream_get_contents无缓冲读取及前端xterm.js协同可补全功能。

popen是 Workerman 实现 WebSSH 最轻量、最直接的进程启动方式,但它不是“开箱即用”的终端方案——它只负责执行命令并吐出原始输出流,不处理 ANSI 转义、回车换行、光标定位、键盘事件映射等终端语义。真正能跑通 WebSSH 的关键,在于前后端协同补全这些缺失环节。
为什么不用 proc_open 而选 popen?
Workerman 的 event loop(如 libevent 或 select)本身不支持 proc_open 返回的三通道资源(stdin/stdout/stderr)的混合监听;而 popen 只暴露单个 resource(通常是 stdout),配合 onReadable 可直接接入全局事件循环。虽然牺牲了 stdin 写入控制权,但可通过以下方式绕过:
-
popen启动的是带伪终端的 shell,例如script -qec "exec bash -i" /dev/null,让子进程自己接管输入输出流 - 前端 xterm.js 发送的每个按键(包括
Ctrl+C、Tab、↑)都拼成完整字符串,通过 WebSocket 传给后端,后端用fwrite写入该resource(需提前用stream_set_blocking($pipe, false)) - 必须确保 shell 进程未退出:
bash -i在无输入时会静默退出,建议加timeout 3600包裹或用while true; do bash -i; done容错
onReadable 回调里读取输出容易丢字节
常见现象是中文乱码、命令提示符错位、长命令截断——根本原因是 fread($pipe, 8192) 每次读取不保证边界对齐,尤其遇到 UTF-8 多字节字符或 ANSI 序列(如 \x1b[2K 清行)被硬切开。
- 务必设置
stream_set_read_buffer($pipe, 0)关闭系统缓冲,避免内核缓存干扰 - 改用
stream_get_contents($pipe, null, 0)一次性捞尽当前可读内容(注意:PHP 8.1+ 才支持第三个参数为 0 表示“直到 EOF 或阻塞”) - 前端接收后不能直接
term.write(),要先做简单 ANSI 过滤或委托xterm-addon-serialize处理,否则\x1b[?25l(隐藏光标)这类控制码会被当成普通文本显示出来
进程池管理不当会导致 popen 资源泄漏
错误写法是每次请求都 popen 新进程却不 fclose,或在 onClose 里忘记释放。Workerman 不会自动回收 PHP 子进程句柄,ps aux | grep bash 能看到大量 defunct 进程堆积。
-
ProcessPool::add()中必须用is_resource($process)校验返回值,popen失败时返回false,直接 push 会导致后续array_search失效 -
onReadable回调中检测到feof($pipe) || $output === false后,除了fclose($pipe),还要调用$processPool->releaseProcess($pipe),否则processCount()永远不准 - 别依赖
pcntl_signal捕获 SIGCHLD:Workerman 默认禁用信号处理,且多进程模型下子进程退出信号不可靠;应以feof为准
WebSSH 必须面对的现实约束
真实部署时,popen + Websocket 方案无法绕过 Linux 权限与容器隔离限制:
- Web 用户(如 www-data)默认无权启动交互式终端,需在
/etc/passwd中确认对应用户 shell 字段不是/usr/sbin/nologin - Docker 环境下必须挂载
/dev/pts并启用--privileged或至少--cap-add=SYS_ADMIN,否则script命令报Failed to allocate pty - 超时机制不能只靠前端心跳:后端需为每个连接维护最后活动时间戳,定时扫描并
fclose长期无读写的 pipe,否则僵尸进程只增不减
真正卡住多数人的不是代码逻辑,而是 script -qec "exec bash -i" /dev/null 这条命令在不同发行版上行为不一致——Ubuntu 22.04 可能需要换成 unshare -r script -qec "exec bash -i" /dev/null 才能获取可用 pts。这点不试一遍环境,文档里永远找不到答案。











