nohup 为什么喂不进输入:用 pty 驱动交互式 CLI 的正确姿势
在服务器上跑一个需要交互的命令行工具,最容易踩的坑不是参数写错,而是它根本不读你喂给它的输入。
现象通常是这样的:
nohup ./deploy-cli --env prod > out.log 2>&1 < input.txt &
input.txt 里明明写好了 yes、路径、确认口令,out.log 里却停在:
请输入目标环境名称:
然后进程一直挂在那儿,top 里 CPU 占用 0,你以为是网络问题,其实是它从来没收到过一行输入。
一、nohup 不是"让它能后台交互"
nohup 只做两件事:忽略 SIGHUP、把 stdout/stderr 重定向(不指定时写到 nohup.out)。它不提供终端。
后台进程的 stdin 有三种常见落法,哪个都不像终端:
- 显式
< input.txt:是个普通文件,读到末尾就是 EOF; < /dev/null:立刻 EOF;- 什么都不写:在某些环境下仍连着启动它的终端,一旦终端关闭就变成不可读。
而大多数交互式 CLI 在真正读输入之前,会先做一次 isatty(STDIN_FILENO) 判断:不是 tty 就走"非交互分支",有的直接报错退出,有的(更糟)走一条没人喂数据的读循环,于是卡死。
二、为什么 echo | cli 也不行
管道给的是一个合法的 stdin,但它仍然不是 tty。两个连带问题:
- 库函数在做终端控制时会失败。比如
getpass()需要关闭回显,它调用tcgetattr()拿不到终端属性,有的实现直接异常退出。 - 行缓冲与回显语义不同。真终端上你按下回车才发送,行编辑(方向键、退格)由终端驱动处理;管道里没有这些,依赖 readline 的程序行为会变得不可预期。
所以"用管道把答案灌进去"只在程序明确支持 stdin 批处理时才可靠,比如 apt-get -y、ssh -o BatchMode=yes。它属于程序提供的非交互模式,不是 pty 的替代品。
三、正确做法:给它一个伪终端
pty(pseudo-terminal)能让子进程以为自己接在真终端上:isatty 返回真、termios 可用、回显与行编辑按终端语义工作。
Python 里最直接:
import os, pty, select, time
pid, fd = pty.fork()
if pid == 0: # 子进程
os.execvp("./deploy-cli", ["./deploy-cli", "--env", "prod"])
else: # 父进程
buf = b""
def wait_for(token, timeout=30):
nonlocal buf
end = time.time() + timeout
while time.time() < end:
r, _, _ = select.select([fd], [], [], 0.5)
if fd in r:
try:
chunk = os.read(fd, 4096)
except OSError: # 子进程退出,从端关闭 -> EIO
return False
buf += chunk
if token.encode() in buf:
return True
return False
if wait_for("环境名称"):
os.write(fd, b"prod\r") # 注意是 \r,不是 \n
if wait_for("确认"):
os.write(fd, b"yes\r")
# 收尾:读干净再 waitpid,避免子进程因管道写满阻塞
os.waitpid(pid, 0)
需要正则匹配、超时、日志留档时,直接用 pexpect(或 shell 的 expect),它把这套封装好了。
四、四个真实的坑
1. 换行要用 \r 而不是 \n。 终端默认开着 ONLCR,你在键盘上按的回车,送到程序手里是 CR。往主端写 \n 时,某些程序(尤其是自绘 TUI)不会当成"确认"。
2. 不设窗口大小,全屏程序会变形甚至崩溃。 用 ioctl(fd, TIOCSWINSZ, ...) 给一个 80×24 之类的默认值。
3. 读到 EIO 是正常的。 子进程退出后从端关闭,主端 read() 抛 OSError(EIO)。把它当成"结束"而不是"出错"。
4. 别用 pty 跑会自己 daemonize 的程序。 双 fork 之后你的 pty 主端再没人读,写满缓冲区反而会把进程卡住。
五、什么时候该换思路
pty 是兜底手段,不是长期方案。如果这个 CLI 你会反复调用,正确做法是给它加一个 --non-interactive / --yes 模式,或者用它的 API、配置文件、环境变量替代问答。pty 只适合三种场景:一次性登录、厂商工具没有非交互模式、以及临时排查。
判断标准很简单:需要人读提示再决定的地方,用 pty + expect;不需要人判断的地方,就不该有提示。
一台没有屏幕的服务器上,"交互"这个词本身就是个信号:要么给它一个假终端,要么把交互去掉。
- 点赞
- 收藏
- 关注作者
评论(0)