use std::sync::Arc;
use tracing::{debug, warn};
use super::manager::ManagedTmuxDriver;
pub struct TmuxSessionGuard {
name: String,
driver: Arc<dyn ManagedTmuxDriver>,
armed: bool,
}
impl TmuxSessionGuard {
pub fn new(name: impl Into<String>, driver: Arc<dyn ManagedTmuxDriver>) -> Self {
Self {
name: name.into(),
driver,
armed: true,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn disarm(&mut self) {
self.armed = false;
}
}
impl std::fmt::Debug for TmuxSessionGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TmuxSessionGuard")
.field("name", &self.name)
.field("armed", &self.armed)
.finish_non_exhaustive()
}
}
impl Drop for TmuxSessionGuard {
fn drop(&mut self) {
if !self.armed {
return;
}
match self.driver.kill_session(&self.name) {
Ok(()) => debug!(name = %self.name, "TmuxSessionGuard reaped owned session on drop"),
Err(e) => warn!(
name = %self.name,
"TmuxSessionGuard: best-effort reap on drop failed (may already be gone): {e}"
),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
use crate::session_manager::manager::ManagedError;
struct RecordingDriver {
kills: Mutex<Vec<String>>,
}
impl RecordingDriver {
fn new() -> Arc<Self> {
Arc::new(Self {
kills: Mutex::new(Vec::new()),
})
}
fn kills(&self) -> Vec<String> {
self.kills.lock().unwrap().clone()
}
}
impl ManagedTmuxDriver for RecordingDriver {
fn create_session(&self, _name: &str, _workdir: &str) -> Result<(), ManagedError> {
Ok(())
}
fn kill_session(&self, name: &str) -> Result<(), ManagedError> {
self.kills.lock().unwrap().push(name.to_owned());
Ok(())
}
fn send_line(&self, _name: &str, _text: &str) -> Result<(), ManagedError> {
Ok(())
}
fn capture(&self, _name: &str, _lines: usize) -> Result<String, ManagedError> {
Ok(String::new())
}
fn list_sessions(&self) -> Result<Vec<String>, ManagedError> {
Ok(Vec::new())
}
}
struct FailingDriver;
impl ManagedTmuxDriver for FailingDriver {
fn create_session(&self, _name: &str, _workdir: &str) -> Result<(), ManagedError> {
Ok(())
}
fn kill_session(&self, name: &str) -> Result<(), ManagedError> {
Err(ManagedError::TmuxUnavailable(format!("boom for {name}")))
}
fn send_line(&self, _name: &str, _text: &str) -> Result<(), ManagedError> {
Ok(())
}
fn capture(&self, _name: &str, _lines: usize) -> Result<String, ManagedError> {
Ok(String::new())
}
fn list_sessions(&self) -> Result<Vec<String>, ManagedError> {
Ok(Vec::new())
}
}
#[test]
fn guard_kills_on_drop_when_armed() {
let driver = RecordingDriver::new();
{
let _guard = TmuxSessionGuard::new("tmpm-armed", driver.clone());
assert!(driver.kills().is_empty(), "no kill before drop");
} assert_eq!(
driver.kills(),
vec!["tmpm-armed".to_string()],
"armed guard must kill its session exactly once on drop"
);
}
#[test]
fn guard_does_not_kill_after_disarm() {
let driver = RecordingDriver::new();
{
let mut guard = TmuxSessionGuard::new("tmpm-disarmed", driver.clone());
guard.disarm();
} assert!(
driver.kills().is_empty(),
"disarmed guard must NOT kill its session on drop (ownership transferred)"
);
}
#[test]
fn guard_kills_only_named_session() {
let driver = RecordingDriver::new();
drop(TmuxSessionGuard::new("tmpm-only-this", driver.clone()));
let kills = driver.kills();
assert_eq!(kills.len(), 1, "exactly one kill");
assert_eq!(kills[0], "tmpm-only-this", "kills only the named session");
}
#[test]
fn guard_drop_never_panics_on_kill_error() {
let driver: Arc<dyn ManagedTmuxDriver> = Arc::new(FailingDriver);
drop(TmuxSessionGuard::new("tmpm-doomed", driver));
}
#[test]
fn guard_exposes_name() {
let driver = RecordingDriver::new();
let mut guard = TmuxSessionGuard::new("tmpm-name-probe", driver);
assert_eq!(guard.name(), "tmpm-name-probe");
guard.disarm(); }
}