几周前,作者在 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) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。