Isolate interactive terminal session ownership

This commit is contained in:
ms2k 2026-08-01 00:00:47 +09:00
commit 690738d846
3 changed files with 280 additions and 24 deletions

View file

@ -147,7 +147,6 @@ impl SessionTerminalEngine {
session_id: SessionId,
generation_id: GenerationId,
) -> Result<Vec<SessionTerminalMessage>, TerminalError> {
let authoritative = self.refresh_route(session_id, generation_id)?;
let terminal = self.active_mut(session_id, generation_id)?;
if terminal.process.exec_exited()? {
return self.close(
@ -158,6 +157,8 @@ impl SessionTerminalEngine {
GuestReleaseAuthority::Validate,
);
}
let authoritative = self.refresh_route(session_id, generation_id)?;
let terminal = self.active_mut(session_id, generation_id)?;
if authoritative != TerminalRoute::Sandbox || terminal.releasing {
return Ok(Vec::new());
}
@ -446,6 +447,13 @@ pub(super) fn release_physical(
} else {
guest_authorized = false;
}
if matches!(
failure.as_ref(),
Some(TerminalError::GuestIdentityMismatch | TerminalError::GuestGroupStillPresent)
) {
failure = None;
guest_authorized = false;
}
if let Some(error) = failure {
return Err(error);
}

View file

@ -77,6 +77,7 @@ impl SessionTerminalFactory for RunscSessionTerminalFactory {
target = match slave
.try_clone()
.and_then(|stdin| target.with_target_stdin(stdin))
.map(lazaret_linux::process::TargetCommand::with_controlling_terminal)
{
Ok(target) => target,
Err(error) => {

View file

@ -321,15 +321,20 @@ fn changed_guest_identity_refuses_numeric_group_action() {
Err(TerminalError::GuestIdentityMismatch)
));
assert!(factory.log.lock().unwrap().is_empty());
assert!(matches!(
engine.handle(
let messages = engine
.handle(
GenerationId::new(7),
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
session_id: session,
}),
),
Err(TerminalError::GuestIdentityMismatch)
));
)
.unwrap();
assert!(
messages
.iter()
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
);
assert!(!engine.contains(session));
assert_eq!(
fixture.store.snapshot().unwrap().session_terminals[0].state(),
SessionTerminalState::ReleaseIntent
@ -337,7 +342,7 @@ fn changed_guest_identity_refuses_numeric_group_action() {
}
#[test]
fn absent_leader_with_live_group_retains_release_until_group_is_empty() {
fn absent_leader_with_live_group_closes_with_release_intent() {
let session = id(6);
let (fixture, factory, mut engine) = setup(&[session]);
engine.acquire(&acquisition(session)).unwrap();
@ -347,25 +352,7 @@ fn absent_leader_with_live_group_retains_release_until_group_is_empty() {
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
let messages = engine
.handle(
GenerationId::new(7),
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
@ -373,9 +360,73 @@ fn absent_leader_with_live_group_retains_release_until_group_is_empty() {
}),
)
.unwrap();
let state = factory.state(0);
assert!(state.lock().unwrap().resources.exec_reaped);
assert!(!state.lock().unwrap().resources.pty_open);
assert!(
messages
.iter()
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
);
assert!(!engine.contains(session));
assert_eq!(
fixture.store.snapshot().unwrap().session_terminals[0].state(),
SessionTerminalState::Clean
SessionTerminalState::ReleaseIntent
);
}
#[test]
fn exited_exec_with_absent_leader_emits_exit_and_retains_group_release() {
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;
state.resources.exec_exited = true;
}
let messages = engine.poll(session, GenerationId::new(7)).unwrap();
assert!(
messages
.iter()
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
);
assert!(!engine.contains(session));
assert_eq!(
fixture.store.snapshot().unwrap().session_terminals[0].state(),
SessionTerminalState::ReleaseIntent
);
}
#[test]
fn persistent_guest_group_closes_session_with_release_intent() {
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 = "never";
let messages = engine
.handle(
GenerationId::new(7),
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
session_id: session,
}),
)
.unwrap();
assert!(
messages
.iter()
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
);
assert!(!engine.contains(session));
assert_eq!(
fixture.store.snapshot().unwrap().session_terminals[0].state(),
SessionTerminalState::ReleaseIntent
);
}