Emacs TRAMP遭遇zsh触发卡死之解
1. 起因
Emacs在过去几年一直是我的日常编辑器,–尽管更多时间,我是在使用IDE(如Android Studio、Visual Studio Code等)与终端(如iTerm2等)。虽然也知道Emacs能够打开并编辑远程计算机上的文件,但还是更多地在ssh登录到远程计算机后运行Emacs,特别是在我尝试了几次利用TRAMP1打开远程文件之后。
使得我对TRAMP敬而远之的主要障碍就是,每次打开局域网下服务器的文件时,Emacs就会卡死!此时,我只好使用 kill
命令来杀死emacs进程。但由于我是采用了 emacs -daemon
来启动后台进程, emacsclient -t
来打开前端进程,一旦杀死emacs进程,就会使得全部的emacsclient退出。而此时,我可能已经打开了大量文件,如果要重新一个一个恢复,将会是一件麻烦的事情。
2. 准备
在我的问题上下文里,ssh并没有采用自建ssh-agent,而是需要和gpg-agent协同工作。梳理整个操作流程,共涉及到三个参与者:
- ssh,负责远程连接、传输等;
- gpg-agent,负责在用户认证环节提供公私钥证书等;
- TRAMP,负责与Emacs适配等。
首先,通过在终端直接执行ssh登录,结果正常,说明ssh与gpg-agent可以正确配合工作。因此,可以大致排除二者的嫌疑,余下的”疑凶”也就是TRAMP了。只有获得卡死的具体原因,才能对症下药,因此就需要补充线索,也就是TRAMP的日志了。从其用户手册上可以找到1如何打开和提高日志的配置项的说明:
TRAMP messages are raised with verbosity levels ranging from 0 to 10.
TRAMP does not display all messages; only those with a verbosity level less than or equal to tramp-verbose.
足足有11个级别,–这远远大于通常的4-5个:
0 Silent (no TRAMP messages at all) 1 Errors 2 Warnings 3 Connection to remote hosts (default verbosity) 4 Activities 5 Internal 6 Sent and received strings 7 Connection properties 8 File caching 9 Test commands 10 Traces (huge) 11 Call traces (maintainer only)
于是,就在Emacs的启动脚本(以我的情况为例,位于 ~/.emacs.d/init.el
)文件,设置日志:(setq tramp-verbose 10)
3. 分析
再次尝试打开远程文件,顺利捕获到报错信息!其中,buffer *Backtrace*
的部分信息如下:
Debugger entered–Lisp error: (file-error "Timeout reached, see buffer ‘*tramp/ssh harvey@192…") signal(file-error ("Timeout reached, see buffer ‘*tramp/ssh harvey@192…")) tramp-error(nil file-error "Timeout reached, see buffer ‘*tramp/ssh harvey@192…") tramp-signal-hook-function(file-error ("Timeout reached, see buffer ‘*tramp/ssh harvey@192…")) signal(file-error ("Timeout reached, see buffer ‘*tramp/ssh harvey@192…")) tramp-maybe-open-connection((tramp-file-name "ssh" "harvey" nil "192.168.31.175" nil "~/Downloads/zshrc.backup.20230905" nil)) tramp-send-command((tramp-file-name "ssh" "harvey" nil "192.168.31.175" nil "~/Downloads/zshrc.backup.20230905" nil) "echo ~ 2>/dev/null; echo tramp_exit_status $?") tramp-send-command-and-check((tramp-file-name "ssh" "harvey" nil "192.168.31.175" nil "~/Downloads/zshrc.backup.20230905" nil) "echo ~") tramp-sh-handle-get-home-directory((tramp-file-name "ssh" "harvey" nil "192.168.31.175" nil "~/Downloads/zshrc.backup.20230905" nil) "")
再查看buffer *debug_tramp*
的内容如下,发现其与buffer *Backtrace*
并未有太多不同:
backtrace() tramp-error(nil file-error "Timeout reached, see buffer ‘*tramp/ssh harvey@192…") tramp-signal-hook-function(file-error ("Timeout reached, see buffer ‘*tramp/ssh harvey@192…")) signal(file-error ("Timeout reached, see buffer ‘*tramp/ssh harvey@192…")) tramp-maybe-open-connection((tramp-file-name "ssh" "harvey" nil "192.168.31.175" nil "~/Downloads/zshrc.backup.20230905" nil)) tramp-send-command((tramp-file-name "ssh" "harvey" nil "192.168.31.175" nil "~/Downloads/zshrc.backup.20230905" nil) "echo ~ 2>/dev/null; echo tramp_exit_status $?") tramp-send-command-and-check((tramp-file-name "ssh" "harvey" nil "192.168.31.175" nil "~/Downloads/zshrc.backup.20230905" nil) "echo ~") tramp-sh-handle-get-home-directory((tramp-file-name "ssh" "harvey" nil "192.168.31.175" nil "~/Downloads/zshrc.backup.20230905" nil) "")
最后,查看buffer *debug tramp/ssh harvey@192.168.31.175*
,部分信息如下:
11:49:28.486065 tramp-process-actions (1) # File error: Timeout reached, see buffer ‘*tramp/ssh harvey@192.168.31.175*’ for details Linux Gen8-1 6.1.0-15-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.66-1 (2023-12-09) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Sun Apr 7 11:47:28 2024 from 192.168.31.100 % harvey@Gen8-1 ~ 11:49:47.044520 tramp-process-actions (3) # Waiting for prompts from remote shell…failed 11:49:47.044772 tramp-maybe-open-connection (3) # Opening connection nil for harvey@192.168.31.175 using ssh…failed 11:49:47.044883 tramp-get-connection-property (7) # process-buffer nil; cache used: t
至此,引发卡死问题的直接元凶露出了马脚:Waiting for prompts from remote shell…failed。大致可以推定是TRAMP执行失败了!余下的工作,就借助搜索引擎和ChatGPT来推进了。果不其然,由于shell promot不被TRAMP识别,导致了Emacs被卡死。而之所以识别失败,是因为目标计算机采用了oh-my-zsh,将 PS1
设置成了“非主流”格式,同时,TRAMP又严重依赖通过文本解析来处理交互。于是,综合作用下,Emacs就表现出卡死的症状,–其实,如果等得时间足够长(大概3-5分钟),并执行Ctrl+g,Emacs会中止TRAMP操作,从而恢复正常。
上述的推理也在 解决Emacs远程连接时卡住的bug 和 Fixing Emacs tramp mode when using zsh 两处网络文章中得到了印证。
4. 解决
根据 Emacs Wiki: TrampMode 的解释,造成 TRAMP Hang 的原因还有其他。对本次问题提供的解决办法就是,在远程计算机的zsh配置文件(即 ~/.zshrc
)的末尾,增加如下设置:
# Fix Emacs Tramp mode's imcompatible with zsh (oh-my-zsh) prompt
if [[ "$TERM" == "dumb" ]]
then
unsetopt zle
unsetopt prompt_cr
unsetopt prompt_subst
if whence -w precmd >/dev/null; then
unfunction precmd
fi
if whence -w preexec >/dev/null; then
unfunction preexec
fi
PS1='$ '
fi
至此,问题得到了解决,也给我一些启发。首先,TRAMP采用文本解析的方式,在我看来是不够健壮的。如果不了解TRAMP对Shell的prompt敏感这个情况,用户很难第一时间会想到zsh/oh-my-zsh。–当然它们没有错。我注意到前文提到两篇网络文章分别发布于2012年和2016年,那么这十多年来一定有不少用户发生了和我一样的遭遇。今天的解决办法只能说是规避,至于如何优雅地处理这个问题,需要更多思考。
其次,开源软件、搜索引擎、大语言模型真是好东西。从前遇到开源软件的问题,首先是用搜索引擎,但是漫无目的。现在把问题提给大语言模型,它能想到可能的原因是哪几个,告诉你从什么地方开始排查。这次的问题排查就是如此。原本开源软件往往因为缺少专业的技术支持,而使得非专业用户望而却步,今天会因为类似ChatGPT一类的工具,使得技术问题也能被非专业用户解决,从而赢得它们青睐。