diff --git a/CHANGELOG.md b/CHANGELOG.md index 16d46b543..8f8de28ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * chore: upgrade wasmi to 1.1.0 (https://github.com/zellij-org/zellij/pull/5319) * fix: fragmented STDIN causing "garbage" input on slow SSH connections (https://github.com/zellij-org/zellij/pull/5320) * feat: stacked lists - new stack UI (https://github.com/zellij-org/zellij/pull/5331) +* fix: more verbose startup errors (https://github.com/zellij-org/zellij/pull/5334) ## [0.44.3] - 2026-05-13 * fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139) diff --git a/src/commands.rs b/src/commands.rs index 4d3b83d83..bc561a754 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -18,7 +18,7 @@ use zellij_utils::sessions::{ generate_unique_session_name, get_active_session, get_resurrectable_sessions, get_sessions, get_sessions_sorted_by_mtime, kill_session as kill_session_impl, match_session_name, print_sessions, print_sessions_with_index, resurrection_layout, session_exists, - validate_session_name, ActiveSession, SessionNameMatch, + session_listing_error_message, validate_session_name, ActiveSession, SessionNameMatch, }; use zellij_utils::consts::session_layout_cache_file_name; @@ -629,18 +629,19 @@ fn attach_with_session_name( create: bool, ) -> ClientInfo { match &session_name { - Some(session) if create => { - if session_exists(session).unwrap() { - ClientInfo::Attach(session_name.unwrap(), config_options) - } else { - ClientInfo::New(session_name.unwrap(), None, None) - } + Some(session) if create => match session_exists(session) { + Ok(true) => ClientInfo::Attach(session_name.unwrap(), config_options), + Ok(false) => ClientInfo::New(session_name.unwrap(), None, None), + Err(kind) => { + eprintln!("{}", session_listing_error_message(kind)); + process::exit(1); + }, }, - Some(prefix) => match match_session_name(prefix).unwrap() { - SessionNameMatch::UniquePrefix(s) | SessionNameMatch::Exact(s) => { + Some(prefix) => match match_session_name(prefix) { + Ok(SessionNameMatch::UniquePrefix(s)) | Ok(SessionNameMatch::Exact(s)) => { ClientInfo::Attach(s, config_options) }, - SessionNameMatch::AmbiguousPrefix(sessions) => { + Ok(SessionNameMatch::AmbiguousPrefix(sessions)) => { println!( "Ambiguous selection: multiple sessions names start with '{}':", prefix @@ -656,10 +657,14 @@ fn attach_with_session_name( ); process::exit(1); }, - SessionNameMatch::None => { + Ok(SessionNameMatch::None) => { eprintln!("No session with the name '{}' found!", prefix); process::exit(1); }, + Err(kind) => { + eprintln!("{}", session_listing_error_message(kind)); + process::exit(1); + }, }, None => match get_active_session() { ActiveSession::None if create => create_new_client(), diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 2c8cc9506..2db4617f4 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -346,6 +346,90 @@ fn check_ipc_pipe_length(ipc_pipe: &Path) { } } +#[derive(Clone, Copy)] +struct TerminalTeardown { + include_kitty_exit: bool, +} + +fn exit_after_startup_error(teardown: Option, message: String) -> ! { + log::error!("{}", message); + match teardown { + Some(teardown) => { + let kitty_exit = if teardown.include_kitty_exit { + EXIT_KITTY_KEYBOARD_MODE + } else { + "" + }; + let rendered = format!( + "{}{}{}{}{}\r\n{}\n", + kitty_exit, + DISABLE_HOST_THEME_NOTIFY, + EXIT_ALTERNATE_SCREEN, + RESET_STYLE, + SHOW_CURSOR, + message + ); + let mut stdout = io::stdout(); + let _ = stdout.write_all(rendered.as_bytes()); + let _ = stdout.flush(); + }, + None => { + eprintln!("{}", message); + }, + } + std::process::exit(1); +} + +fn spawn_server_error_message(e: io::Error) -> String { + format!( + "Error: failed to start the Zellij server process:\n\n\ + Reason: {}\n\n\ + This can happen if the Zellij binary cannot be executed, or if the server \ + could not create its session socket - for example due to a permission issue \ + in the socket directory.\n\ + To fix a socket directory issue, set a writable socket directory, eg.:\n \ + ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij", + e + ) +} + +fn create_ipc_pipe(teardown: Option) -> PathBuf { + let mut sock_dir = ZELLIJ_SOCK_DIR.clone(); + if let Err(e) = std::fs::create_dir_all(&sock_dir) { + exit_after_startup_error( + teardown, + format!( + "Error: failed to create the Zellij socket directory:\n {}\n\n\ + Reason: {}\n\n\ + This usually means the directory (or one of its parents) is owned by \ + another user or is not writable - for example if Zellij was previously \ + run with `sudo`, or if $XDG_RUNTIME_DIR points to a directory you do not \ + own.\nTo fix this, remove or correct the offending directory, or set a \ + writable socket directory, eg.:\n ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij", + sock_dir.display(), + e + ), + ); + } + if let Err(e) = set_permissions(&sock_dir, 0o700) { + exit_after_startup_error( + teardown, + format!( + "Error: failed to set permissions (0700) on the Zellij socket directory:\n {}\n\n\ + Reason: {}\n\n\ + This usually means the directory is owned by another user.\n\ + To fix this, remove or correct the offending directory, or set a writable \ + socket directory, eg.:\n ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij", + sock_dir.display(), + e + ), + ); + } + sock_dir.push(envs::get_session_name().unwrap()); + check_ipc_pipe_length(&sock_dir); + sock_dir +} + /// Spawn the Zellij server process. /// /// On Unix the server daemonizes (double-fork) inside start_server(), so @@ -782,20 +866,15 @@ pub fn start_client( config_options.web_server_cert.is_some() && config_options.web_server_key.is_some(); let enforce_https_for_localhost = config_options.enforce_https_for_localhost.unwrap_or(false); - let create_ipc_pipe = || -> std::path::PathBuf { - let mut sock_dir = ZELLIJ_SOCK_DIR.clone(); - std::fs::create_dir_all(&sock_dir).unwrap(); - set_permissions(&sock_dir, 0o700).unwrap(); - sock_dir.push(envs::get_session_name().unwrap()); - check_ipc_pipe_length(&sock_dir); - sock_dir + let terminal_teardown = TerminalTeardown { + include_kitty_exit: !explicitly_disable_kitty_keyboard_protocol, }; let (first_msg, ipc_pipe) = match info { ClientInfo::Attach(name, config_options) => { envs::set_session_name(name.clone()); os_input.update_session_name(name); - let ipc_pipe = create_ipc_pipe(); + let ipc_pipe = create_ipc_pipe(Some(terminal_teardown)); let is_web_client = false; let cli_assets = CliAssets { @@ -845,7 +924,7 @@ pub fn start_client( ClientInfo::Watch(name, _config_options) => { envs::set_session_name(name.clone()); os_input.update_session_name(name); - let ipc_pipe = create_ipc_pipe(); + let ipc_pipe = create_ipc_pipe(Some(terminal_teardown)); let is_web_client = false; ( @@ -877,9 +956,11 @@ pub fn start_client( }; os_input.update_session_name(name); - let ipc_pipe = create_ipc_pipe(); + let ipc_pipe = create_ipc_pipe(Some(terminal_teardown)); - os_input.spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + if let Err(e) = os_input.spawn_server(&*ipc_pipe, cli_args.debug) { + exit_after_startup_error(Some(terminal_teardown), spawn_server_error_message(e)); + } if should_start_web_server { if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); @@ -931,9 +1012,11 @@ pub fn start_client( }; os_input.update_session_name(name); - let ipc_pipe = create_ipc_pipe(); + let ipc_pipe = create_ipc_pipe(Some(terminal_teardown)); - os_input.spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + if let Err(e) = os_input.spawn_server(&*ipc_pipe, cli_args.debug) { + exit_after_startup_error(Some(terminal_teardown), spawn_server_error_message(e)); + } if should_start_web_server { if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); @@ -1301,15 +1384,6 @@ pub fn start_server_detached( let should_start_web_server = config_options.web_server.map(|w| w).unwrap_or(false); - let create_ipc_pipe = || -> std::path::PathBuf { - let mut sock_dir = ZELLIJ_SOCK_DIR.clone(); - std::fs::create_dir_all(&sock_dir).unwrap(); - set_permissions(&sock_dir, 0o700).unwrap(); - sock_dir.push(envs::get_session_name().unwrap()); - check_ipc_pipe_length(&sock_dir); - sock_dir - }; - let (first_msg, ipc_pipe) = match info { ClientInfo::Resurrect(name, path_to_layout, force_run_commands, cwd) => { envs::set_session_name(name.clone()); @@ -1333,9 +1407,11 @@ pub fn start_server_detached( }; os_input.update_session_name(name); - let ipc_pipe = create_ipc_pipe(); + let ipc_pipe = create_ipc_pipe(None); - os_input.spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + if let Err(e) = os_input.spawn_server(&*ipc_pipe, cli_args.debug) { + exit_after_startup_error(None, spawn_server_error_message(e)); + } if should_start_web_server { if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); @@ -1388,9 +1464,11 @@ pub fn start_server_detached( }; os_input.update_session_name(name); - let ipc_pipe = create_ipc_pipe(); + let ipc_pipe = create_ipc_pipe(None); - os_input.spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + if let Err(e) = os_input.spawn_server(&*ipc_pipe, cli_args.debug) { + exit_after_startup_error(None, spawn_server_error_message(e)); + } if should_start_web_server { if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); diff --git a/zellij-utils/src/sessions.rs b/zellij-utils/src/sessions.rs index 6c49fdbe4..7e420778c 100644 --- a/zellij-utils/src/sessions.rs +++ b/zellij-utils/src/sessions.rs @@ -552,6 +552,20 @@ pub fn assert_session_ne(name: &str) { process::exit(1); } +pub fn session_listing_error_message(kind: io::ErrorKind) -> String { + format!( + "Failed to list existing Zellij sessions in the socket directory:\n {}\n\n\ + Reason: {}\n\n\ + This usually means the directory (or one of its parents) is not readable \ + by the current user - for example if $ZELLIJ_SOCKET_DIR or $XDG_RUNTIME_DIR \ + points to a directory you do not own.\n\ + To fix this, set a readable and writable socket directory, eg.:\n \ + ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij", + ZELLIJ_SOCK_DIR.display(), + io::Error::from(kind) + ) +} + pub fn generate_unique_session_name() -> Option { let sessions = get_sessions().map(|sessions| { sessions @@ -560,9 +574,12 @@ pub fn generate_unique_session_name() -> Option { .collect::>() }); let dead_sessions = get_resurrectable_session_names(); - let Ok(sessions) = sessions else { - eprintln!("Failed to list existing sessions: {:?}", sessions); - return None; + let sessions = match sessions { + Ok(sessions) => sessions, + Err(kind) => { + eprintln!("{}", session_listing_error_message(kind)); + return None; + }, }; let name = get_name_generator()