use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;
use tempfile::TempDir;
use super::manager::{ManagedError, ManagedTmuxDriver, SessionManager};
use super::pane_identity::PaneIdentity;
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord, StopCause};
use super::tests::{FakeTmuxDriver, bind_pane, seed_record};
const STORE_BOUND: Duration = Duration::from_millis(500);
const PARK_BOUND: Duration = Duration::from_secs(10);
#[derive(Default)]
struct Park {
flags: Mutex<(bool, bool)>,
changed: Condvar,
}
struct StallingDriver {
inner: Arc<FakeTmuxDriver>,
park: Park,
}
impl StallingDriver {
fn wait_entered(&self) -> bool {
let flags = self.park.flags.lock().unwrap();
let (flags, _) = self
.park
.changed
.wait_timeout_while(flags, PARK_BOUND, |f| !f.0)
.unwrap();
flags.0
}
fn release(&self) {
self.park.flags.lock().unwrap().1 = true;
self.park.changed.notify_all();
}
}
impl ManagedTmuxDriver for StallingDriver {
fn create_session(&self, name: &str, workdir: &str) -> Result<(), ManagedError> {
self.inner.create_session(name, workdir)
}
fn kill_session(&self, name: &str) -> Result<(), ManagedError> {
self.inner.kill_session(name)
}
fn send_line(&self, name: &str, text: &str) -> Result<(), ManagedError> {
self.inner.send_line(name, text)
}
fn capture(&self, name: &str, lines: usize) -> Result<String, ManagedError> {
self.inner.capture(name, lines)
}
fn list_sessions(&self) -> Result<Vec<String>, ManagedError> {
self.inner.list_sessions()
}
fn pane_exists(&self, name: &str, pane_id: &str) -> bool {
self.inner.pane_exists(name, pane_id)
}
fn pane_identity(&self, pane_id: &str) -> Result<PaneIdentity, ManagedError> {
self.inner.pane_identity(pane_id)
}
fn capture_pane(
&self,
name: &str,
pane_id: &str,
lines: usize,
) -> Result<String, ManagedError> {
let mut flags = self.park.flags.lock().unwrap();
flags.0 = true;
self.park.changed.notify_all();
let flags = self
.park
.changed
.wait_timeout_while(flags, PARK_BOUND, |f| !f.1)
.unwrap();
drop(flags);
self.inner.capture_pane(name, pane_id, lines)
}
}
async fn stalled_reap_fixture() -> (
TempDir,
Arc<SessionManager>,
Arc<StallingDriver>,
ManagedSessionId,
) {
let dir = TempDir::new().unwrap();
let inner = FakeTmuxDriver::new();
*inner.pane_id_override.lock().unwrap() = Some("%1".into());
let driver = Arc::new(StallingDriver {
inner,
park: Park::default(),
});
let mgr = Arc::new(
SessionManager::new(dir.path(), driver.clone())
.await
.expect("manager"),
);
let id = ManagedSessionId::new();
seed_record(&mgr, &dir, id, ManagedSessionState::Active, false).await;
bind_pane(&mgr, &id).await;
(dir, mgr, driver, id)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn list_not_blocked_by_stalled_runtime_exit_capture_9034() {
let (_dir, mgr, driver, id) = stalled_reap_fixture().await;
let m = Arc::clone(&mgr);
let reap = tokio::spawn(async move { m.mark_runtime_exited_stopped(&id).await });
assert!(driver.wait_entered(), "the reap never reached capture_pane");
let listed = tokio::time::timeout(STORE_BOUND, mgr.list()).await;
driver.release();
let reaped = reap.await.expect("join");
assert!(
listed.is_ok(),
"list() waited {STORE_BOUND:?} behind the reap's stalled capture-pane (#9034)"
);
let reaped = reaped.expect("reap");
assert_eq!(reaped.state, ManagedSessionState::Stopped);
let stored = mgr.get(&id).await.expect("get");
assert_eq!(stored.state, ManagedSessionState::Stopped);
assert!(
stored.scrollback_path.is_some(),
"the capture result was not kept"
);
}
#[derive(Debug, Clone, Copy)]
enum Change {
Stopped,
Removed,
Rebound,
}
async fn apply(mgr: &SessionManager, id: &ManagedSessionId, change: Change) {
let mut store = mgr.store.write().await;
let mut record: SessionRecord = store.cached_get(id).expect("record");
match change {
Change::Stopped => {
record.state = ManagedSessionState::Stopped;
record.stop_cause = Some(StopCause::Deliberate);
}
Change::Removed => {
store.remove(id).await.expect("remove");
return;
}
Change::Rebound => record.pane_id = Some("%2".into()),
}
store.upsert(record).await.expect("upsert");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_record_changed_during_the_runtime_exit_capture_is_not_written_9034() {
for change in [Change::Stopped, Change::Removed, Change::Rebound] {
let (_dir, mgr, driver, id) = stalled_reap_fixture().await;
let generation = mgr.residency_generation();
let m = Arc::clone(&mgr);
let reap = tokio::spawn(async move { m.mark_runtime_exited_stopped(&id).await });
assert!(
driver.wait_entered(),
"{change:?}: the reap never reached capture_pane"
);
let written = tokio::time::timeout(STORE_BOUND, apply(&mgr, &id, change)).await;
driver.release();
let reaped = reap.await.expect("join");
assert!(
written.is_ok(),
"{change:?}: the store guard was held across tmux (#9034)"
);
let after = mgr.get(&id).await;
match change {
Change::Stopped => {
assert!(
matches!(reaped, Err(ManagedError::InvalidState(..))),
"{change:?}: {reaped:?}"
);
let after = after.expect("get");
assert_eq!(after.state, ManagedSessionState::Stopped);
assert!(
matches!(after.stop_cause, Some(StopCause::Deliberate)),
"{change:?}: the reap overwrote the deliberate stop: {:?}",
after.stop_cause
);
}
Change::Removed => {
assert!(
matches!(reaped, Err(ManagedError::SessionNotFound(_))),
"{change:?}: {reaped:?}"
);
assert!(
matches!(after, Err(ManagedError::SessionNotFound(_))),
"{change:?}: the reap resurrected a removed record: {after:?}"
);
}
Change::Rebound => {
assert!(
matches!(reaped, Err(ManagedError::InvalidState(..))),
"{change:?}: {reaped:?}"
);
let after = after.expect("get");
assert_eq!(after.state, ManagedSessionState::Active, "{change:?}");
assert_eq!(after.pane_id.as_deref(), Some("%2"), "{change:?}");
}
}
assert_eq!(
mgr.residency_generation(),
generation,
"{change:?}: a refused reap advanced the residency generation"
);
}
}