remote server: Do not spawn server when proxy reconnects (#18864)

This ensures that we only ever reconnect to a running server and not
spawn a new server with no state.

This avoids the problem of the server process crashing, `proxy`
reconnecting, starting a new server, and the user getting errors like
"unknown buffer id: ...".

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
This commit is contained in:
Thorsten Ball 2024-10-09 16:51:12 +02:00 committed by GitHub
parent dbf986d37a
commit c674d73734
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 248 additions and 91 deletions

View file

@ -24,6 +24,8 @@ enum Commands {
stdout_socket: PathBuf,
},
Proxy {
#[arg(long)]
reconnect: bool,
#[arg(long)]
identifier: String,
},
@ -37,6 +39,7 @@ fn main() {
#[cfg(not(windows))]
fn main() -> Result<()> {
use remote::proxy::ProxyLaunchError;
use remote_server::unix::{execute_proxy, execute_run, init};
let cli = Cli::parse();
@ -51,9 +54,20 @@ fn main() -> Result<()> {
init(Some(log_file))?;
execute_run(pid_file, stdin_socket, stdout_socket)
}
Some(Commands::Proxy { identifier }) => {
Some(Commands::Proxy {
identifier,
reconnect,
}) => {
init(None)?;
execute_proxy(identifier)
match execute_proxy(identifier, reconnect) {
Ok(_) => Ok(()),
Err(err) => {
if let Some(err) = err.downcast_ref::<ProxyLaunchError>() {
std::process::exit(err.to_exit_code());
}
Err(err)
}
}
}
Some(Commands::Version) => {
eprintln!("{}", env!("ZED_PKG_VERSION"));

View file

@ -4,6 +4,7 @@ use fs::RealFs;
use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, FutureExt, SinkExt};
use gpui::{AppContext, Context as _};
use remote::proxy::ProxyLaunchError;
use remote::ssh_session::ChannelClient;
use remote::{
json_log::LogRecord,
@ -227,30 +228,62 @@ pub fn execute_run(pid_file: PathBuf, stdin_socket: PathBuf, stdout_socket: Path
Ok(())
}
pub fn execute_proxy(identifier: String) -> Result<()> {
#[derive(Clone)]
struct ServerPaths {
log_file: PathBuf,
pid_file: PathBuf,
stdin_socket: PathBuf,
stdout_socket: PathBuf,
}
impl ServerPaths {
fn new(identifier: &str) -> Result<Self> {
let project_dir = create_state_directory(identifier)?;
let pid_file = project_dir.join("server.pid");
let stdin_socket = project_dir.join("stdin.sock");
let stdout_socket = project_dir.join("stdout.sock");
let log_file = project_dir.join("server.log");
Ok(Self {
pid_file,
stdin_socket,
stdout_socket,
log_file,
})
}
}
pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
log::debug!("proxy: starting up. PID: {}", std::process::id());
let project_dir = ensure_project_dir(&identifier)?;
let server_paths = ServerPaths::new(&identifier)?;
let pid_file = project_dir.join("server.pid");
let stdin_socket = project_dir.join("stdin.sock");
let stdout_socket = project_dir.join("stdout.sock");
let log_file = project_dir.join("server.log");
let server_pid = check_pid_file(&server_paths.pid_file)?;
let server_running = server_pid.is_some();
if is_reconnecting {
if !server_running {
log::error!("proxy: attempted to reconnect, but no server running");
return Err(anyhow!(ProxyLaunchError::ServerNotRunning));
}
} else {
if let Some(pid) = server_pid {
log::debug!("proxy: found server already running with PID {}. Killing process and cleaning up files...", pid);
kill_running_server(pid, &server_paths)?;
}
let server_running = check_pid_file(&pid_file)?;
if !server_running {
spawn_server(&log_file, &pid_file, &stdin_socket, &stdout_socket)?;
};
spawn_server(&server_paths)?;
}
let stdin_task = smol::spawn(async move {
let stdin = Async::new(std::io::stdin())?;
let stream = smol::net::unix::UnixStream::connect(stdin_socket).await?;
let stream = smol::net::unix::UnixStream::connect(&server_paths.stdin_socket).await?;
handle_io(stdin, stream, "stdin").await
});
let stdout_task: smol::Task<Result<()>> = smol::spawn(async move {
let stdout = Async::new(std::io::stdout())?;
let stream = smol::net::unix::UnixStream::connect(stdout_socket).await?;
let stream = smol::net::unix::UnixStream::connect(&server_paths.stdout_socket).await?;
handle_io(stream, stdout, "stdout").await
});
@ -267,50 +300,62 @@ pub fn execute_proxy(identifier: String) -> Result<()> {
Ok(())
}
fn ensure_project_dir(identifier: &str) -> Result<PathBuf> {
let project_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let project_dir = PathBuf::from(project_dir)
fn create_state_directory(identifier: &str) -> Result<PathBuf> {
let home_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let server_dir = PathBuf::from(home_dir)
.join(".local")
.join("state")
.join("zed-remote-server")
.join(identifier);
std::fs::create_dir_all(&project_dir)?;
std::fs::create_dir_all(&server_dir)?;
Ok(project_dir)
Ok(server_dir)
}
fn spawn_server(
log_file: &Path,
pid_file: &Path,
stdin_socket: &Path,
stdout_socket: &Path,
) -> Result<()> {
if stdin_socket.exists() {
std::fs::remove_file(&stdin_socket)?;
fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> {
log::info!("proxy: killing existing server with PID {}", pid);
std::process::Command::new("kill")
.arg(pid.to_string())
.output()
.context("proxy: failed to kill existing server")?;
for file in [&paths.pid_file, &paths.stdin_socket, &paths.stdout_socket] {
log::debug!(
"proxy: cleaning up file {:?} before starting new server",
file
);
std::fs::remove_file(file).ok();
}
if stdout_socket.exists() {
std::fs::remove_file(&stdout_socket)?;
Ok(())
}
fn spawn_server(paths: &ServerPaths) -> Result<()> {
if paths.stdin_socket.exists() {
std::fs::remove_file(&paths.stdin_socket)?;
}
if paths.stdout_socket.exists() {
std::fs::remove_file(&paths.stdout_socket)?;
}
let binary_name = std::env::current_exe()?;
let server_process = std::process::Command::new(binary_name)
.arg("run")
.arg("--log-file")
.arg(log_file)
.arg(&paths.log_file)
.arg("--pid-file")
.arg(pid_file)
.arg(&paths.pid_file)
.arg("--stdin-socket")
.arg(stdin_socket)
.arg(&paths.stdin_socket)
.arg("--stdout-socket")
.arg(stdout_socket)
.arg(&paths.stdout_socket)
.spawn()?;
log::debug!("proxy: server started. PID: {:?}", server_process.id());
let mut total_time_waited = std::time::Duration::from_secs(0);
let wait_duration = std::time::Duration::from_millis(20);
while !stdout_socket.exists() || !stdin_socket.exists() {
while !paths.stdout_socket.exists() || !paths.stdin_socket.exists() {
log::debug!("proxy: waiting for server to be ready to accept connections...");
std::thread::sleep(wait_duration);
total_time_waited += wait_duration;
@ -323,12 +368,12 @@ fn spawn_server(
Ok(())
}
fn check_pid_file(path: &Path) -> Result<bool> {
fn check_pid_file(path: &Path) -> Result<Option<u32>> {
let Some(pid) = std::fs::read_to_string(&path)
.ok()
.and_then(|contents| contents.parse::<u32>().ok())
else {
return Ok(false);
return Ok(None);
};
log::debug!("proxy: Checking if process with PID {} exists...", pid);
@ -339,12 +384,12 @@ fn check_pid_file(path: &Path) -> Result<bool> {
{
Ok(output) if output.status.success() => {
log::debug!("proxy: Process with PID {} exists. NOT spawning new server, but attaching to existing one.", pid);
Ok(true)
Ok(Some(pid))
}
_ => {
log::debug!("proxy: Found PID file, but process with that PID does not exist. Removing PID file.");
std::fs::remove_file(&path).context("proxy: Failed to remove PID file")?;
Ok(false)
Ok(None)
}
}
}