ssh remoting: Daemonize server process properly by forking and closing stdio (#19544)

This fixes the `ssh` proxy process not being notified when the proxy
process dies. Turns out that the server would have stdout/stderr/stdin
connected to the grand-parent ssh process connected to it and as long as
the server kept running (even once it was daemonized into the
background) the grand-parent ssh process wouldn't exit.

That in turn meant that the Zed client wasn't notified when the proxy
process died.

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
Thorsten Ball 2024-10-22 16:36:40 +02:00 committed by GitHub
parent f16461d7d0
commit b3aa7055e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 10 deletions

2
Cargo.lock generated
View file

@ -9159,6 +9159,7 @@ dependencies = [
"client",
"clock",
"env_logger",
"fork",
"fs",
"futures 0.3.30",
"git",
@ -9167,6 +9168,7 @@ dependencies = [
"http_client",
"language",
"languages",
"libc",
"log",
"lsp",
"node_runtime",

View file

@ -53,6 +53,10 @@ smol.workspace = true
util.workspace = true
worktree.workspace = true
[target.'cfg(not(windows))'.dependencies]
fork.workspace = true
libc.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }

View file

@ -27,6 +27,7 @@ use smol::io::AsyncReadExt;
use smol::Async;
use smol::{net::unix::UnixListener, stream::StreamExt as _};
use std::ops::ControlFlow;
use std::{
io::Write,
mem,
@ -305,10 +306,15 @@ pub fn execute_run(
stdout_socket: PathBuf,
stderr_socket: PathBuf,
) -> Result<()> {
let log_rx = init_logging_server(log_file)?;
init_panic_hook();
init_paths()?;
match daemonize()? {
ControlFlow::Break(_) => return Ok(()),
ControlFlow::Continue(_) => {}
}
init_panic_hook();
let log_rx = init_logging_server(log_file)?;
log::info!(
"starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}",
pid_file,
@ -322,8 +328,6 @@ pub fn execute_run(
let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?;
log::info!("starting headless gpui app");
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
gpui::App::headless().run(move |cx| {
settings::init(cx);
@ -523,7 +527,8 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
}
let binary_name = std::env::current_exe()?;
let server_process = smol::process::Command::new(binary_name)
let mut server_process = std::process::Command::new(binary_name);
server_process
.arg("run")
.arg("--log-file")
.arg(&paths.log_file)
@ -534,12 +539,14 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
.arg("--stdout-socket")
.arg(&paths.stdout_socket)
.arg("--stderr-socket")
.arg(&paths.stderr_socket)
.spawn()?;
.arg(&paths.stderr_socket);
log::info!(
"proxy spawned server process. PID: {:?}",
server_process.id()
let status = server_process
.status()
.context("failed to launch server process")?;
anyhow::ensure!(
status.success(),
"failed to launch and detach server process"
);
let mut total_time_waited = std::time::Duration::from_secs(0);
@ -745,3 +752,43 @@ fn read_proxy_settings(cx: &mut ModelContext<'_, HeadlessProject>) -> Option<Uri
.or_else(read_proxy_from_env);
proxy_url
}
fn daemonize() -> Result<ControlFlow<()>> {
match fork::fork().map_err(|e| anyhow::anyhow!("failed to call fork with error code {}", e))? {
fork::Fork::Parent(_) => {
return Ok(ControlFlow::Break(()));
}
fork::Fork::Child => {}
}
// Once we've detached from the parent, we want to close stdout/stderr/stdin
// so that the outer SSH process is not attached to us in any way anymore.
unsafe { redirect_standard_streams() }?;
Ok(ControlFlow::Continue(()))
}
unsafe fn redirect_standard_streams() -> Result<()> {
let devnull_fd = libc::open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR);
anyhow::ensure!(devnull_fd != -1, "failed to open /dev/null");
let process_stdio = |name, fd| {
let reopened_fd = libc::dup2(devnull_fd, fd);
anyhow::ensure!(
reopened_fd != -1,
format!("failed to redirect {} to /dev/null", name)
);
Ok(())
};
process_stdio("stdin", libc::STDIN_FILENO)?;
process_stdio("stdout", libc::STDOUT_FILENO)?;
process_stdio("stderr", libc::STDERR_FILENO)?;
anyhow::ensure!(
libc::close(devnull_fd) != -1,
"failed to close /dev/null fd after redirecting"
);
Ok(())
}