Repository navigation
fix(tmux): close panes on their owning server #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
60624a1
08753e7
b7a9f4a
55ebca1
324cfc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -200,6 +200,7 @@ fn capture_context() -> serde_json::Map<String, serde_json::Value> { | |
| "GUAKE_TAB_UUID", | ||
| "WT_SESSION", | ||
| "ConEmuHWND", | ||
| "TMUX", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Capturing Capture the socket in Knowledge Base Used: |
||
| "TMUX_PANE", | ||
| "STY", | ||
| "ZELLIJ_SESSION_NAME", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,6 +98,59 @@ fn is_wezterm_argv(argv: &[String]) -> bool { | |
| .is_some_and(|stem| stem.eq_ignore_ascii_case("wezterm")) | ||
| } | ||
|
|
||
| /// True if `argv` runs the tmux CLI (`tmux` or a full path). | ||
| fn is_tmux_argv(argv: &[String]) -> bool { | ||
| argv.first() | ||
| .and_then(|bin| std::path::Path::new(bin).file_stem()) | ||
| .and_then(|stem| stem.to_str()) | ||
| .is_some_and(|stem| stem == "tmux") | ||
| } | ||
|
|
||
| /// True if `pane_id` exists on the tmux server named by `tmux` (a pane's | ||
| /// `TMUX` value) and that server is still the process that started the pane. | ||
| /// False if the server doesn't answer within `TERMINAL_CLOSE_TIMEOUT`, so a | ||
| /// hung server can't keep kill from signalling the process. | ||
| fn tmux_pane_on_server(tmux_bin: &str, tmux: &str, pane_id: &str) -> bool { | ||
| let Some(server_pid) = tmux.split(',').nth(1).filter(|pid| !pid.is_empty()) else { | ||
| return false; | ||
| }; | ||
| let Ok(mut child) = Command::new(tmux_bin) | ||
| .args(["display-message", "-p", "-t", pane_id, "#{pid}"]) | ||
| .env("TMUX", tmux) | ||
| .stdin(std::process::Stdio::null()) | ||
| .stdout(std::process::Stdio::piped()) | ||
| .stderr(std::process::Stdio::null()) | ||
| .spawn() | ||
| else { | ||
| return false; | ||
| }; | ||
| let started = Instant::now(); | ||
| loop { | ||
| match child.try_wait() { | ||
| Ok(Some(status)) if status.success() => break, | ||
| Ok(None) if started.elapsed() < TERMINAL_CLOSE_TIMEOUT => { | ||
| std::thread::sleep(Duration::from_millis(25)); | ||
| } | ||
| Ok(None) => { | ||
| let _ = child.kill(); | ||
| let _ = child.wait(); | ||
| eprintln!( | ||
| "Timed out after {}s asking tmux about pane {pane_id}; not closing it", | ||
| TERMINAL_CLOSE_TIMEOUT.as_secs() | ||
| ); | ||
| return false; | ||
| } | ||
| _ => return false, | ||
| } | ||
| } | ||
| let mut out = String::new(); | ||
| child | ||
| .stdout | ||
| .take() | ||
| .is_some_and(|mut stdout| std::io::Read::read_to_string(&mut stdout, &mut out).is_ok()) | ||
| && out.trim() == server_pid | ||
| } | ||
|
|
||
| /// Terminal info resolved for an instance. | ||
| #[derive(Debug, Clone, Default)] | ||
| pub struct TerminalInfo { | ||
|
|
@@ -111,6 +164,8 @@ pub struct TerminalInfo { | |
| pub herdr_socket_path: String, | ||
| /// The pane's `WEZTERM_UNIX_SOCKET`: which wezterm GUI process owns it. | ||
| pub wezterm_unix_socket: String, | ||
| /// The pane's `TMUX` (`socket,server_pid,session`): which tmux server owns it. | ||
| pub tmux: String, | ||
| } | ||
|
|
||
| /// Result from launch_terminal. | ||
|
|
@@ -2790,6 +2845,7 @@ pub fn close_terminal_pane(pid: u32, info: &TerminalInfo) -> PaneCloseResult { | |
| zellij_session_name, | ||
| herdr_socket_path, | ||
| wezterm_unix_socket, | ||
| tmux, | ||
| } = info; | ||
| let failed_without_command = || PaneCloseResult { | ||
| closed: false, | ||
|
|
@@ -2886,10 +2942,18 @@ pub fn close_terminal_pane(pid: u32, info: &TerminalInfo) -> PaneCloseResult { | |
| // environment would close another session's pane. | ||
| // Same for wezterm: pane ids are per GUI process, and without the socket | ||
| // `wezterm cli` talks to the most recently started GUI. | ||
| // And tmux: pane ids are per server and the CLI picks the server from | ||
| // `TMUX`. They also restart at %0 with each server, so a server restarted | ||
| // on the same socket may hold an unrelated pane with this id. | ||
| let close_env: Vec<(&str, &str)> = if is_herdr_argv(&argv) && !herdr_socket_path.is_empty() { | ||
| vec![("HERDR_SOCKET_PATH", herdr_socket_path.as_str())] | ||
| } else if is_wezterm_argv(&argv) && !wezterm_unix_socket.is_empty() { | ||
| vec![("WEZTERM_UNIX_SOCKET", wezterm_unix_socket.as_str())] | ||
| } else if is_tmux_argv(&argv) && !tmux.is_empty() { | ||
| if !tmux_pane_on_server(&argv[0], tmux, effective_pane_id) { | ||
| return failed_without_command(); | ||
|
Comment on lines
+2953
to
+2954
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the caller cannot reach the tmux socket, the query fails and returns Keep an error or retry command for query failures instead of treating them like a confirmed stale owner. Knowledge Base Used: Command workflows |
||
| } | ||
| vec![("TMUX", tmux.as_str())] | ||
| } else { | ||
| Vec::new() | ||
|
Comment on lines
+2952
to
2958
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Instances saved before this change lack Refuse to close without a saved owner unless the close command explicitly selects a server. Knowledge Base Used: Runtime environment and process setup |
||
| }; | ||
|
|
@@ -3093,6 +3157,10 @@ pub fn resolve_terminal_info( | |
| .and_then(|e| e.get("WEZTERM_UNIX_SOCKET").and_then(|v| v.as_str())) | ||
| .unwrap_or("") | ||
| .to_string(); | ||
| info.tmux = lc_env | ||
| .and_then(|e| e.get("TMUX").and_then(|v| v.as_str())) | ||
| .unwrap_or("") | ||
| .to_string(); | ||
| } | ||
|
|
||
| // Legacy kitty launches may have pane/socket metadata but no persisted preset. | ||
|
|
@@ -4450,6 +4518,35 @@ mod tests { | |
| assert_eq!(info.herdr_socket_path, "/run/herdr/work.sock"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_resolve_terminal_info_reads_tmux_from_env_snapshot() { | ||
| let info = resolve_terminal_info( | ||
| Some("tmux-split"), | ||
| Some(r#"{"pane_id":"%1","env":{"TMUX":"/tmp/tmux-1000/other,4242,0"}}"#), | ||
| ); | ||
| assert_eq!(info.pane_id, "%1"); | ||
| assert_eq!(info.tmux, "/tmp/tmux-1000/other,4242,0"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_is_tmux_argv() { | ||
| assert!(is_tmux_argv(&["tmux".to_string()])); | ||
| assert!(is_tmux_argv(&["/opt/homebrew/bin/tmux".to_string()])); | ||
| assert!(!is_tmux_argv(&["tmuxinator".to_string()])); | ||
| assert!(!is_tmux_argv(&[])); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_tmux_pane_on_server_needs_server_pid() { | ||
| // Without the server pid the pane's server can't be confirmed. | ||
| assert!(!tmux_pane_on_server("tmux", "/tmp/tmux-1000/default", "%1")); | ||
| assert!(!tmux_pane_on_server( | ||
| "tmux", | ||
| "/tmp/tmux-1000/default,,0", | ||
| "%1" | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_is_herdr_argv_matches_bare_name_and_paths() { | ||
| let is = |bin: &str| is_herdr_argv(&[bin.to_string(), "pane".to_string()]); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.