use std::io::{self, Write};
use ohno::AppError;
use crate::attach::attach;
use crate::detect::{DetectOutcome, auto_detect};
use crate::gc::{live_sessions, require_live_session};
use crate::list_fmt::format_list;
use crate::pal::local_console::LocalConsole;
use crate::pal::processes::Processes;
use crate::pal::session_store::SessionStore;
use crate::pal::transport::Transport;
use crate::session_id::SessionId;
use crate::session_record::SessionRecord;
use crate::trace::{Trace, trace};
use crate::types::Outcome;
use crate::wall_clock::unix_now_ms;
use crate::{
CanonicalizeError, CurrentDirectoryError, NoLiveSessionsError, PalFailedError,
PromptFailedError, SessionNotFoundError, parse_prompted_id,
};
pub(crate) fn execute<S, P, T, C>(
store: &S,
processes: &P,
transport: &T,
console: &C,
id: Option<SessionId>,
trace: Trace,
) -> Result<Outcome, AppError>
where
S: SessionStore,
P: Processes,
T: Transport + Clone + Send + Sync + 'static,
C: LocalConsole + Clone + Send + Sync + 'static,
{
let record = match id {
Some(id) => {
trace!(
trace,
"session {id} was named on the command line, so auto-detect is skipped"
);
require_live_session(store, processes, id, trace)?
}
None => {
let live = live_sessions(store, processes, trace)?;
let id = resolve_auto(store, console, &live, trace)?;
live.into_iter()
.find(|record| record.session_id() == id)
.ok_or_else(|| AppError::from(SessionNotFoundError::for_id(id)))?
}
};
trace!(
trace,
"attaching to session {} on {}",
record.session_id(),
record.pipe_name
);
attach(transport, console, &record.pipe_name, record.session_id())
}
fn resolve_auto<S, C>(
store: &S,
console: &C,
live: &[SessionRecord],
trace: Trace,
) -> Result<SessionId, AppError>
where
S: SessionStore,
C: LocalConsole,
{
let cwd = store
.current_dir()
.map_err(|_error| CurrentDirectoryError::new())?;
let cwd = store
.canonicalize(&cwd)
.map_err(|_error| CanonicalizeError::new(cwd))?;
match auto_detect(live, &cwd, trace) {
DetectOutcome::None => Err(NoLiveSessionsError::new().into()),
DetectOutcome::Unique(id) => Ok(id),
DetectOutcome::Ambiguous(sessions) => {
println!("{}", format_list(&sessions, unix_now_ms()));
if !console.stdin_is_terminal() {
return Err(PromptFailedError::new().into());
}
print!("Session id to resume: ");
io::stdout()
.flush()
.map_err(|_error| PalFailedError::new())?;
let line = console
.read_prompt_line()
.map_err(|_error| PromptFailedError::new())?;
parse_prompted_id(&line).map_err(AppError::from)
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::path::PathBuf;
use std::thread;
use super::*;
use crate::pal::error::{PalError, PalErrorKind};
use crate::pal::local_console::{LocalConsoleFacade, MockLocalConsole};
use crate::pal::processes::{MockProcesses, ProcessLiveness};
use crate::pal::pseudoconsole::WindowSize;
use crate::pal::session_store::{FsSessionStore, SessionStore};
use crate::pal::transport::MemoryTransport;
use crate::protocol::Message;
use crate::session_id::SessionId;
use crate::session_record::ProcessIdentity;
use crate::{InvalidSessionIdError, PromptFailedError};
fn publish_ambiguous_sessions(store: &FsSessionStore) {
for name in ["one", "two"] {
let id = store.allocate_id(&ProcessIdentity::for_test(1)).unwrap();
store
.publish(&SessionRecord {
id: id.get(),
supervisor_pid: 10,
supervisor_creation_time: 100,
pipe_name: name.to_string(),
launch_directory: PathBuf::from(format!("/nowhere/{name}")),
command: vec!["app.exe".to_string()],
started_at_unix_ms: 1,
attached: false,
})
.unwrap();
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn no_live_sessions_fails() {
let dir = tempfile::TempDir::new().unwrap();
let store = FsSessionStore::new(dir.path().to_path_buf());
let processes = MockProcesses::new();
let transport = MemoryTransport::new();
let console = LocalConsoleFacade::from_mock(MockLocalConsole::new());
execute(
&store,
&processes,
&transport,
&console,
None,
Trace::default(),
)
.unwrap_err();
}
#[test]
#[cfg_attr(miri, ignore)]
fn missing_explicit_id_fails() {
let dir = tempfile::TempDir::new().unwrap();
let store = FsSessionStore::new(dir.path().to_path_buf());
let processes = MockProcesses::new();
let transport = MemoryTransport::new();
let console = LocalConsoleFacade::from_mock(MockLocalConsole::new());
let id = SessionId::from_u32(9).unwrap();
execute(
&store,
&processes,
&transport,
&console,
Some(id),
Trace::default(),
)
.unwrap_err();
}
#[test]
#[cfg_attr(miri, ignore)]
fn unique_launch_directory_attaches() {
testing::with_watchdog(|| {
let dir = tempfile::TempDir::new().unwrap();
let store = FsSessionStore::new(dir.path().to_path_buf());
let cwd = store.current_dir().unwrap();
let launch_directory = store.canonicalize(&cwd).unwrap();
let id = store.allocate_id(&ProcessIdentity::for_test(1)).unwrap();
let pipe = "resume-unique";
store
.publish(&SessionRecord {
id: id.get(),
supervisor_pid: 10,
supervisor_creation_time: 100,
pipe_name: pipe.to_string(),
launch_directory,
command: vec!["app.exe".to_string()],
started_at_unix_ms: 1,
attached: false,
})
.unwrap();
let mut processes = MockProcesses::new();
processes
.expect_probe()
.returning(|_| ProcessLiveness::Live);
let transport = MemoryTransport::new();
let listener = transport.listen(pipe).unwrap();
thread::spawn({
let transport = transport.clone();
move || {
let conn = transport.accept(listener).unwrap();
_ = transport.recv(conn);
_ = transport.send(conn, &Message::Attached { session_id: id });
_ = transport.send(conn, &Message::AppExited { status: 0 });
}
});
let mut console = MockLocalConsole::new();
console.expect_has_console().return_const(true);
console.expect_disable_ctrl_c_handler().returning(|| Ok(()));
console.expect_enter_raw_relay().returning(|| Ok(()));
console.expect_leave_raw_relay().returning(|| Ok(()));
console
.expect_window_size()
.returning(|| Ok(WindowSize { cols: 80, rows: 24 }));
console.expect_read_input().returning(|| {
thread::park();
Err(PalError::new(PalErrorKind::Disconnected))
});
console.expect_write_output().returning(|_| Ok(()));
let console = LocalConsoleFacade::from_mock(console);
let outcome = execute(
&store,
&processes,
&transport,
&console,
None,
Trace::default(),
)
.unwrap();
assert!(matches!(outcome, Outcome::AppExit(0)));
});
}
#[test]
#[cfg_attr(miri, ignore)]
fn ambiguous_without_terminal_does_not_prompt() {
let dir = tempfile::TempDir::new().unwrap();
let store = FsSessionStore::new(dir.path().to_path_buf());
publish_ambiguous_sessions(&store);
let mut processes = MockProcesses::new();
processes
.expect_probe()
.returning(|_| ProcessLiveness::Live);
let transport = MemoryTransport::new();
let mut console = MockLocalConsole::new();
console.expect_stdin_is_terminal().return_const(false);
let console = LocalConsoleFacade::from_mock(console);
let error = execute(
&store,
&processes,
&transport,
&console,
None,
Trace::default(),
)
.unwrap_err();
assert!(error.find_source::<PromptFailedError>().is_some());
}
#[test]
#[cfg_attr(miri, ignore)]
fn ambiguous_with_terminal_reads_selection() {
let dir = tempfile::TempDir::new().unwrap();
let store = FsSessionStore::new(dir.path().to_path_buf());
publish_ambiguous_sessions(&store);
let mut processes = MockProcesses::new();
processes
.expect_probe()
.returning(|_| ProcessLiveness::Live);
let transport = MemoryTransport::new();
let mut console = MockLocalConsole::new();
console.expect_stdin_is_terminal().return_const(true);
console
.expect_read_prompt_line()
.returning(|| Ok("not a number".to_string()));
let console = LocalConsoleFacade::from_mock(console);
let error = execute(
&store,
&processes,
&transport,
&console,
None,
Trace::default(),
)
.unwrap_err();
assert!(error.find_source::<InvalidSessionIdError>().is_some());
}
}