Interactive与Non-interactive Shell
interactive 与 non-interactive shell
我们日常用到最多的,是交互式 shell。没有输入任何命令之前,命令行会等待输入命令。输入了命令,在命令执行过程也可以进行输入,如 sudo apt install vim
,会交互式的让你输入密码、安装前会让你输入 Y
进行确认。
而所谓无交互式 “shell”,就是没有办法进行交互式输入的程序。要特别对 “shell” 强调 “无交互” ,只不过提一到 shell,我们预想到的是终端及其交互式的界面,预期的都是有交互的。很多程序都是以无交互的形式在运行的,各类 server、cronjob、甚至连交互式shell中跟在管道后面的命令都是以无交互式的形式运行的。
在 shell 中,我们可以通过 tty
命令来查询当前 shell 所使用的虚拟 tty,如果当前有所使用的 tty,那基本就说明当前是以交互式的方式运行的 shell。其实这很像一句废话,都能输入 tty
这三个字符了,当然是交互式的。
$ tty
/dev/ttys011
# 运行在管道里命令,那就没有 tty了,也就不是交互式 shell。 因为它是没有继承父进程(交互式 shell)的 stdio 的子进程
$ echo "hell" | tty
not a tty
# 还可以通过 echo $- 来查看是不是 interactive,输出带有 `i` 就表示 interactive
$ echo $-
himBHs
如果你在实现一个交互式命令,其调用子命令(指子进程)的时候你又希望其子命令也能与用户有交互,那么你就需要让这个子进程继承父进程的 stdio。比如,nodejs 的 child_process 就有 stdio: 'inherit'
这样的选项。上面的示例 go 程序可以这样改:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("vim", ".git/COMMIT_EDITMSG")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Printf("%v\n", err.Error())
os.Exit(-1)
}
}
# 执行这个程序,它就会打开一个 vim 。像不像 git commit 提交的过程?
go run main.go
假如,注释掉 cmd.Stdin = os.Stdin
,那么在 go 语言里 cmd 的 stdio 就是 /dev/null
,运行之后程序会非正常退出, exit code 是 1
。我之前试过 nodejs 的脚本,子进程等待用户输入,但是却没有继承交互式父进程的 stdio,那么程序就会卡在那里。
所以,自己写脚本、写命令行工具的时候,如果遇到类似的“卡死”问题或者报错应该知道如何处理。总结起来就是不要让需要 interactive 的程序运行在 non-interactive 的环境下。
- 那些 service,cronjob 是万万不能依赖 interactive 的,它们运行在 non-interactive 环境下,依赖用户输入会导致程序“卡死”;
- 那些希望与用户进行交互的子进程,要处理好 stdio 继承,免得让它们运行在一个 non-interactive 环境下;
另外 interactive shell (或者说 tty,这里不细分)本身也意味着是一种“常驻进程”,因为它“时刻”在等待着用户进行输入操作。所以一个 docker container 想要一直存活,要嘛以 -d
即守护进程的形式运行,要嘛以 -t
即 tty: true
的形式运行。之前部署 jenkins in k8s 的时候,发现 runner 容器一启动就退出,导致构建中止。我是通过让 runner 容器的启动命令变成 sleep 99d
来保持其常驻的。 而今天才明白,XX的 runner container 是通过 tty: true
来保活的。