Docker 的 tty size 问题
问题
由于把 Postgres 运行在 Docker 容器里,所以经常使用如下命令登录到数据库 docker exec -it -u postgres mypg psql mydb
。然后在 psql 提示符下执行操作。然后经常会出现输入太长的 sql 语句时,psql 会在还没遇到终端宽度限制之前就开始换行,而且是把把输入回显换行到上面一行。这个行为说明了在容器内的 tty 的配置出现了问题。但是,很奇怪的是,退出 docker exec
之后,再次进入,就不会遇到这个问题了。
现象分析
这个问题看起来应该出在是 Docker 容器对于 tty size 的处理上。当出现这个问题的时候,通过 docker exec
进入容器的 shell,执行 stty size
会得到如下结果:
$ docker exec -it -u postgres postgres bash
bash-4.3$ stty size
stty: standard input
bash-4.3$ stty size 2> /dev/null
bash-4.3$ stty size > /dev/null
stty: standard input
bash-4.3$ stty -a
speed 38400 baud;stty: standard input
line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R;
werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
echoctl echoke
bash-4.3$ echo $LINES
24
bash-4.3$ echo $COLUMNS
80
bash-4.3$ stty rows 40 cols 100
bash-4.3$ echo $?
0
bash-4.3$ stty size
40 100
从上面的命令可以看出,stty
获取当前终端的窗口大小失败了,所以 psql
程序的回显就错乱了。不过,这里要注意的是,stty size
命令没有返回失败(返回值为0)。但是,通过 stty rows 40 cols 100
命令设置 tty 的参数确实可以成功的,说明问题可能是因为 tty 参数没有被正常初始化造成的。另外需要说明的是,这个问题不是必现的。
这个问题在 Docker 项目的 issue 里被提到过,地址是 https://github.com/moby/moby/issues/33794。根据该 issue 的记录,目前该问题还没有找到原因。
关于这个问题,我不确定是 kernel 引起的,还是 shell 程序引起的,因为 termio
结构体会从内核拷贝到用户空间。
stty size
的返回值为什么是0?
我想知道为什么stty
命令没有返回失败,所以去看了一下代码。我使用的是 Postgres Alpine 的镜像,安装的 shell 环境是 BusyBox,通过查看 BusyBox 的代码可以找到这里:https://github.com/mirror/busybox/blob/master/coreutils/stty.c#L899,对应的代码是:
static void display_window_size(int fancy)
{
const char *fmt_str = "%s\0%s: no size information for this device";
unsigned width, height;
if (get_terminal_width_height(STDIN_FILENO, &width, &height)) {
if ((errno != EINVAL) || ((fmt_str += 2), !fancy)) {
perror_on_device(fmt_str);
}
} else {
wrapf(fancy ? "rows %u; columns %u;" : "%u %u\n",
height, width);
}
}
这个函数获取当前窗口的大小,stty size
命令会直接调用这个函数。这个函数继续调用了下面这个函数 https://github.com/mirror/busybox/blob/master/libbb/xfuncs.c#L263:
/* It is perfectly ok to pass in a NULL for either width or for
* height, in which case that value will not be set. */
int FAST_FUNC get_terminal_width_height(int fd, unsigned *width, unsigned *height)
{
struct winsize win;
int err;
int close_me = -1;
if (fd == -1) {
if (isatty(STDOUT_FILENO))
fd = STDOUT_FILENO;
else
if (isatty(STDERR_FILENO))
fd = STDERR_FILENO;
else
if (isatty(STDIN_FILENO))
fd = STDIN_FILENO;
else
close_me = fd = open("/dev/tty", O_RDONLY);
}
win.ws_row = 0;
win.ws_col = 0;
/* I've seen ioctl returning 0, but row/col is (still?) 0.
* We treat that as an error too. */
err = ioctl(fd, TIOCGWINSZ, &win) != 0 || win.ws_row == 0;
if (height)
*height = wh_helper(win.ws_row, 24, "LINES", &err);
if (width)
*width = wh_helper(win.ws_col, 80, "COLUMNS", &err);
if (close_me >= 0)
close(close_me);
return err;
}
这里的关键点就在于 get_terminal_width_height
函数的内部注释,即 ioctl
可能返回0,但是却没有获取到窗口大小。这种情况下,该函数返回 win.ws_row == 0
,即 1
。当 get_terminal_width_height
函数返回 1
时,调用者 display_window_size
的处理逻辑如下:因为返回值不等于 EINVAL
,所以直接调用 perror_on_device
,这个函数会调用 libbb/perror_msg.c 中的 bb_perror_msg
函数,输出一个错误信息,然后就返回了(并不会导致程序错误退出)。随后,display_window_size
函数返回,stty size
命令正常返回。
这个看起来是个 BusyBox 的 bug,所以有点怀疑是否是 Docker 和 BusyBox 的配合有问题,导致了bug。
解决办法
虽然没有找到原因,但是解决办法还是有的,就是在进入 shell 前显示的设定终端窗口的大小,通过指定 COLUMNS
和 LINES
两个环境变量的方式,命令如下:
$ docker exec -it -u postgres -e COLUMNS=$(tput cols) -e LINES=$(tput lines) postgres bash
bash-4.3$ stty size
47 272
