Fix zed sometimes stopping by using setsid on interactive shells (#29070)

For some reason `SIGTTIN` sometimes gets sent to the process group,
causing it to stop when run from a terminal. This solves that issue by
putting the shell in a new session + progress group.

This allows removal of a workaround of using `exit 0;` to restore
handling of ctrl-c after exit. In testing this appears to no longer be
necessary.

Closes #27716

Release Notes:

- Fixed Zed sometimes becoming a stopped background process when run
from a terminal.
This commit is contained in:
Michael Sloan 2025-04-18 15:04:26 -06:00 committed by GitHub
parent 7abe2c9c31
commit 8c55063417
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 42 additions and 44 deletions

1
Cargo.lock generated
View file

@ -15835,7 +15835,6 @@ dependencies = [
"indoc", "indoc",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
"libc",
"log", "log",
"lsp", "lsp",
"multi_buffer", "multi_buffer",

View file

@ -273,23 +273,14 @@ async fn load_shell_environment(
// //
// In certain shells we need to execute additional_command in order to // In certain shells we need to execute additional_command in order to
// trigger the behavior of direnv, etc. // trigger the behavior of direnv, etc.
//
//
// The `exit 0` is the result of hours of debugging, trying to find out
// why running this command here, without `exit 0`, would mess
// up signal process for our process so that `ctrl-c` doesn't work
// anymore.
//
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
// do that, but it does, and `exit 0` helps.
let command = match shell_name { let command = match shell_name {
Some("fish") => format!( Some("fish") => format!(
"cd '{}'; emit fish_prompt; printf '%s' {MARKER}; /usr/bin/env; exit 0;", "cd '{}'; emit fish_prompt; printf '%s' {MARKER}; /usr/bin/env;",
dir.display() dir.display()
), ),
_ => format!( _ => format!(
"cd '{}'; printf '%s' {MARKER}; /usr/bin/env; exit 0;", "cd '{}'; printf '%s' {MARKER}; /usr/bin/env;",
dir.display() dir.display()
), ),
}; };
@ -297,16 +288,21 @@ async fn load_shell_environment(
// csh/tcsh only supports `-l` if it's the only flag. So this won't be a login shell. // csh/tcsh only supports `-l` if it's the only flag. So this won't be a login shell.
// Users must rely on vars from `~/.tcshrc` or `~/.cshrc` and not `.login` as a result. // Users must rely on vars from `~/.tcshrc` or `~/.cshrc` and not `.login` as a result.
let args = match shell_name { let args = match shell_name {
Some("tcsh") | Some("csh") => vec!["-i", "-c", &command], Some("tcsh") | Some("csh") => vec!["-i".to_string(), "-c".to_string(), command],
_ => vec!["-l", "-i", "-c", &command], _ => vec![
"-l".to_string(),
"-i".to_string(),
"-c".to_string(),
command,
],
}; };
let Some(output) = smol::process::Command::new(&shell) let Some(output) = smol::unblock(move || {
.args(&args) util::set_pre_exec_to_start_new_session(std::process::Command::new(&shell).args(&args))
.output() .output()
.await })
.log_err() .await
else { .log_err() else {
return message( return message(
"Failed to spawn login shell to source login environment variables. See logs for details", "Failed to spawn login shell to source login environment variables. See logs for details",
); );

View file

@ -321,21 +321,16 @@ pub fn load_login_shell_environment() -> Result<()> {
.and_then(|home| home.into_string().ok()) .and_then(|home| home.into_string().ok())
.map(|home| format!("cd '{home}';")); .map(|home| format!("cd '{home}';"));
// The `exit 0` is the result of hours of debugging, trying to find out
// why running this command here, without `exit 0`, would mess
// up signal process for our process so that `ctrl-c` doesn't work
// anymore.
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
// do that, but it does, and `exit 0` helps.
let shell_cmd = format!( let shell_cmd = format!(
"{}printf '%s' {marker}; /usr/bin/env; exit 0;", "{}printf '%s' {marker}; /usr/bin/env;",
shell_cmd_prefix.as_deref().unwrap_or("") shell_cmd_prefix.as_deref().unwrap_or("")
); );
let output = std::process::Command::new(&shell) let output = set_pre_exec_to_start_new_session(
.args(["-l", "-i", "-c", &shell_cmd]) std::process::Command::new(&shell).args(["-l", "-i", "-c", &shell_cmd]),
.output() )
.context("failed to spawn login shell to source login environment variables")?; .output()
.context("failed to spawn login shell to source login environment variables")?;
if !output.status.success() { if !output.status.success() {
Err(anyhow!("login shell exited with error"))?; Err(anyhow!("login shell exited with error"))?;
} }
@ -357,6 +352,26 @@ pub fn load_login_shell_environment() -> Result<()> {
Ok(()) Ok(())
} }
/// Configures the process to start a new session, to prevent interactive shells from taking control
/// of the terminal.
///
/// For more details: https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell
pub fn set_pre_exec_to_start_new_session(
command: &mut std::process::Command,
) -> &mut std::process::Command {
// safety: code in pre_exec should be signal safe.
// https://man7.org/linux/man-pages/man7/signal-safety.7.html
#[cfg(not(target_os = "windows"))]
unsafe {
use std::os::unix::process::CommandExt;
command.pre_exec(|| {
libc::setsid();
Ok(())
});
};
command
}
/// Parse the result of calling `usr/bin/env` with no arguments /// Parse the result of calling `usr/bin/env` with no arguments
pub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) { pub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) {
let mut current_key: Option<String> = None; let mut current_key: Option<String> = None;

View file

@ -28,7 +28,6 @@ futures.workspace = true
gpui.workspace = true gpui.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
libc.workspace = true
log.workspace = true log.workspace = true
multi_buffer.workspace = true multi_buffer.workspace = true
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true } nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }

View file

@ -1519,18 +1519,7 @@ impl ShellExec {
process.stdin(Stdio::null()); process.stdin(Stdio::null());
}; };
// https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell util::set_pre_exec_to_start_new_session(&mut process);
//
// safety: code in pre_exec should be signal safe.
// https://man7.org/linux/man-pages/man7/signal-safety.7.html
#[cfg(not(target_os = "windows"))]
unsafe {
use std::os::unix::process::CommandExt;
process.pre_exec(|| {
libc::setsid();
Ok(())
});
};
let is_read = self.is_read; let is_read = self.is_read;
let task = cx.spawn_in(window, async move |vim, cx| { let task = cx.spawn_in(window, async move |vim, cx| {