Decompose oversized Rust files by responsibility
This commit is contained in:
parent
03bfe92ca0
commit
ee9201b057
3 changed files with 18648 additions and 18513 deletions
|
|
@ -0,0 +1,507 @@
|
|||
fn assert_no_guest_action_after(factory: &MockFactory, marker: &str) {
|
||||
let log = factory.log.lock().unwrap();
|
||||
let marker = log.iter().position(|action| action == marker).unwrap();
|
||||
assert!(
|
||||
log[marker + 1..]
|
||||
.iter()
|
||||
.all(|action| !action.starts_with("query:") && !action.starts_with("signal:"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquisition_refuses_authority_outside_the_committed_sandbox() {
|
||||
let session = id(6);
|
||||
let (fixture, _factory, mut engine) = setup(&[session]);
|
||||
let mut request = acquisition(session);
|
||||
request.host_coordinator = process(700);
|
||||
assert!(matches!(
|
||||
engine.acquire(&request),
|
||||
Err(TerminalError::AuthorityMismatch)
|
||||
));
|
||||
assert!(
|
||||
fixture
|
||||
.store
|
||||
.snapshot()
|
||||
.unwrap()
|
||||
.session_terminals
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn acquisition_requires_attached_current_session() {
|
||||
for phase in [
|
||||
SessionPhase::New,
|
||||
SessionPhase::Waiting,
|
||||
SessionPhase::Registering,
|
||||
] {
|
||||
let session = id(6);
|
||||
let fixture = Fixture::new(&[]);
|
||||
fixture.insert_session(session, phase);
|
||||
let (_factory, mut engine) = engine_for(&fixture);
|
||||
assert!(matches!(
|
||||
engine.acquire(&acquisition(session)),
|
||||
Err(TerminalError::AuthorityMismatch)
|
||||
));
|
||||
assert!(
|
||||
fixture
|
||||
.store
|
||||
.snapshot()
|
||||
.unwrap()
|
||||
.session_terminals
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn durable_detach_failure_and_sandbox_release_revoke_active_terminal() {
|
||||
for revoke in ["closed", "failed", "sandbox-release"] {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
match revoke {
|
||||
"closed" => fixture.set_session_phase(session, SessionPhase::Closed),
|
||||
"failed" => fixture.set_session_phase(session, SessionPhase::Failed),
|
||||
"sandbox-release" => fixture.release_sandbox(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
session_id: session,
|
||||
bytes: b"refused".to_vec(),
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::AuthorityMismatch)
|
||||
));
|
||||
assert!(!engine.contains(session));
|
||||
assert_no_group_signal(&factory);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_container_hc_and_authority_read_mismatch_use_host_only_release() {
|
||||
for mismatch in ["domain", "container", "host-coordinator"] {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
let request = &mut engine.active.get_mut(&session).unwrap().request;
|
||||
match mismatch {
|
||||
"domain" => request.domain_id = DomainId::new(bytes(80)),
|
||||
"container" => request.container_id = ContainerId::new(bytes(81)),
|
||||
"host-coordinator" => request.host_coordinator = process(800),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
session_id: session,
|
||||
bytes: b"refused".to_vec(),
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::AuthorityMismatch)
|
||||
));
|
||||
assert_no_group_signal(&factory);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
}
|
||||
|
||||
let session = id(6);
|
||||
let fixture = Fixture::new(&[session]);
|
||||
let store = Arc::new(FailingRunningStore::new(Arc::clone(&fixture.store)));
|
||||
let (factory, mut engine) = engine_for_store(store.clone());
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
store.arm_snapshot_failure();
|
||||
assert!(matches!(
|
||||
engine.poll(session, GenerationId::new(7)),
|
||||
Err(TerminalError::Store(DomainStoreError::Io(_)))
|
||||
));
|
||||
assert_no_group_signal(&factory);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_close_and_loss_revalidate_before_guest_cleanup() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
fixture.release_sandbox();
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_no_group_signal(&factory);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
fixture.add_newer_generation();
|
||||
engine
|
||||
.lose(session, GenerationId::new(7), TerminalLoss::PtyLost)
|
||||
.unwrap();
|
||||
assert_no_group_signal(&factory);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grace_wait_revocation_and_read_error_stop_all_later_guest_actions() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
let store = Arc::clone(&fixture.store);
|
||||
let log = Arc::clone(&factory.log);
|
||||
factory.state(0).lock().unwrap().group_query_hook = Some((
|
||||
2,
|
||||
Arc::new(move || {
|
||||
log.lock().unwrap().push("revoke:sandbox".into());
|
||||
release_sandbox(&store);
|
||||
}),
|
||||
));
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_no_guest_action_after(&factory, "revoke:sandbox");
|
||||
let state = factory.state(0);
|
||||
assert!(state.lock().unwrap().resources.exec_reaped);
|
||||
assert!(!state.lock().unwrap().resources.pty_open);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
|
||||
let fixture = Fixture::new(&[session]);
|
||||
let store = Arc::new(FailingRunningStore::new(Arc::clone(&fixture.store)));
|
||||
let (factory, mut engine) = engine_for_store(store.clone());
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
let failing_store = Arc::clone(&store);
|
||||
let log = Arc::clone(&factory.log);
|
||||
factory.state(0).lock().unwrap().group_query_hook = Some((
|
||||
2,
|
||||
Arc::new(move || {
|
||||
log.lock().unwrap().push("revoke:read-error".into());
|
||||
failing_store.arm_snapshot_failure();
|
||||
}),
|
||||
));
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_no_guest_action_after(&factory, "revoke:read-error");
|
||||
let state = factory.state(0);
|
||||
assert!(state.lock().unwrap().resources.exec_reaped);
|
||||
assert!(!state.lock().unwrap().resources.pty_open);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_caller_and_superseded_generation_refuse_without_cross_generation_action() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(8),
|
||||
SessionTerminalEvent::Signal(TerminalSignal {
|
||||
session_id: session,
|
||||
signal: TerminalSignalKind::Interrupt,
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::StaleGeneration)
|
||||
));
|
||||
assert_no_group_signal(&factory);
|
||||
assert!(!engine.contains(session));
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
fixture.add_newer_generation();
|
||||
assert!(matches!(
|
||||
engine.poll(session, GenerationId::new(7)),
|
||||
Err(TerminalError::AuthorityMismatch)
|
||||
));
|
||||
assert!(!engine.contains(session));
|
||||
assert_no_group_signal(&factory);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_and_terminal_signals_use_the_exact_group_map() {
|
||||
let session = id(6);
|
||||
let (_fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Resize(TerminalResize {
|
||||
session_id: session,
|
||||
rows: NonZeroU16::new(40).unwrap(),
|
||||
columns: NonZeroU16::new(120).unwrap(),
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
for (kind, expected) in [
|
||||
(TerminalSignalKind::Interrupt, "signal:INT"),
|
||||
(TerminalSignalKind::Quit, "signal:QUIT"),
|
||||
(TerminalSignalKind::Suspend, "signal:TSTP"),
|
||||
] {
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Signal(TerminalSignal {
|
||||
session_id: session,
|
||||
signal: kind,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(factory.log.lock().unwrap().last().unwrap(), expected);
|
||||
}
|
||||
assert_eq!(
|
||||
&factory.log.lock().unwrap()[..2],
|
||||
&["resize:40x120", "signal:WINCH"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_guest_identity_refuses_numeric_group_action() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
factory.state(0).lock().unwrap().guest.identity_mismatch = true;
|
||||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Signal(TerminalSignal {
|
||||
session_id: session,
|
||||
signal: TerminalSignalKind::Interrupt,
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::GuestIdentityMismatch)
|
||||
));
|
||||
assert!(factory.log.lock().unwrap().is_empty());
|
||||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::GuestIdentityMismatch)
|
||||
));
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_leader_with_live_group_retains_release_until_group_is_empty() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
{
|
||||
let state = factory.state(0);
|
||||
let mut state = state.lock().unwrap();
|
||||
state.guest.leader_present = false;
|
||||
state.guest.group_present = true;
|
||||
}
|
||||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::GuestIdentityMismatch)
|
||||
));
|
||||
let state = factory.state(0);
|
||||
assert!(state.lock().unwrap().resources.exec_reaped);
|
||||
assert!(!state.lock().unwrap().resources.pty_open);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::ReleaseIntent
|
||||
);
|
||||
|
||||
state.lock().unwrap().guest.group_present = false;
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::Clean
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn running_store_failures_release_every_live_partial_handle() {
|
||||
for point in [
|
||||
StoreFailurePoint::Snapshot,
|
||||
StoreFailurePoint::Write,
|
||||
StoreFailurePoint::Commit,
|
||||
] {
|
||||
for persistent in [false, true] {
|
||||
let session = id(6);
|
||||
let fixture = Fixture::new(&[session]);
|
||||
let store = Arc::new(FailingRunningStore::new(Arc::clone(&fixture.store)));
|
||||
let recorder = HostCoordinatorTerminalRecorder::with_store(store.clone());
|
||||
let request = acquisition(session);
|
||||
recorder
|
||||
.put(
|
||||
terminal_snapshot(
|
||||
&request,
|
||||
SessionTerminalState::Intent,
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
None,
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let pty = PtyIdentity {
|
||||
master_fd: FileDescriptor::new(9),
|
||||
device: DeviceId::new(10),
|
||||
inode: InodeId::new(11),
|
||||
};
|
||||
recorder
|
||||
.put(
|
||||
staged_terminal(
|
||||
&request,
|
||||
pty,
|
||||
StagedProcessOwnership::v1(
|
||||
request.domain_id,
|
||||
request.generation_id,
|
||||
request.coordinator_id,
|
||||
lazaret_core::records::HostProcessOwnershipSlot::SessionTerminalExecClient(
|
||||
request.session_id,
|
||||
),
|
||||
process(90),
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let guest = GuestProcessIdentity {
|
||||
leader_pid: GuestProcessId::new(41),
|
||||
leader_start_time: GuestProcessStartTimeTicks::new(101),
|
||||
process_group_id: GuestProcessGroupId::new(41),
|
||||
};
|
||||
let running = terminal_snapshot(
|
||||
&request,
|
||||
SessionTerminalState::Running,
|
||||
PersistedHostProcessOwnership::committed(request.host_coordinator),
|
||||
Some(pty),
|
||||
PersistedHostProcessOwnership::committed(process(90)),
|
||||
Some(guest),
|
||||
)
|
||||
.unwrap();
|
||||
let state = Arc::new(Mutex::new(MockState {
|
||||
output: VecDeque::new(),
|
||||
log: Arc::new(Mutex::new(Vec::new())),
|
||||
guest: MockGuestState {
|
||||
group_present: true,
|
||||
leader_present: true,
|
||||
identity_mismatch: false,
|
||||
clears_after_signal: "HUP",
|
||||
},
|
||||
resources: MockResourceState {
|
||||
exec_exited: false,
|
||||
exec_reaped: false,
|
||||
pty_open: true,
|
||||
},
|
||||
group_queries: 0,
|
||||
group_query_hook: None,
|
||||
}));
|
||||
let mut process_handle = MockProcess {
|
||||
state: Arc::clone(&state),
|
||||
};
|
||||
store.arm(point, persistent);
|
||||
let error = commit_running_or_cleanup(
|
||||
&recorder,
|
||||
&request,
|
||||
running,
|
||||
pty,
|
||||
process(90),
|
||||
guest,
|
||||
&mut process_handle,
|
||||
)
|
||||
.unwrap_err();
|
||||
match error {
|
||||
TerminalError::AcquisitionCleanup { cleanup: None, .. } if !persistent => {}
|
||||
TerminalError::AcquisitionCleanup {
|
||||
cleanup: Some(_), ..
|
||||
} if persistent => {}
|
||||
error => panic!("unexpected cleanup result: {error}"),
|
||||
}
|
||||
let state = state.lock().unwrap();
|
||||
assert_eq!(state.guest.group_present, persistent);
|
||||
assert_eq!(
|
||||
state
|
||||
.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|action| action.starts_with("signal:")),
|
||||
!persistent
|
||||
);
|
||||
assert!(state.resources.exec_exited);
|
||||
assert!(state.resources.exec_reaped);
|
||||
assert!(!state.resources.pty_open);
|
||||
drop(state);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
if persistent {
|
||||
SessionTerminalState::Intent
|
||||
} else {
|
||||
SessionTerminalState::Clean
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
142
crates/lazaret-host-coordinator/src/terminal/tests/output.rs
Normal file
142
crates/lazaret-host-coordinator/src/terminal/tests/output.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#[test]
|
||||
fn merged_output_is_ordered_and_prompt_route_isolated_by_session() {
|
||||
let first = id(6);
|
||||
let second = id(7);
|
||||
let (fixture, factory, mut engine) = setup(&[first, second]);
|
||||
engine.acquire(&acquisition(first)).unwrap();
|
||||
engine.acquire(&acquisition(second)).unwrap();
|
||||
factory
|
||||
.state(0)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.output
|
||||
.extend([b"one".to_vec(), b"two".to_vec()]);
|
||||
factory
|
||||
.state(1)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.output
|
||||
.push_back(b"other".to_vec());
|
||||
fixture.set_session_phase(first, SessionPhase::Prompting);
|
||||
engine
|
||||
.set_route(first, GenerationId::new(7), TerminalRoute::Prompt)
|
||||
.unwrap();
|
||||
assert!(engine.poll(first, GenerationId::new(7)).unwrap().is_empty());
|
||||
assert_eq!(
|
||||
engine.poll(second, GenerationId::new(7)).unwrap(),
|
||||
vec![SessionTerminalMessage::Output(TerminalOutput {
|
||||
session_id: second,
|
||||
bytes: b"other".to_vec(),
|
||||
})]
|
||||
);
|
||||
fixture.set_session_phase(first, SessionPhase::Attached);
|
||||
engine
|
||||
.set_route(first, GenerationId::new(7), TerminalRoute::Sandbox)
|
||||
.unwrap();
|
||||
let mut merged = Vec::new();
|
||||
for _ in 0..2 {
|
||||
let messages = engine.poll(first, GenerationId::new(7)).unwrap();
|
||||
let [SessionTerminalMessage::Output(output)] = messages.as_slice() else {
|
||||
panic!("expected one merged output message");
|
||||
};
|
||||
merged.extend_from_slice(&output.bytes);
|
||||
}
|
||||
assert_eq!(merged, b"onetwo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_cancels_route_then_bounds_drain_and_emits_one_final_exit() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
factory.state(0).lock().unwrap().guest.clears_after_signal = "KILL";
|
||||
factory
|
||||
.state(0)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.output
|
||||
.extend((0..10).map(|_| vec![b'x'; MAX_OUTPUT_BYTES]));
|
||||
let messages = engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let bytes = messages
|
||||
.iter()
|
||||
.filter_map(|message| match message {
|
||||
SessionTerminalMessage::Output(output) => Some(output.bytes.len()),
|
||||
SessionTerminalMessage::Exit(_) => None,
|
||||
})
|
||||
.sum::<usize>();
|
||||
assert_eq!(bytes, MAX_DRAIN_BYTES);
|
||||
assert!(
|
||||
matches!(messages.last(), Some(SessionTerminalMessage::Exit(exit)) if exit.exit_code() == Some(17))
|
||||
);
|
||||
let log = factory.log.lock().unwrap();
|
||||
assert_eq!(log[0], "cancel-route");
|
||||
assert_eq!(
|
||||
log.iter()
|
||||
.filter(|action| action.starts_with("signal:"))
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>(),
|
||||
["signal:HUP", "signal:TERM", "signal:KILL"]
|
||||
);
|
||||
drop(log);
|
||||
assert!(!engine.contains(session));
|
||||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
session_id: session,
|
||||
bytes: b"reattach".to_vec(),
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::UnknownSession)
|
||||
));
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::Clean
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnect_component_loss_and_hard_hc_loss_never_reattach() {
|
||||
for loss in [
|
||||
TerminalLoss::ClientDisconnected,
|
||||
TerminalLoss::PtyLost,
|
||||
TerminalLoss::ExecClientLost,
|
||||
] {
|
||||
let session = id(6);
|
||||
let (_fixture, _factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
let messages = engine.lose(session, GenerationId::new(7), loss).unwrap();
|
||||
assert_eq!(
|
||||
messages.is_empty(),
|
||||
loss == TerminalLoss::ClientDisconnected
|
||||
);
|
||||
assert!(!engine.contains(session));
|
||||
}
|
||||
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
assert!(
|
||||
engine
|
||||
.lose(
|
||||
session,
|
||||
GenerationId::new(7),
|
||||
TerminalLoss::HostCoordinatorLost,
|
||||
)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(!engine.contains(session));
|
||||
assert_no_group_signal(&factory);
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::Running
|
||||
);
|
||||
}
|
||||
204
crates/lazaret-host-coordinator/src/terminal/tests/prompt.rs
Normal file
204
crates/lazaret-host-coordinator/src/terminal/tests/prompt.rs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn prompt_adapter_retains_resume_resize_before_authenticated_denial() {
|
||||
let generation = GenerationIdentity {
|
||||
agent_profile: "trusted".into(),
|
||||
domain_id: DomainId::new(bytes(1)),
|
||||
generation_id: GenerationId::new(7),
|
||||
configuration_fingerprint: ConfigurationFingerprint::new(bytes(2)),
|
||||
coordinator_id: CoordinatorId::new(bytes(3)),
|
||||
sandbox_id: SandboxId::new(bytes(5)),
|
||||
};
|
||||
let mut trust_anchor = TrustAnchor::create(generation.clone()).unwrap();
|
||||
let publication = trust_anchor.publication_identity();
|
||||
let coordinator_key = trust_anchor.coordinator_protocol_key();
|
||||
let credentials = trust_anchor.take_sandbox_credential_bundle().unwrap();
|
||||
let sandbox_key = SigningKey::from_bytes(
|
||||
credentials
|
||||
.sandbox_private_key
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
let anchor_key = SigningKey::from_bytes(&bytes(61));
|
||||
let binding = SessionBinding {
|
||||
generation_identity: generation,
|
||||
cli_id: ClientId::new(bytes(62)),
|
||||
session_id: SessionId::new(bytes(63)),
|
||||
session_anchor_public_key: anchor_key.verifying_key().to_bytes().to_vec(),
|
||||
};
|
||||
let capability = issue_session_capability(
|
||||
binding.clone(),
|
||||
credentials.coordinator_protocol_credential().credential_id,
|
||||
credentials.sandbox_protocol_credential().credential_id,
|
||||
&coordinator_key,
|
||||
)
|
||||
.unwrap();
|
||||
let authority = VerifiedSession::new(
|
||||
publication.trust_manifest_sha256,
|
||||
credentials.trust_manifest.tls_ca_sha256,
|
||||
&credentials.trust_manifest,
|
||||
binding.clone(),
|
||||
credentials.coordinator_protocol_credential(),
|
||||
credentials.sandbox_protocol_credential(),
|
||||
capability.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let fixture = Fixture::new(&[]);
|
||||
let session_anchor_id =
|
||||
SessionAnchorId::new(Sha256::digest(&binding.session_anchor_public_key).into());
|
||||
let mut session = Session {
|
||||
generation_id: GenerationId::new(7),
|
||||
session_id: binding.session_id,
|
||||
cli_id: binding.cli_id,
|
||||
session_anchor_id,
|
||||
session_phase: SessionPhase::Registering,
|
||||
};
|
||||
fixture
|
||||
.store
|
||||
.apply(&[DomainStoreMutation::PutSession(session.clone())])
|
||||
.unwrap();
|
||||
session.session_phase = SessionPhase::Attached;
|
||||
fixture
|
||||
.store
|
||||
.apply(&[DomainStoreMutation::PutSession(session)])
|
||||
.unwrap();
|
||||
|
||||
let prompt_transport = Arc::new(SessionPromptTransport::new(Arc::clone(&fixture.store)));
|
||||
let factory = Arc::new(MockFactory::default());
|
||||
let mut engine = SessionTerminalEngine::new(
|
||||
Arc::clone(&fixture.store),
|
||||
factory.clone(),
|
||||
prompt_transport.clone(),
|
||||
);
|
||||
engine.acquire(&acquisition(binding.session_id)).unwrap();
|
||||
let request = MailboxRequest::new(
|
||||
binding.clone(),
|
||||
RequestId::new(bytes(64)),
|
||||
capability,
|
||||
credentials.sandbox_protocol_credential().credential_id,
|
||||
vec![b"/usr/bin/tool".to_vec()],
|
||||
"update host metadata".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let signed_request = encode_signed(&request, &sandbox_key).unwrap();
|
||||
let expected_prompt = prompt_transport
|
||||
.queue_signed_request(authority, &signed_request)
|
||||
.unwrap();
|
||||
let engine = Arc::new(Mutex::new(engine));
|
||||
let mut control = RunscInteractiveControlSession {
|
||||
engine: Arc::clone(&engine),
|
||||
prompt_transport: prompt_transport.clone(),
|
||||
generation_id: GenerationId::new(7),
|
||||
session_id: binding.session_id,
|
||||
};
|
||||
let batch = control
|
||||
.exchange(InteractiveSessionEvent::Poll {
|
||||
session_id: binding.session_id,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
batch.messages,
|
||||
vec![InteractiveSessionMessage::Prompt(expected_prompt.clone())]
|
||||
);
|
||||
let resumed_size = TerminalResize {
|
||||
session_id: binding.session_id,
|
||||
rows: NonZeroU16::new(55).unwrap(),
|
||||
columns: NonZeroU16::new(144).unwrap(),
|
||||
pixel_width: 1_440,
|
||||
pixel_height: 900,
|
||||
};
|
||||
control
|
||||
.exchange(InteractiveSessionEvent::Terminal(
|
||||
SessionTerminalEvent::Resize(resumed_size),
|
||||
))
|
||||
.unwrap();
|
||||
let signed_decision = encode_signed(
|
||||
&DeveloperDecision {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: binding.clone(),
|
||||
request_id: expected_prompt.request_id,
|
||||
request_sha256: expected_prompt.request_sha256,
|
||||
decision: DeveloperDecisionKind::Deny,
|
||||
approved_command_argv: Vec::new(),
|
||||
},
|
||||
&anchor_key,
|
||||
)
|
||||
.unwrap();
|
||||
control
|
||||
.exchange(InteractiveSessionEvent::PromptDecision {
|
||||
session_id: binding.session_id,
|
||||
signed_decision: signed_decision.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
let routed = prompt_transport
|
||||
.take_decision(binding.session_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(routed.signed_decision, signed_decision);
|
||||
assert_eq!(routed.decision.decision, DeveloperDecisionKind::Deny);
|
||||
assert_eq!(routed.resumed_size, Some(resumed_size));
|
||||
assert_eq!(
|
||||
factory.log.lock().unwrap().as_slice(),
|
||||
["resize:55x144", "signal:WINCH"]
|
||||
);
|
||||
assert!(
|
||||
!factory
|
||||
.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|event| event == "signal:TSTP")
|
||||
);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.store
|
||||
.snapshot()
|
||||
.unwrap()
|
||||
.sessions
|
||||
.into_iter()
|
||||
.find(|session| session.session_id == binding.session_id)
|
||||
.unwrap()
|
||||
.session_phase,
|
||||
SessionPhase::Attached
|
||||
);
|
||||
engine
|
||||
.lock()
|
||||
.unwrap()
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
session_id: binding.session_id,
|
||||
bytes: b"sandbox-input".to_vec(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn engine_for_store(
|
||||
store: Arc<dyn TerminalDomainStore>,
|
||||
) -> (Arc<MockFactory>, SessionTerminalEngine) {
|
||||
let factory = Arc::new(MockFactory::default());
|
||||
let engine = SessionTerminalEngine {
|
||||
recorder: HostCoordinatorTerminalRecorder::with_store(store),
|
||||
factory: factory.clone(),
|
||||
cancellation: Arc::new(Cancellation(Arc::clone(&factory.log))),
|
||||
active: HashMap::new(),
|
||||
};
|
||||
(factory, engine)
|
||||
}
|
||||
|
||||
fn id(value: u8) -> SessionId {
|
||||
SessionId::new(bytes(value))
|
||||
}
|
||||
|
||||
fn assert_no_group_signal(factory: &MockFactory) {
|
||||
assert!(
|
||||
factory
|
||||
.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|action| !action.starts_with("signal:"))
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue