remote_server: Improve error reporting (#33770)

Closes #33736

Use `thiserror` to implement error stack and `anyhow` to report is to
user.
Also move some code from main to remote_server to have better crate
isolation.

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
Gwen Lg 2025-08-25 22:23:29 +02:00 committed by GitHub
parent 99cee8778c
commit ad25aba990
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 216 additions and 105 deletions

1
Cargo.lock generated
View file

@ -13521,6 +13521,7 @@ dependencies = [
"smol", "smol",
"sysinfo", "sysinfo",
"telemetry_events", "telemetry_events",
"thiserror 2.0.12",
"toml 0.8.20", "toml 0.8.20",
"unindent", "unindent",
"util", "util",

View file

@ -65,6 +65,7 @@ telemetry_events.workspace = true
util.workspace = true util.workspace = true
watch.workspace = true watch.workspace = true
worktree.workspace = true worktree.workspace = true
thiserror.workspace = true
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
crashes.workspace = true crashes.workspace = true

View file

@ -1,6 +1,7 @@
#![cfg_attr(target_os = "windows", allow(unused, dead_code))] #![cfg_attr(target_os = "windows", allow(unused, dead_code))]
use clap::{Parser, Subcommand}; use clap::Parser;
use remote_server::Commands;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Parser)] #[derive(Parser)]
@ -21,105 +22,34 @@ struct Cli {
printenv: bool, printenv: bool,
} }
#[derive(Subcommand)]
enum Commands {
Run {
#[arg(long)]
log_file: PathBuf,
#[arg(long)]
pid_file: PathBuf,
#[arg(long)]
stdin_socket: PathBuf,
#[arg(long)]
stdout_socket: PathBuf,
#[arg(long)]
stderr_socket: PathBuf,
},
Proxy {
#[arg(long)]
reconnect: bool,
#[arg(long)]
identifier: String,
},
Version,
}
#[cfg(windows)] #[cfg(windows)]
fn main() { fn main() {
unimplemented!() unimplemented!()
} }
#[cfg(not(windows))] #[cfg(not(windows))]
fn main() { fn main() -> anyhow::Result<()> {
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
use remote::proxy::ProxyLaunchError;
use remote_server::unix::{execute_proxy, execute_run};
let cli = Cli::parse(); let cli = Cli::parse();
if let Some(socket_path) = &cli.askpass { if let Some(socket_path) = &cli.askpass {
askpass::main(socket_path); askpass::main(socket_path);
return; return Ok(());
} }
if let Some(socket) = &cli.crash_handler { if let Some(socket) = &cli.crash_handler {
crashes::crash_server(socket.as_path()); crashes::crash_server(socket.as_path());
return; return Ok(());
} }
if cli.printenv { if cli.printenv {
util::shell_env::print_env(); util::shell_env::print_env();
return; return Ok(());
} }
let result = match cli.command { if let Some(command) = cli.command {
Some(Commands::Run { remote_server::run(command)
log_file, } else {
pid_file, eprintln!("usage: remote <run|proxy|version>");
stdin_socket,
stdout_socket,
stderr_socket,
}) => execute_run(
log_file,
pid_file,
stdin_socket,
stdout_socket,
stderr_socket,
),
Some(Commands::Proxy {
identifier,
reconnect,
}) => 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) => {
let release_channel = *RELEASE_CHANNEL;
match release_channel {
ReleaseChannel::Stable | ReleaseChannel::Preview => {
println!("{}", env!("ZED_PKG_VERSION"))
}
ReleaseChannel::Nightly | ReleaseChannel::Dev => {
println!(
"{}",
option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name())
)
}
};
std::process::exit(0);
}
None => {
eprintln!("usage: remote <run|proxy|version>");
std::process::exit(1);
}
};
if let Err(error) = result {
log::error!("exiting due to error: {}", error);
std::process::exit(1); std::process::exit(1);
} }
} }

View file

@ -6,4 +6,78 @@ pub mod unix;
#[cfg(test)] #[cfg(test)]
mod remote_editing_tests; mod remote_editing_tests;
use clap::Subcommand;
use std::path::PathBuf;
pub use headless_project::{HeadlessAppState, HeadlessProject}; pub use headless_project::{HeadlessAppState, HeadlessProject};
#[derive(Subcommand)]
pub enum Commands {
Run {
#[arg(long)]
log_file: PathBuf,
#[arg(long)]
pid_file: PathBuf,
#[arg(long)]
stdin_socket: PathBuf,
#[arg(long)]
stdout_socket: PathBuf,
#[arg(long)]
stderr_socket: PathBuf,
},
Proxy {
#[arg(long)]
reconnect: bool,
#[arg(long)]
identifier: String,
},
Version,
}
#[cfg(not(windows))]
pub fn run(command: Commands) -> anyhow::Result<()> {
use anyhow::Context;
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
use unix::{ExecuteProxyError, execute_proxy, execute_run};
match command {
Commands::Run {
log_file,
pid_file,
stdin_socket,
stdout_socket,
stderr_socket,
} => execute_run(
log_file,
pid_file,
stdin_socket,
stdout_socket,
stderr_socket,
),
Commands::Proxy {
identifier,
reconnect,
} => execute_proxy(identifier, reconnect)
.inspect_err(|err| {
if let ExecuteProxyError::ServerNotRunning(err) = err {
std::process::exit(err.to_exit_code());
}
})
.context("running proxy on the remote server"),
Commands::Version => {
let release_channel = *RELEASE_CHANNEL;
match release_channel {
ReleaseChannel::Stable | ReleaseChannel::Preview => {
println!("{}", env!("ZED_PKG_VERSION"))
}
ReleaseChannel::Nightly | ReleaseChannel::Dev => {
println!(
"{}",
option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name())
)
}
};
Ok(())
}
}
}

View file

@ -36,6 +36,7 @@ use smol::Async;
use smol::{net::unix::UnixListener, stream::StreamExt as _}; use smol::{net::unix::UnixListener, stream::StreamExt as _};
use std::ffi::OsStr; use std::ffi::OsStr;
use std::ops::ControlFlow; use std::ops::ControlFlow;
use std::process::ExitStatus;
use std::str::FromStr; use std::str::FromStr;
use std::sync::LazyLock; use std::sync::LazyLock;
use std::{env, thread}; use std::{env, thread};
@ -46,6 +47,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use telemetry_events::LocationData; use telemetry_events::LocationData;
use thiserror::Error;
use util::ResultExt; use util::ResultExt;
pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL {
@ -526,7 +528,23 @@ pub fn execute_run(
Ok(()) Ok(())
} }
#[derive(Clone)] #[derive(Debug, Error)]
pub(crate) enum ServerPathError {
#[error("Failed to create server_dir `{path}`")]
CreateServerDir {
#[source]
source: std::io::Error,
path: PathBuf,
},
#[error("Failed to create logs_dir `{path}`")]
CreateLogsDir {
#[source]
source: std::io::Error,
path: PathBuf,
},
}
#[derive(Clone, Debug)]
struct ServerPaths { struct ServerPaths {
log_file: PathBuf, log_file: PathBuf,
pid_file: PathBuf, pid_file: PathBuf,
@ -536,10 +554,19 @@ struct ServerPaths {
} }
impl ServerPaths { impl ServerPaths {
fn new(identifier: &str) -> Result<Self> { fn new(identifier: &str) -> Result<Self, ServerPathError> {
let server_dir = paths::remote_server_state_dir().join(identifier); let server_dir = paths::remote_server_state_dir().join(identifier);
std::fs::create_dir_all(&server_dir)?; std::fs::create_dir_all(&server_dir).map_err(|source| {
std::fs::create_dir_all(&logs_dir())?; ServerPathError::CreateServerDir {
source,
path: server_dir.clone(),
}
})?;
let log_dir = logs_dir();
std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir {
source: source,
path: log_dir.clone(),
})?;
let pid_file = server_dir.join("server.pid"); let pid_file = server_dir.join("server.pid");
let stdin_socket = server_dir.join("stdin.sock"); let stdin_socket = server_dir.join("stdin.sock");
@ -557,7 +584,43 @@ impl ServerPaths {
} }
} }
pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { #[derive(Debug, Error)]
pub(crate) enum ExecuteProxyError {
#[error("Failed to init server paths")]
ServerPath(#[from] ServerPathError),
#[error(transparent)]
ServerNotRunning(#[from] ProxyLaunchError),
#[error("Failed to check PidFile '{path}'")]
CheckPidFile {
#[source]
source: CheckPidError,
path: PathBuf,
},
#[error("Failed to kill existing server with pid '{pid}'")]
KillRunningServer {
#[source]
source: std::io::Error,
pid: u32,
},
#[error("failed to spawn server")]
SpawnServer(#[source] SpawnServerError),
#[error("stdin_task failed")]
StdinTask(#[source] anyhow::Error),
#[error("stdout_task failed")]
StdoutTask(#[source] anyhow::Error),
#[error("stderr_task failed")]
StderrTask(#[source] anyhow::Error),
}
pub(crate) fn execute_proxy(
identifier: String,
is_reconnecting: bool,
) -> Result<(), ExecuteProxyError> {
init_logging_proxy(); init_logging_proxy();
let server_paths = ServerPaths::new(&identifier)?; let server_paths = ServerPaths::new(&identifier)?;
@ -574,12 +637,19 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
log::info!("starting proxy process. PID: {}", std::process::id()); log::info!("starting proxy process. PID: {}", std::process::id());
let server_pid = check_pid_file(&server_paths.pid_file)?; let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| {
ExecuteProxyError::CheckPidFile {
source,
path: server_paths.pid_file.clone(),
}
})?;
let server_running = server_pid.is_some(); let server_running = server_pid.is_some();
if is_reconnecting { if is_reconnecting {
if !server_running { if !server_running {
log::error!("attempted to reconnect, but no server running"); log::error!("attempted to reconnect, but no server running");
anyhow::bail!(ProxyLaunchError::ServerNotRunning); return Err(ExecuteProxyError::ServerNotRunning(
ProxyLaunchError::ServerNotRunning,
));
} }
} else { } else {
if let Some(pid) = server_pid { if let Some(pid) = server_pid {
@ -590,7 +660,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
kill_running_server(pid, &server_paths)?; kill_running_server(pid, &server_paths)?;
} }
spawn_server(&server_paths)?; spawn_server(&server_paths).map_err(ExecuteProxyError::SpawnServer)?;
}; };
let stdin_task = smol::spawn(async move { let stdin_task = smol::spawn(async move {
@ -630,9 +700,9 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
if let Err(forwarding_result) = smol::block_on(async move { if let Err(forwarding_result) = smol::block_on(async move {
futures::select! { futures::select! {
result = stdin_task.fuse() => result.context("stdin_task failed"), result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask),
result = stdout_task.fuse() => result.context("stdout_task failed"), result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask),
result = stderr_task.fuse() => result.context("stderr_task failed"), result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask),
} }
}) { }) {
log::error!( log::error!(
@ -645,12 +715,12 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
Ok(()) Ok(())
} }
fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> {
log::info!("killing existing server with PID {}", pid); log::info!("killing existing server with PID {}", pid);
std::process::Command::new("kill") std::process::Command::new("kill")
.arg(pid.to_string()) .arg(pid.to_string())
.output() .output()
.context("failed to kill existing server")?; .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?;
for file in [ for file in [
&paths.pid_file, &paths.pid_file,
@ -664,18 +734,39 @@ fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> {
Ok(()) Ok(())
} }
fn spawn_server(paths: &ServerPaths) -> Result<()> { #[derive(Debug, Error)]
pub(crate) enum SpawnServerError {
#[error("failed to remove stdin socket")]
RemoveStdinSocket(#[source] std::io::Error),
#[error("failed to remove stdout socket")]
RemoveStdoutSocket(#[source] std::io::Error),
#[error("failed to remove stderr socket")]
RemoveStderrSocket(#[source] std::io::Error),
#[error("failed to get current_exe")]
CurrentExe(#[source] std::io::Error),
#[error("failed to launch server process")]
ProcessStatus(#[source] std::io::Error),
#[error("failed to launch and detach server process: {status}\n{paths}")]
LaunchStatus { status: ExitStatus, paths: String },
}
fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> {
if paths.stdin_socket.exists() { if paths.stdin_socket.exists() {
std::fs::remove_file(&paths.stdin_socket)?; std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?;
} }
if paths.stdout_socket.exists() { if paths.stdout_socket.exists() {
std::fs::remove_file(&paths.stdout_socket)?; std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?;
} }
if paths.stderr_socket.exists() { if paths.stderr_socket.exists() {
std::fs::remove_file(&paths.stderr_socket)?; std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?;
} }
let binary_name = std::env::current_exe()?; let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?;
let mut server_process = std::process::Command::new(binary_name); let mut server_process = std::process::Command::new(binary_name);
server_process server_process
.arg("run") .arg("run")
@ -692,11 +783,17 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
let status = server_process let status = server_process
.status() .status()
.context("failed to launch server process")?; .map_err(SpawnServerError::ProcessStatus)?;
anyhow::ensure!(
status.success(), if !status.success() {
"failed to launch and detach server process" return Err(SpawnServerError::LaunchStatus {
); status,
paths: format!(
"log file: {:?}, pid file: {:?}",
paths.log_file, paths.pid_file,
),
});
}
let mut total_time_waited = std::time::Duration::from_secs(0); let mut total_time_waited = std::time::Duration::from_secs(0);
let wait_duration = std::time::Duration::from_millis(20); let wait_duration = std::time::Duration::from_millis(20);
@ -717,7 +814,15 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
Ok(()) Ok(())
} }
fn check_pid_file(path: &Path) -> Result<Option<u32>> { #[derive(Debug, Error)]
#[error("Failed to remove PID file for missing process (pid `{pid}`")]
pub(crate) struct CheckPidError {
#[source]
source: std::io::Error,
pid: u32,
}
fn check_pid_file(path: &Path) -> Result<Option<u32>, CheckPidError> {
let Some(pid) = std::fs::read_to_string(&path) let Some(pid) = std::fs::read_to_string(&path)
.ok() .ok()
.and_then(|contents| contents.parse::<u32>().ok()) .and_then(|contents| contents.parse::<u32>().ok())
@ -742,7 +847,7 @@ fn check_pid_file(path: &Path) -> Result<Option<u32>> {
log::debug!( log::debug!(
"Found PID file, but process with that PID does not exist. Removing PID file." "Found PID file, but process with that PID does not exist. Removing PID file."
); );
std::fs::remove_file(&path).context("Failed to remove PID file")?; std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?;
Ok(None) Ok(None)
} }
} }