use std::process::Command;
use crate::core::Error;
use crate::core::Result;
use crate::core::tmux::TmuxTarget;
pub struct ClaudeCodeRestarter;
impl ClaudeCodeRestarter {
pub fn find_claude_processes() -> Vec<u32> {
let output = match Command::new("pgrep").args(["-x", "claude"]).output() {
Ok(out) => out,
Err(e) => {
tracing::info!("pgrep unavailable: {e}; reporting no claude processes");
return Vec::new();
}
};
if !output.status.success() {
return Vec::new();
}
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|l| l.trim().parse::<u32>().ok())
.collect()
}
pub fn restart_in_session(tmux_session: &str, pane_id: Option<&str>) -> Result<()> {
Self::restart_in_session_with(
tmux_session,
pane_id,
crate::daemon::tmux::TmuxDriver::discover,
)
}
fn restart_in_session_with(
tmux_session: &str,
pane_id: Option<&str>,
discover: impl FnOnce() -> Result<crate::daemon::tmux::TmuxDriver>,
) -> Result<()> {
crate::core::tmux::check_session_name(tmux_session)
.map_err(|e| Error::Protocol(format!("restart refused before tmux ran: {e}")))?;
let driver = discover()?;
driver.apply_scrollback_options();
let pane_alive = pane_id.is_some_and(|p| driver.pane_exists(tmux_session, p));
let target = restart_target(tmux_session, pane_id, pane_alive)?;
driver.send_interrupt(&target)?;
std::thread::sleep(std::time::Duration::from_millis(500));
driver.send_line(&target, &crate::daemon::spawn_command::relaunch_command())?;
Ok(())
}
}
fn restart_target(
tmux_session: &str,
pane_id: Option<&str>,
pane_alive: bool,
) -> Result<TmuxTarget> {
match pane_id {
Some(p) if pane_alive => Ok(TmuxTarget::pane(tmux_session, p)),
Some(p) => Err(Error::Protocol(format!(
"recorded pane {p} for session {tmux_session} no longer exists; refusing to \
restart into an unrelated active pane"
))),
None => TmuxTarget::try_session(tmux_session).map_err(|e| Error::Protocol(e.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn restart_target_uses_pane_when_alive() {
let target = restart_target("trusty-mpm-xyz", Some("%6015"), true).expect("pane target");
assert_eq!(target.as_target(), "%6015");
}
#[test]
fn restart_target_refuses_when_pane_gone() {
let err = restart_target("trusty-mpm-xyz", Some("%6015"), false)
.expect_err("a confirmed-gone recorded pane must refuse, not fall back");
let msg = err.to_string();
assert!(msg.contains("%6015"), "error must name the pane: {msg}");
assert!(
msg.contains("trusty-mpm-xyz"),
"error must name the session: {msg}"
);
}
#[test]
fn restart_target_falls_back_to_session_for_legacy_caller() {
let target = restart_target("trusty-mpm-xyz", None, false).expect("session target");
assert_eq!(target.as_target(), "=trusty-mpm-xyz:");
}
#[test]
fn restart_refuses_an_empty_session_name() {
for name in ["", "=", " "] {
assert!(restart_target(name, None, false).is_err(), "{name:?}");
let discovered = std::cell::Cell::new(false);
let err = ClaudeCodeRestarter::restart_in_session_with(name, None, || {
discovered.set(true);
Ok(crate::daemon::tmux::TmuxDriver::with_tmux_path_for_test(
"/nonexistent/tmux-8443-missing",
))
})
.expect_err("an empty session must be refused");
assert!(
!discovered.get(),
"{name:?}: the guard must refuse before any tmux driver is built"
);
assert!(err.to_string().contains("before tmux ran"), "{err}");
}
}
}