Gate terminal group cleanup on live authority
This commit is contained in:
parent
4dc5daa57b
commit
c5ed009fb8
2 changed files with 355 additions and 48 deletions
|
|
@ -57,6 +57,12 @@ pub enum TerminalLoss {
|
||||||
HostCoordinatorLost,
|
HostCoordinatorLost,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum GuestReleaseAuthority {
|
||||||
|
Validate,
|
||||||
|
HostOnly,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum TerminalError {
|
pub enum TerminalError {
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
|
|
@ -279,6 +285,57 @@ impl HostCoordinatorTerminalRecorder {
|
||||||
Ok(route)
|
Ok(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_guest_signal_authority(
|
||||||
|
&self,
|
||||||
|
request: &TerminalAcquisition,
|
||||||
|
terminal: &SessionTerminal,
|
||||||
|
) -> Result<(), TerminalError> {
|
||||||
|
let snapshot = self.store.snapshot()?;
|
||||||
|
let current = snapshot
|
||||||
|
.generations
|
||||||
|
.iter()
|
||||||
|
.all(|candidate| candidate.generation_id <= request.generation_id);
|
||||||
|
let generation = snapshot.generations.iter().any(|generation| {
|
||||||
|
generation.generation_id == request.generation_id
|
||||||
|
&& generation.coordinator_id == request.coordinator_id
|
||||||
|
&& generation.durable_decision == DurableDecision::Published
|
||||||
|
});
|
||||||
|
let session = snapshot.sessions.iter().any(|session| {
|
||||||
|
session.generation_id == request.generation_id
|
||||||
|
&& session.session_id == request.session_id
|
||||||
|
&& matches!(
|
||||||
|
session.session_phase,
|
||||||
|
SessionPhase::Attached | SessionPhase::Closed
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let sandbox = snapshot.participants.iter().any(|participant| {
|
||||||
|
participant.generation_id() == request.generation_id
|
||||||
|
&& participant.role() == ParticipantRole::Sandbox
|
||||||
|
&& participant.state() == ParticipantState::Committed
|
||||||
|
&& matches!(
|
||||||
|
participant.ownership(),
|
||||||
|
Some(ParticipantOwnership::Sandbox(sandbox))
|
||||||
|
if sandbox.container_id == Some(request.container_id)
|
||||||
|
&& sandbox.host_coordinator.serviceable_pidfs().ok()
|
||||||
|
== Some(&request.host_coordinator)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let terminal = snapshot.session_terminals.iter().any(|record| {
|
||||||
|
record == terminal && record.state() == SessionTerminalState::ReleaseIntent
|
||||||
|
});
|
||||||
|
if snapshot.domain.domain_id == request.domain_id
|
||||||
|
&& current
|
||||||
|
&& generation
|
||||||
|
&& session
|
||||||
|
&& sandbox
|
||||||
|
&& terminal
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(TerminalError::AuthorityMismatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn begin_release(&self, terminal: SessionTerminal) -> Result<(), TerminalError> {
|
fn begin_release(&self, terminal: SessionTerminal) -> Result<(), TerminalError> {
|
||||||
let snapshot = self.store.snapshot()?;
|
let snapshot = self.store.snapshot()?;
|
||||||
let mut session = snapshot
|
let mut session = snapshot
|
||||||
|
|
@ -426,7 +483,13 @@ impl SessionTerminalEngine {
|
||||||
SessionTerminalEvent::Close(event) => event.session_id,
|
SessionTerminalEvent::Close(event) => event.session_id,
|
||||||
};
|
};
|
||||||
if matches!(event, SessionTerminalEvent::Close(_)) {
|
if matches!(event, SessionTerminalEvent::Close(_)) {
|
||||||
return self.close(session_id, generation_id, None, true);
|
return self.close(
|
||||||
|
session_id,
|
||||||
|
generation_id,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
GuestReleaseAuthority::Validate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let authoritative = self.refresh_route(session_id, generation_id)?;
|
let authoritative = self.refresh_route(session_id, generation_id)?;
|
||||||
if authoritative != TerminalRoute::Sandbox {
|
if authoritative != TerminalRoute::Sandbox {
|
||||||
|
|
@ -478,7 +541,13 @@ impl SessionTerminalEngine {
|
||||||
let authoritative = self.refresh_route(session_id, generation_id)?;
|
let authoritative = self.refresh_route(session_id, generation_id)?;
|
||||||
let terminal = self.active_mut(session_id, generation_id)?;
|
let terminal = self.active_mut(session_id, generation_id)?;
|
||||||
if terminal.process.exec_exited()? {
|
if terminal.process.exec_exited()? {
|
||||||
return self.close(session_id, generation_id, None, true);
|
return self.close(
|
||||||
|
session_id,
|
||||||
|
generation_id,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
GuestReleaseAuthority::Validate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if authoritative != TerminalRoute::Sandbox || terminal.releasing {
|
if authoritative != TerminalRoute::Sandbox || terminal.releasing {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
|
|
@ -486,13 +555,25 @@ impl SessionTerminalEngine {
|
||||||
flush_input(terminal)?;
|
flush_input(terminal)?;
|
||||||
let mut bytes = [0_u8; MAX_OUTPUT_BYTES];
|
let mut bytes = [0_u8; MAX_OUTPUT_BYTES];
|
||||||
match terminal.process.read_output(&mut bytes) {
|
match terminal.process.read_output(&mut bytes) {
|
||||||
Ok(0) => self.close(session_id, generation_id, Some(TerminalLoss::PtyLost), true),
|
Ok(0) => self.close(
|
||||||
|
session_id,
|
||||||
|
generation_id,
|
||||||
|
Some(TerminalLoss::PtyLost),
|
||||||
|
true,
|
||||||
|
GuestReleaseAuthority::Validate,
|
||||||
|
),
|
||||||
Ok(length) => Ok(vec![SessionTerminalMessage::Output(TerminalOutput {
|
Ok(length) => Ok(vec![SessionTerminalMessage::Output(TerminalOutput {
|
||||||
session_id,
|
session_id,
|
||||||
bytes: bytes[..length].to_vec(),
|
bytes: bytes[..length].to_vec(),
|
||||||
})]),
|
})]),
|
||||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => Ok(Vec::new()),
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => Ok(Vec::new()),
|
||||||
Err(_) => self.close(session_id, generation_id, Some(TerminalLoss::PtyLost), true),
|
Err(_) => self.close(
|
||||||
|
session_id,
|
||||||
|
generation_id,
|
||||||
|
Some(TerminalLoss::PtyLost),
|
||||||
|
true,
|
||||||
|
GuestReleaseAuthority::Validate,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,7 +601,13 @@ impl SessionTerminalEngine {
|
||||||
loss,
|
loss,
|
||||||
TerminalLoss::ClientDisconnected | TerminalLoss::HostCoordinatorLost
|
TerminalLoss::ClientDisconnected | TerminalLoss::HostCoordinatorLost
|
||||||
);
|
);
|
||||||
self.close(session_id, generation_id, Some(loss), connected)
|
self.close(
|
||||||
|
session_id,
|
||||||
|
generation_id,
|
||||||
|
Some(loss),
|
||||||
|
connected,
|
||||||
|
GuestReleaseAuthority::Validate,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -533,21 +620,46 @@ impl SessionTerminalEngine {
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
) -> Result<TerminalRoute, TerminalError> {
|
) -> Result<TerminalRoute, TerminalError> {
|
||||||
|
let active_generation = self
|
||||||
|
.active
|
||||||
|
.get(&session_id)
|
||||||
|
.ok_or(TerminalError::UnknownSession)?
|
||||||
|
.generation_id;
|
||||||
|
if active_generation != generation_id {
|
||||||
|
let cleanup = self.close(
|
||||||
|
session_id,
|
||||||
|
active_generation,
|
||||||
|
Some(TerminalLoss::ExecClientLost),
|
||||||
|
false,
|
||||||
|
GuestReleaseAuthority::HostOnly,
|
||||||
|
);
|
||||||
|
return Err(with_cleanup(TerminalError::StaleGeneration, cleanup));
|
||||||
|
}
|
||||||
let request = {
|
let request = {
|
||||||
let terminal = self.active_mut(session_id, generation_id)?;
|
let terminal = self.active_mut(session_id, generation_id)?;
|
||||||
terminal.request.clone()
|
terminal.request.clone()
|
||||||
};
|
};
|
||||||
match self.recorder.require_authority(&request) {
|
match self.recorder.require_authority(&request) {
|
||||||
Ok(TerminalRoute::Detached) | Err(TerminalError::AuthorityMismatch) => {
|
Ok(TerminalRoute::Detached) => {
|
||||||
let cleanup = self.close(
|
let cleanup = self.close(
|
||||||
session_id,
|
session_id,
|
||||||
generation_id,
|
generation_id,
|
||||||
Some(TerminalLoss::ExecClientLost),
|
Some(TerminalLoss::ExecClientLost),
|
||||||
false,
|
false,
|
||||||
|
GuestReleaseAuthority::HostOnly,
|
||||||
);
|
);
|
||||||
Err(with_cleanup(TerminalError::AuthorityMismatch, cleanup))
|
Err(with_cleanup(TerminalError::AuthorityMismatch, cleanup))
|
||||||
}
|
}
|
||||||
Err(error) => Err(error),
|
Err(error) => {
|
||||||
|
let cleanup = self.close(
|
||||||
|
session_id,
|
||||||
|
generation_id,
|
||||||
|
Some(TerminalLoss::ExecClientLost),
|
||||||
|
false,
|
||||||
|
GuestReleaseAuthority::HostOnly,
|
||||||
|
);
|
||||||
|
Err(with_cleanup(error, cleanup))
|
||||||
|
}
|
||||||
Ok(route) => {
|
Ok(route) => {
|
||||||
self.active_mut(session_id, generation_id)?.route = route;
|
self.active_mut(session_id, generation_id)?.route = route;
|
||||||
Ok(route)
|
Ok(route)
|
||||||
|
|
@ -576,15 +688,36 @@ impl SessionTerminalEngine {
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
loss: Option<TerminalLoss>,
|
loss: Option<TerminalLoss>,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
|
authority: GuestReleaseAuthority,
|
||||||
) -> Result<Vec<SessionTerminalMessage>, TerminalError> {
|
) -> Result<Vec<SessionTerminalMessage>, TerminalError> {
|
||||||
|
let active_generation = self
|
||||||
|
.active
|
||||||
|
.get(&session_id)
|
||||||
|
.ok_or(TerminalError::UnknownSession)?
|
||||||
|
.generation_id;
|
||||||
|
if active_generation != generation_id {
|
||||||
|
let cleanup = self.close(
|
||||||
|
session_id,
|
||||||
|
active_generation,
|
||||||
|
loss,
|
||||||
|
false,
|
||||||
|
GuestReleaseAuthority::HostOnly,
|
||||||
|
);
|
||||||
|
return Err(with_cleanup(TerminalError::StaleGeneration, cleanup));
|
||||||
|
}
|
||||||
let mut terminal = self
|
let mut terminal = self
|
||||||
.active
|
.active
|
||||||
.remove(&session_id)
|
.remove(&session_id)
|
||||||
.ok_or(TerminalError::UnknownSession)?;
|
.ok_or(TerminalError::UnknownSession)?;
|
||||||
if terminal.generation_id != generation_id {
|
let was_serviceable = matches!(authority, GuestReleaseAuthority::Validate)
|
||||||
self.active.insert(session_id, terminal);
|
&& (matches!(
|
||||||
return Err(TerminalError::StaleGeneration);
|
self.recorder.require_authority(&terminal.request),
|
||||||
}
|
Ok(TerminalRoute::Sandbox | TerminalRoute::Prompt | TerminalRoute::HostProcess)
|
||||||
|
) || (terminal.record.state() == SessionTerminalState::ReleaseIntent
|
||||||
|
&& self
|
||||||
|
.recorder
|
||||||
|
.require_guest_signal_authority(&terminal.request, &terminal.record)
|
||||||
|
.is_ok()));
|
||||||
let result = (|| {
|
let result = (|| {
|
||||||
terminal.releasing = true;
|
terminal.releasing = true;
|
||||||
terminal.route = TerminalRoute::Detached;
|
terminal.route = TerminalRoute::Detached;
|
||||||
|
|
@ -594,14 +727,23 @@ impl SessionTerminalEngine {
|
||||||
terminal.record = release;
|
terminal.record = release;
|
||||||
self.cancellation.cancel_for_close(session_id)?;
|
self.cancellation.cancel_for_close(session_id)?;
|
||||||
|
|
||||||
let (mut output, exit) = release_physical(
|
let mut authorize_guest = || {
|
||||||
|
was_serviceable
|
||||||
|
&& self
|
||||||
|
.recorder
|
||||||
|
.require_guest_signal_authority(&terminal.request, &terminal.record)
|
||||||
|
.is_ok()
|
||||||
|
};
|
||||||
|
let (mut output, exit, guest_absent) = release_physical(
|
||||||
terminal.process.as_mut(),
|
terminal.process.as_mut(),
|
||||||
session_id,
|
session_id,
|
||||||
connected,
|
connected,
|
||||||
!matches!(loss, Some(TerminalLoss::HostCoordinatorLost)),
|
&mut authorize_guest,
|
||||||
)?;
|
)?;
|
||||||
|
if guest_absent {
|
||||||
let clean = clean_terminal(generation_id, session_id)?;
|
let clean = clean_terminal(generation_id, session_id)?;
|
||||||
self.recorder.clean(clean)?;
|
self.recorder.clean(clean)?;
|
||||||
|
}
|
||||||
if connected {
|
if connected {
|
||||||
output.push(SessionTerminalMessage::Exit(exit_message(
|
output.push(SessionTerminalMessage::Exit(exit_message(
|
||||||
session_id, loss, exit,
|
session_id, loss, exit,
|
||||||
|
|
@ -620,11 +762,16 @@ fn release_physical(
|
||||||
process: &mut dyn SessionTerminalProcess,
|
process: &mut dyn SessionTerminalProcess,
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
signal_guest: bool,
|
authorize_guest: &mut dyn FnMut() -> bool,
|
||||||
) -> Result<(Vec<SessionTerminalMessage>, ProcessExit), TerminalError> {
|
) -> Result<(Vec<SessionTerminalMessage>, ProcessExit, bool), TerminalError> {
|
||||||
let mut failure = None;
|
let mut failure = None;
|
||||||
if signal_guest {
|
let mut guest_authorized = authorize_guest();
|
||||||
|
if guest_authorized {
|
||||||
for signal in ["HUP", "TERM", "KILL"] {
|
for signal in ["HUP", "TERM", "KILL"] {
|
||||||
|
if !authorize_guest() {
|
||||||
|
guest_authorized = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
match process.group_absent() {
|
match process.group_absent() {
|
||||||
Ok(true) => break,
|
Ok(true) => break,
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
|
|
@ -633,6 +780,10 @@ fn release_physical(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !authorize_guest() {
|
||||||
|
guest_authorized = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if let Err(error) = process.signal_group(signal) {
|
if let Err(error) = process.signal_group(signal) {
|
||||||
failure.get_or_insert(error);
|
failure.get_or_insert(error);
|
||||||
break;
|
break;
|
||||||
|
|
@ -685,8 +836,10 @@ fn release_physical(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
process.close_pty();
|
process.close_pty();
|
||||||
|
if guest_authorized && authorize_guest() {
|
||||||
match process.group_absent() {
|
match process.group_absent() {
|
||||||
Ok(true) => {}
|
Ok(true) if authorize_guest() => {}
|
||||||
|
Ok(true) => guest_authorized = false,
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
failure.get_or_insert(TerminalError::GuestGroupStillPresent);
|
failure.get_or_insert(TerminalError::GuestGroupStillPresent);
|
||||||
}
|
}
|
||||||
|
|
@ -694,12 +847,16 @@ fn release_physical(
|
||||||
failure.get_or_insert(error);
|
failure.get_or_insert(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
guest_authorized = false;
|
||||||
|
}
|
||||||
if let Some(error) = failure {
|
if let Some(error) = failure {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
Ok((
|
Ok((
|
||||||
output,
|
output,
|
||||||
exit.ok_or_else(|| io::Error::other("exec client exit is unavailable"))?,
|
exit.ok_or_else(|| io::Error::other("exec client exit is unavailable"))?,
|
||||||
|
guest_authorized,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -951,13 +1108,17 @@ fn cleanup_failed_running_commit(
|
||||||
PersistedHostProcessOwnership::committed(exec_client),
|
PersistedHostProcessOwnership::committed(exec_client),
|
||||||
Some(guest),
|
Some(guest),
|
||||||
)?;
|
)?;
|
||||||
let durable = recorder.put(observed.clone()).and_then(|()| {
|
let release = copy_terminal_state(&observed, SessionTerminalState::ReleaseIntent)?;
|
||||||
recorder.put(copy_terminal_state(
|
let durable = recorder
|
||||||
&observed,
|
.put(observed)
|
||||||
SessionTerminalState::ReleaseIntent,
|
.and_then(|()| recorder.put(release.clone()));
|
||||||
)?)
|
let mut authorize_guest = || {
|
||||||
});
|
recorder
|
||||||
let physical = release_physical(process, request.session_id, false, true).map(|_| ());
|
.require_guest_signal_authority(request, &release)
|
||||||
|
.is_ok()
|
||||||
|
};
|
||||||
|
let physical = release_physical(process, request.session_id, false, &mut authorize_guest)
|
||||||
|
.map(|(_, _, guest_absent)| guest_absent);
|
||||||
match (durable, physical) {
|
match (durable, physical) {
|
||||||
(Err(durable), Err(physical)) => {
|
(Err(durable), Err(physical)) => {
|
||||||
return Err(TerminalError::AcquisitionCleanup {
|
return Err(TerminalError::AcquisitionCleanup {
|
||||||
|
|
@ -965,8 +1126,9 @@ fn cleanup_failed_running_commit(
|
||||||
cleanup: Some(Box::new(physical)),
|
cleanup: Some(Box::new(physical)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
(Err(error), Ok(())) | (Ok(()), Err(error)) => return Err(error),
|
(Err(error), Ok(_)) | (Ok(()), Err(error)) => return Err(error),
|
||||||
(Ok(()), Ok(())) => {}
|
(Ok(()), Ok(false)) => return Ok(()),
|
||||||
|
(Ok(()), Ok(true)) => {}
|
||||||
}
|
}
|
||||||
recorder.clean(clean_terminal(request.generation_id, request.session_id)?)
|
recorder.clean(clean_terminal(request.generation_id, request.session_id)?)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@ struct MockGuestState {
|
||||||
group_present: bool,
|
group_present: bool,
|
||||||
leader_present: bool,
|
leader_present: bool,
|
||||||
identity_mismatch: bool,
|
identity_mismatch: bool,
|
||||||
|
clears_after_signal: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -258,6 +259,7 @@ enum StoreFailurePoint {
|
||||||
struct FailingRunningStore {
|
struct FailingRunningStore {
|
||||||
inner: Arc<HostCoordinatorDomainStore>,
|
inner: Arc<HostCoordinatorDomainStore>,
|
||||||
failure: Mutex<Option<(StoreFailurePoint, bool)>>,
|
failure: Mutex<Option<(StoreFailurePoint, bool)>>,
|
||||||
|
snapshot_failure: Mutex<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FailingRunningStore {
|
impl FailingRunningStore {
|
||||||
|
|
@ -265,16 +267,29 @@ impl FailingRunningStore {
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
failure: Mutex::new(None),
|
failure: Mutex::new(None),
|
||||||
|
snapshot_failure: Mutex::new(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arm(&self, point: StoreFailurePoint, persistent: bool) {
|
fn arm(&self, point: StoreFailurePoint, persistent: bool) {
|
||||||
*self.failure.lock().unwrap() = Some((point, persistent));
|
*self.failure.lock().unwrap() = Some((point, persistent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn arm_snapshot_failure(&self) {
|
||||||
|
*self.snapshot_failure.lock().unwrap() = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalDomainStore for FailingRunningStore {
|
impl TerminalDomainStore for FailingRunningStore {
|
||||||
fn snapshot(&self) -> Result<lazaret_core::records::DomainStore, DomainStoreError> {
|
fn snapshot(&self) -> Result<lazaret_core::records::DomainStore, DomainStoreError> {
|
||||||
|
let mut failure = self.snapshot_failure.lock().unwrap();
|
||||||
|
if *failure {
|
||||||
|
*failure = false;
|
||||||
|
return Err(DomainStoreError::Io(io::Error::other(
|
||||||
|
"injected authority read failure",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
drop(failure);
|
||||||
self.inner.snapshot()
|
self.inner.snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,7 +360,7 @@ impl SessionTerminalProcess for MockProcess {
|
||||||
return Err(TerminalError::GuestIdentityMismatch);
|
return Err(TerminalError::GuestIdentityMismatch);
|
||||||
}
|
}
|
||||||
state.log.lock().unwrap().push(format!("signal:{signal}"));
|
state.log.lock().unwrap().push(format!("signal:{signal}"));
|
||||||
if signal == "HUP" {
|
if signal == state.guest.clears_after_signal {
|
||||||
state.guest.group_present = false;
|
state.guest.group_present = false;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -446,6 +461,7 @@ impl SessionTerminalFactory for MockFactory {
|
||||||
group_present: true,
|
group_present: true,
|
||||||
leader_present: true,
|
leader_present: true,
|
||||||
identity_mismatch: false,
|
identity_mismatch: false,
|
||||||
|
clears_after_signal: "HUP",
|
||||||
},
|
},
|
||||||
resources: MockResourceState {
|
resources: MockResourceState {
|
||||||
exec_exited: false,
|
exec_exited: false,
|
||||||
|
|
@ -489,12 +505,20 @@ fn setup(ids: &[SessionId]) -> (Fixture, Arc<MockFactory>, SessionTerminalEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
fn engine_for(fixture: &Fixture) -> (Arc<MockFactory>, SessionTerminalEngine) {
|
fn engine_for(fixture: &Fixture) -> (Arc<MockFactory>, SessionTerminalEngine) {
|
||||||
|
let store: Arc<dyn TerminalDomainStore> = fixture.store.clone();
|
||||||
|
engine_for_store(store)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn engine_for_store(
|
||||||
|
store: Arc<dyn TerminalDomainStore>,
|
||||||
|
) -> (Arc<MockFactory>, SessionTerminalEngine) {
|
||||||
let factory = Arc::new(MockFactory::default());
|
let factory = Arc::new(MockFactory::default());
|
||||||
let engine = SessionTerminalEngine::new(
|
let engine = SessionTerminalEngine {
|
||||||
Arc::clone(&fixture.store),
|
recorder: HostCoordinatorTerminalRecorder::with_store(store),
|
||||||
factory.clone(),
|
factory: factory.clone(),
|
||||||
Arc::new(Cancellation(Arc::clone(&factory.log))),
|
cancellation: Arc::new(Cancellation(Arc::clone(&factory.log))),
|
||||||
);
|
active: HashMap::new(),
|
||||||
|
};
|
||||||
(factory, engine)
|
(factory, engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -502,6 +526,17 @@ fn id(value: u8) -> SessionId {
|
||||||
SessionId::new(bytes(value))
|
SessionId::new(bytes(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_no_group_signal(factory: &MockFactory) {
|
||||||
|
assert!(
|
||||||
|
factory
|
||||||
|
.log
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.all(|action| !action.starts_with("signal:"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn acquisition_refuses_authority_outside_the_committed_sandbox() {
|
fn acquisition_refuses_authority_outside_the_committed_sandbox() {
|
||||||
let session = id(6);
|
let session = id(6);
|
||||||
|
|
@ -552,7 +587,7 @@ fn acquisition_requires_attached_current_session() {
|
||||||
fn durable_detach_failure_and_sandbox_release_revoke_active_terminal() {
|
fn durable_detach_failure_and_sandbox_release_revoke_active_terminal() {
|
||||||
for revoke in ["closed", "failed", "sandbox-release"] {
|
for revoke in ["closed", "failed", "sandbox-release"] {
|
||||||
let session = id(6);
|
let session = id(6);
|
||||||
let (fixture, _factory, mut engine) = setup(&[session]);
|
let (fixture, factory, mut engine) = setup(&[session]);
|
||||||
engine.acquire(&acquisition(session)).unwrap();
|
engine.acquire(&acquisition(session)).unwrap();
|
||||||
match revoke {
|
match revoke {
|
||||||
"closed" => fixture.set_session_phase(session, SessionPhase::Closed),
|
"closed" => fixture.set_session_phase(session, SessionPhase::Closed),
|
||||||
|
|
@ -571,13 +606,94 @@ fn durable_detach_failure_and_sandbox_release_revoke_active_terminal() {
|
||||||
Err(TerminalError::AuthorityMismatch)
|
Err(TerminalError::AuthorityMismatch)
|
||||||
));
|
));
|
||||||
assert!(!engine.contains(session));
|
assert!(!engine.contains(session));
|
||||||
|
assert_no_group_signal(&factory);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||||
SessionTerminalState::Clean
|
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]
|
#[test]
|
||||||
fn stale_caller_and_superseded_generation_refuse_without_cross_generation_action() {
|
fn stale_caller_and_superseded_generation_refuse_without_cross_generation_action() {
|
||||||
let session = id(6);
|
let session = id(6);
|
||||||
|
|
@ -593,15 +709,26 @@ fn stale_caller_and_superseded_generation_refuse_without_cross_generation_action
|
||||||
),
|
),
|
||||||
Err(TerminalError::StaleGeneration)
|
Err(TerminalError::StaleGeneration)
|
||||||
));
|
));
|
||||||
assert!(factory.log.lock().unwrap().is_empty());
|
assert_no_group_signal(&factory);
|
||||||
assert!(engine.contains(session));
|
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();
|
fixture.add_newer_generation();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
engine.poll(session, GenerationId::new(7)),
|
engine.poll(session, GenerationId::new(7)),
|
||||||
Err(TerminalError::AuthorityMismatch)
|
Err(TerminalError::AuthorityMismatch)
|
||||||
));
|
));
|
||||||
assert!(!engine.contains(session));
|
assert!(!engine.contains(session));
|
||||||
|
assert_no_group_signal(&factory);
|
||||||
|
assert_eq!(
|
||||||
|
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||||
|
SessionTerminalState::ReleaseIntent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -789,6 +916,7 @@ fn running_store_failures_release_every_live_partial_handle() {
|
||||||
group_present: true,
|
group_present: true,
|
||||||
leader_present: true,
|
leader_present: true,
|
||||||
identity_mismatch: false,
|
identity_mismatch: false,
|
||||||
|
clears_after_signal: "HUP",
|
||||||
},
|
},
|
||||||
resources: MockResourceState {
|
resources: MockResourceState {
|
||||||
exec_exited: false,
|
exec_exited: false,
|
||||||
|
|
@ -818,7 +946,16 @@ fn running_store_failures_release_every_live_partial_handle() {
|
||||||
error => panic!("unexpected cleanup result: {error}"),
|
error => panic!("unexpected cleanup result: {error}"),
|
||||||
}
|
}
|
||||||
let state = state.lock().unwrap();
|
let state = state.lock().unwrap();
|
||||||
assert!(!state.guest.group_present);
|
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_exited);
|
||||||
assert!(state.resources.exec_reaped);
|
assert!(state.resources.exec_reaped);
|
||||||
assert!(!state.resources.pty_open);
|
assert!(!state.resources.pty_open);
|
||||||
|
|
@ -886,6 +1023,7 @@ fn close_cancels_route_then_bounds_drain_and_emits_one_final_exit() {
|
||||||
let session = id(6);
|
let session = id(6);
|
||||||
let (fixture, factory, mut engine) = setup(&[session]);
|
let (fixture, factory, mut engine) = setup(&[session]);
|
||||||
engine.acquire(&acquisition(session)).unwrap();
|
engine.acquire(&acquisition(session)).unwrap();
|
||||||
|
factory.state(0).lock().unwrap().guest.clears_after_signal = "KILL";
|
||||||
factory
|
factory
|
||||||
.state(0)
|
.state(0)
|
||||||
.lock()
|
.lock()
|
||||||
|
|
@ -911,10 +1049,16 @@ fn close_cancels_route_then_bounds_drain_and_emits_one_final_exit() {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(messages.last(), Some(SessionTerminalMessage::Exit(exit)) if exit.exit_code() == Some(17))
|
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!(
|
assert_eq!(
|
||||||
&factory.log.lock().unwrap()[..2],
|
log.iter()
|
||||||
&["cancel-route", "signal:HUP"]
|
.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!(!engine.contains(session));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
engine.handle(
|
engine.handle(
|
||||||
|
|
@ -951,7 +1095,7 @@ fn disconnect_component_loss_and_hard_hc_loss_never_reattach() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = id(6);
|
let session = id(6);
|
||||||
let (fixture, _factory, mut engine) = setup(&[session]);
|
let (fixture, factory, mut engine) = setup(&[session]);
|
||||||
engine.acquire(&acquisition(session)).unwrap();
|
engine.acquire(&acquisition(session)).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
engine
|
engine
|
||||||
|
|
@ -964,6 +1108,7 @@ fn disconnect_component_loss_and_hard_hc_loss_never_reattach() {
|
||||||
.is_empty()
|
.is_empty()
|
||||||
);
|
);
|
||||||
assert!(!engine.contains(session));
|
assert!(!engine.contains(session));
|
||||||
|
assert_no_group_signal(&factory);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||||
SessionTerminalState::Running
|
SessionTerminalState::Running
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue