几周前,作者在 Zed 中致力于语言服务器支持,试图让 Zed 检测给定的语言服务器二进制文件(如gopls
)是否已在$PATH
中。挑战在于$PATH
常被direnv
、asdf
等工具动态修改,不能仅使用 Zed 进程的$PATH
,而需获取进入项目目录时的$PATH
。
作者认为可通过启动$SHELL
、进入项目触发相关工具、运行env
获取环境并挑选$PATH
来解决。代码如下:
fn load_shell_environment(dir: &Path) -> Result<HashMap<String, String>> {
let shell = std::env::var("SHELL")?;
let command = format!("cd {:?}; /usr/bin/env -0;", dir);
let output = std::process::Command::new(&shell)
.args(["-i", "-c", &command])
.output()?;
// [...检查退出码、获取标准输出、将标准输出转换为 HashMap 等...]
}
但执行该函数启动 Zed 实例后,无法通过Ctrl-C
终止 Zed。作者尝试各种方法,如改变command
为不同命令(/usr/bin/env;
、echo lol
、/usr/bin/env; echo lol
、ls
等),发现仅在某些情况下Ctrl-C
有效,在某些情况下无效。
作者还尝试在新的 Rust 项目中复制该函数,在 Go 中也出现相同问题,最终找到一个 workaround:let command = format!("/usr/bin/env; exit 0;");
。
作者建立了一个仓库来重现问题,通过kill -INT
命令发现信号能到达并使进程停止,并非信号处理出问题,而是Ctrl-C
在启动$SHELL
进程后不能传递正确信号。
作者通过tcgetpgrp
函数获取与STDIN
相关的进程组 ID,发现启动$SHELL
前后进程组 ID 发生变化,通过pre_exec
钩子将$SHELL
进程置于新的独立进程会话中,使其不再成为STDIN
相关进程组的领导,Ctrl-C
又能正常工作,从而确定前台进程组 ID 是问题所在。
作者通过strace -f
查看系统调用,发现当zsh
以-c
运行且最后命令为非内置命令时,zsh
会execve
到该命令,不再创建子进程运行该命令,导致无法重置前台进程组领导。而先运行非内置命令再运行内置命令时,zsh
会进行fork
和exec
操作并清理前台进程组领导。但作者最终因难以理解 ZSH 内部机制而放弃,期待他人解释。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。