直接用os/exec.Command启动shell会卡住,因未配置stdin/stdout/stderr流式转发且缺少pty,导致交互式shell无法正常运行;需用creack/pty创建pty并实时转发IO,WebSocket断开时须主动发SIGTERM并Wait超时清理,resize需通过IoctlSetWinsize同步。

为什么直接用 os/exec.Command 启动 shell 会卡住?
因为没配好 stdin/stdout/stderr 的流式转发,cmd.Run() 会阻塞等待命令结束,而交互式 shell(如 /bin/bash)根本不会“结束”——它一直等着用户输入。更关键的是,没分配伪终端(pty),shell 缺少 isatty 环境,会禁用行编辑、颜色输出、信号处理等基本能力。
- 必须用
golang.org/x/sys/unix调用unix.IoctlSetInt和unix.Setsid创建新会话,并用pty.Start(推荐github.com/creack/pty)启动带 pty 的进程 - 别用
cmd.Output()或cmd.CombinedOutput(),它们读完才返回;改用cmd.StdoutPipe()+io.Copy实时转发 - 务必设置
syscall.Setpgid(0, 0)防止 Ctrl+C 杀掉整个 Go 进程而非子 shell
WebSocket 连接断开时,怎么安全清理 pty 进程?
WebSocket 关闭不等于 pty 自动退出——子进程可能还在后台跑着,造成句柄泄漏和僵尸进程。不能只靠 defer 或 context cancel,得主动发信号并等待。
- 用
pty.Process.Signal(syscall.SIGWINCH)没用,要发syscall.SIGTERM或syscall.SIGKILL - 调用
pty.Process.Wait()必须加超时,否则可能永久阻塞(比如子进程忽略信号);建议用time.AfterFunc强制 kill - 在 WebSocket
CloseHandler里触发清理,而不是依赖defer—— defer 在 handler 返回后才执行,此时连接可能已断,但 goroutine 还活着
github.com/creack/pty 和 golang.org/x/term 有什么区别?该选哪个?
golang.org/x/term 只提供终端能力封装(如读密码、设置光标),不负责创建 pty;github.com/creack/pty 才是真正帮你 fork+exec+打开主从 pty 设备的库。后者是必须的,前者可选。
- macOS / Linux 下必须用
creack/pty,Windows 不支持原生 pty,得换方案(如 conpty) -
creack/pty的pty.Start()返回的*os.Process是子 shell 进程,不是 pty 文件描述符本身;读写要走pty.Slave的Read/Write - 别手动调用
unix.Open("/dev/pts/X")—— 权限、生命周期、自动挂载都难控制,pty.Start已处理好这些
如何把终端尺寸变化(resize)从浏览器传到 pty?
前端发来的 resize 消息必须转成 unix.IoctlWinsize 写入 pty 主设备,否则 stty size 还是旧值,vim、ls --color 等都会错乱。
立即学习“go语言免费学习笔记(深入)”;
- 前端需监听
window.onresize,通过 WebSocket 发送类似{"type":"resize","cols":120,"rows":40}的消息 - 服务端收到后,构造
unix.Winsize{Rows: uint16(rows), Cols: uint16(cols)},调用unix.IoctlSetWinsize(pty.Fd(), unix.TIOCSWINSZ, uintptr(unsafe.Pointer(&ws))) - 注意:
pty.Fd()是主设备 fd,不是pty.Slave的;且必须在子进程已启动、pty 已绑定后调用
ps aux | grep bash 都能看到残留。










