Eshell 系统调用 ioctl TIOCGWINSZ 返回值不对

最近用 eshell 碰到了个问题,在系统 terminal 里调用 ioctl(stderr, TIOCGWINSZ, winds) 能拿到窗口的尺寸,但是在 Eshell 里就一直返回 0, 0

不知道是 Eshell 对 system call 支持上不足呢? 还是 TIOCGWINSZ 的值我传错了呢?

不了解 “Terminal” 是怎么工作的,虽然我并不意外你提到的不能工作,窗口的尺寸由应用本身控制,比如 gnome-terminal、Terminal.app 或者 Emacs.app,Emacs 并不是 Terminal 模拟器,所以我说我不觉得意外。

Eshell 跟 Emacs 本身没多少差别,跟你一次次用 M-! 意思差不多,所以你提到的在 Eshell 中无效也就是在 Emacs 中无效。

在 Emacs 中要获得窗口大小用 window-widthwindow-height,Eshell 还把这两个函数绑定到了 COLUMNSLINES 上。

顺便一提,M-x ansi-term 中是可以正常工作的,因为 ansi-term 是一个真正的 Terminal 模拟器。


测试用代码(Python 3):

# https://stackoverflow.com/questions/16237137/what-is-termios-tiocgwinsz
import sys, struct, fcntl, termios

s = struct.pack('HHHH', 0, 0, 0, 0)
t = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s)
print(struct.unpack('HHHH', t))
1 个赞

我理解的 ioctl 应该是一个系统调用,能返回 io 设备的一些信息

在 Eshell 中,fd 1 用 libc 的 isatty 返回的也是 true

但是我想不明白为什么 t = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s) 拿到的却都是 0。

在 term 里能够拿到我也能理解,我主要不理解的是为什么在 Eshell,也就是 Emacs 里拿不到,按说都是系统调用。

感觉还是哪里我理解的有问题,想不通 :face_with_raised_eyebrow:

「尺寸」在终端窗口里其实是行x列数吧,并非像素高度。

在 eshell 里可以通过 tput 命令得到「尺寸」:

~ $ /usr/bin/tput cols
181
~ $ /usr/bin/tput lines
49

相关主题:

1 个赞

或许这个系统调用总得请求应用本身(或者窗口管理器)才能知道窗口的尺寸吧,Kernel 自己不会知道,不是所有应用都像 terminal emulator 那样支持这样的请求。(纯属猜测)

我这也是,但 M-! python3 -c 'import os; print(os.isatty(1))' 返回 False,不知为何。

tput 应该直接读取了 Eshell 设置的 COLUMNSLINES

start-process / 异步情况下 isatty 返回 True,call-process / 同步情况下返回 False:

(call-process "python3" nil t nil "-c" "import os; print(os.isatty(1))")
False
0

(start-process "python3" (current-buffer)
               "python3" "-c" "import os; print(os.isatty(1))")
#<process python3>

True

Process python3 finished

而当 process-connection-type 设置为 nil / Pipe 时(默认值为 t / pty),start-process 返回值变成了 False

(let ((process-connection-type nil))
  (start-process "python3" (current-buffer)
                 "python3" "-c" "import os; print(os.isatty(1))"))
#<process python3>

False

Process python3 finished

不是 Emacs 拿不到,我的理解是因为 Emacs 没有按照 ioctl 的方式提供信息。


另外,这个问题似乎跟环境变量一样,存在这继承的现象。

首先我准备了一个 c 写的命令:

#include <sys/ioctl.h>
#include <stdio.h>
int main(int argc, char **argv)
{
  struct winsize sz;
  ioctl(0, TIOCGWINSZ, &sz);
  printf("Screen width: %i, Screen height: %i\n", sz.ws_col, sz.ws_row);
  return 0;
}

在终端里:

⋊> [~/D/s/c/winsize] ./a.out
Screen width: 181, Screen height: 24

⋊> [~/D/s/c/winsize] sh -c './a.out'
Screen width: 181, Screen height: 24

第二条命令是在一个子进程中执行,执行完立即退出。如果把它想象成一个有形的窗体,那么这个窗体还没创建就销毁了,所以它返回的窗口信息应该是来自于父进程。

Script Editor.app 中试验:

do shell script "/usr/local/bin/python3 -c 'import os; print(os.isatty(1))'"
-- => False
do shell script "~/.scratch/c/winsize/a.out"
-- => "Screen width: 0, Screen height: 0"

结果跟 Eshell 和 M-! 是一样的,其创建的 shell 子进程还没来得及生成窗口信息就退出了,又无法从父进程中继承(类似 Emacs 与 Eshell 的情形),或许这就是为什么 shell-command 文档里要注明 inferior shell 的原因了。


1 个赞

看了下 ansi-term 中之所以能工作,因为在 term-exec-1 中使用下面的方式启动 subprocess:

$ /bin/sh -c 'stty -nl echo rows 40 columns 80 sane 2&gt;/dev/null;if [ $1 = .. ]; then shift; fi; exec "$@"' .. python3 play.py

模仿 term-exec-1

(defun start-process-TIOCGWINSZ (command &rest switches)
  "Like `start-process' but respect ioctl with TIOCGWINSZ.

Adapted from `term-exec-1'."
  (apply 'start-process "term-exec" (current-buffer)
         "/bin/sh" "-c"
         (format "stty -nl echo rows %d columns %d sane 2>/dev/null;\
if [ $1 = .. ]; then shift; fi; exec \"$@\""
	         (window-width) (window-height))
         ".."
         command switches))

这样一来就能获得 Emacs 窗口的大小了:

(start-process-TIOCGWINSZ "python3" "play.py")

#<process term-exec>

(88, 49, 0, 0)

Process term-exec finished

用同样的方法修改 eshell-gather-process-output 估计也能使得 Eshell 支持 ioctl TIOCGWINSZ。

1 个赞