From 0ff0f27e222633c97ba4291f6b9be4137002ca84 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Mon, 21 Sep 2026 00:29:45 +0300 Subject: fix: preserve session layouts across shutdown and restore failures (#4400) * fix: preserve session layouts across shutdown and restore failures refs #4320 * test: update subscription fixture for pane restore errors --------- Co-authored-by: JJ Liebig --- src/pane.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) (limited to 'src/pane.rs') diff --git a/src/pane.rs b/src/pane.rs index 3e1c52a4..099580c3 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -1255,6 +1255,8 @@ pub struct PaneRuntime { current_size: Cell<(u16, u16, u32, u32)>, child_pid: Arc, reported_cwd: Arc>>, + persistence_cwd: Mutex>, + cwd_process_exited: Arc, child_wait_completed: Option>, kitty_keyboard_flags: Arc, content_seq: Arc, @@ -2304,6 +2306,7 @@ impl PaneRuntime { let compression = TerminalCompressionTask::spawn(pane_id, terminal.clone()); let child_pid = Arc::new(AtomicU32::new(child_pid)); let reported_cwd = Arc::new(Mutex::new(None)); + let cwd_process_exited = Arc::new(AtomicBool::new(false)); let kitty_keyboard_flags = Arc::new(AtomicU16::new(keyboard_protocol_flags)); let content_seq = Arc::new(AtomicU64::new(0)); let content_write_lock = Arc::new(Mutex::new(())); @@ -2370,7 +2373,9 @@ impl PaneRuntime { } }); let exit_events = events.clone(); + let cwd_process_exited = cwd_process_exited.clone(); let on_reader_exit = Box::new(move || { + cwd_process_exited.store(true, Ordering::Release); // Imported handoff panes have no child wait handle, so their exit cause is // unknowable. Checkpoint conservatively; normal autosave settles clean exits. let _ = rt.block_on(exit_events.send(AppEvent::PaneDied { @@ -2405,6 +2410,8 @@ impl PaneRuntime { current_size: Cell::new((rows, cols, cell_width_px, cell_height_px)), child_pid, reported_cwd, + persistence_cwd: Mutex::new(None), + cwd_process_exited, child_wait_completed: None, kitty_keyboard_flags, content_seq, @@ -2981,6 +2988,8 @@ impl PaneRuntime { current_size: Cell::new((rows, cols, 0, 0)), child_pid, reported_cwd, + persistence_cwd: Mutex::new(None), + cwd_process_exited: child_wait_completed.clone(), child_wait_completed: Some(child_wait_completed), kitty_keyboard_flags, content_seq, @@ -3491,6 +3500,27 @@ impl PaneRuntime { crate::platform::process_cwd(pid) } + pub fn cwd_for_persistence(&self) -> Option { + let pid = self.child_pid.load(Ordering::Acquire); + let exited = self.cwd_process_exited.load(Ordering::Acquire); + if let Some(cwd) = (!exited) + .then(|| crate::platform::process_cwd(pid)) + .flatten() + .filter(|cwd| cwd.is_absolute()) + { + // Persistence observations must not change OSC authority or follow-cwd behavior. + if let Ok(mut known) = self.persistence_cwd.lock() { + *known = Some(cwd.clone()); + } + return Some(cwd); + } + self.persistence_cwd + .lock() + .ok() + .and_then(|cwd| cwd.clone()) + .or_else(|| self.reported_cwd.lock().ok().and_then(|cwd| cwd.clone())) + } + pub fn child_pid(&self) -> Option { let pid = self.child_pid.load(Ordering::Acquire); (pid > 0).then_some(pid) @@ -3661,6 +3691,8 @@ impl PaneRuntime { current_size: Cell::new((rows, cols, 0, 0)), child_pid: Arc::new(AtomicU32::new(0)), reported_cwd: Arc::new(Mutex::new(None)), + persistence_cwd: Mutex::new(None), + cwd_process_exited: Arc::new(AtomicBool::new(false)), child_wait_completed: None, kitty_keyboard_flags: Arc::new(AtomicU16::new(0)), content_seq: Arc::new(AtomicU64::new(0)), @@ -4592,6 +4624,23 @@ mod tests { assert!(runtime.handoff_history_ansi().is_none()); } + #[cfg(unix)] + #[tokio::test] + async fn ended_handoff_keeps_persistence_cwd_when_pid_is_reused() { + let (runtime, _rx) = PaneRuntime::test_with_channel(80, 24); + assert!(runtime.child_wait_completed.is_none()); + let saved = std::env::temp_dir().join("saved-handoff-cwd"); + *runtime.persistence_cwd.lock().unwrap() = Some(saved.clone()); + // A different live process now owns the imported shell's numeric PID. + runtime + .child_pid + .store(std::process::id(), Ordering::Release); + runtime.cwd_process_exited.store(true, Ordering::Release); + assert_eq!(runtime.cwd_for_persistence(), Some(saved)); + *runtime.persistence_cwd.lock().unwrap() = None; + assert_eq!(runtime.cwd_for_persistence(), None); + } + #[cfg(unix)] #[tokio::test] async fn handoff_runtime_state_captures_terminal_input_and_title_state() { @@ -4749,6 +4798,8 @@ mod tests { )); let compression = TerminalCompressionTask::spawn(pane_id, terminal.clone()); let runtime = PaneRuntime { + cwd_process_exited: Arc::new(AtomicBool::new(false)), + persistence_cwd: Mutex::new(None), pane_id, terminal, io: PaneRuntimeIo::TestChannel { @@ -4786,6 +4837,8 @@ mod tests { )); let compression = TerminalCompressionTask::spawn(pane_id, terminal.clone()); let runtime = PaneRuntime { + cwd_process_exited: Arc::new(AtomicBool::new(false)), + persistence_cwd: Mutex::new(None), pane_id, terminal, io: PaneRuntimeIo::TestChannel { -- cgit v1.2.3