Isolate interactive terminal session ownership
This commit is contained in:
parent
b240bc1d12
commit
690738d846
8 changed files with 280 additions and 24 deletions
|
|
@ -422,7 +422,13 @@ impl ClientBackend for CoordinatorClientBackend {
|
|||
} else {
|
||||
execute_over_control_face_as(&endpoint, argv, stdout, stderr, &self.identity)?
|
||||
};
|
||||
if let Some(coordinator) = self.coordinator.take() {
|
||||
self.bootstrap_owner.take();
|
||||
if let Some(coordinator) = self.coordinator.take()
|
||||
&& coordinator
|
||||
.has_exited()
|
||||
.map_err(failure)
|
||||
.map_err(ClientError::Backend)?
|
||||
{
|
||||
let status = coordinator
|
||||
.reap()
|
||||
.map_err(failure)
|
||||
|
|
@ -433,7 +439,6 @@ impl ClientBackend for CoordinatorClientBackend {
|
|||
))));
|
||||
}
|
||||
}
|
||||
self.bootstrap_owner.take();
|
||||
Ok(exit)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ pub struct TargetCommand {
|
|||
parent_edge: ParentEdge,
|
||||
executable: Option<OwnedFd>,
|
||||
stdin: Option<File>,
|
||||
terminal: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -106,6 +107,7 @@ impl TargetCommand {
|
|||
parent_edge: ParentEdge::ParentDeathKill,
|
||||
executable: None,
|
||||
stdin: None,
|
||||
terminal: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +126,7 @@ impl TargetCommand {
|
|||
parent_edge: ParentEdge::ParentDeathKill,
|
||||
executable: Some(executable),
|
||||
stdin: None,
|
||||
terminal: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +159,12 @@ impl TargetCommand {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_controlling_terminal(mut self) -> Self {
|
||||
self.terminal = true;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn parent_edge(&self) -> ParentEdge {
|
||||
self.parent_edge
|
||||
|
|
@ -180,6 +189,7 @@ impl TargetCommand {
|
|||
ParentEdge::ParentDeathKill => "kill",
|
||||
ParentEdge::LifetimePipe => "lifetime-pipe",
|
||||
})
|
||||
.args(self.terminal.then_some("--target-terminal"))
|
||||
.args(
|
||||
self.stdin
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use super::{
|
|||
use crate::ipc::private_stream_pair;
|
||||
use crate::user_namespace::UserNamespaceMap;
|
||||
use std::io::{Read as _, Write as _};
|
||||
use std::os::unix::process::CommandExt as _;
|
||||
|
||||
impl ChildBroker {
|
||||
/// Spawns the private Host Coordinator bootstrap with exact pipe ownership and pidfd reaping.
|
||||
|
|
@ -44,7 +45,8 @@ impl ChildBroker {
|
|||
.args(&target.arguments)
|
||||
.stdin(Stdio::from(lifetime_reader))
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit());
|
||||
.stderr(Stdio::inherit())
|
||||
.process_group(0);
|
||||
let mut child = command.spawn()?;
|
||||
let status = child
|
||||
.stdout
|
||||
|
|
@ -743,6 +745,7 @@ fn run_mapped_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
|
|||
rustix::process::Uid::ROOT,
|
||||
rustix::process::Uid::ROOT,
|
||||
)?;
|
||||
establish_controlling_terminal(&target)?;
|
||||
if let Some(stdin) = target.stdin.as_ref() {
|
||||
rustix::stdio::dup2_stdin(stdin)?;
|
||||
}
|
||||
|
|
@ -761,12 +764,27 @@ fn run_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
|
|||
require_lifetime_pipe_and_recheck(expected_parent)?;
|
||||
}
|
||||
}
|
||||
establish_controlling_terminal(&target)?;
|
||||
if let Some(stdin) = target.stdin.as_ref() {
|
||||
rustix::stdio::dup2_stdin(stdin)?;
|
||||
}
|
||||
Err(exec_target(target))
|
||||
}
|
||||
|
||||
fn establish_controlling_terminal(target: &TargetCommand) -> io::Result<()> {
|
||||
if !target.terminal {
|
||||
return Ok(());
|
||||
}
|
||||
let stdin = target
|
||||
.stdin
|
||||
.as_ref()
|
||||
.ok_or_else(|| io::Error::other("terminal target stdin is missing"))?;
|
||||
rustix::process::setsid()?;
|
||||
rustix::process::ioctl_tiocsctty(stdin)?;
|
||||
rustix::termios::tcsetpgrp(stdin, rustix::process::getpgrp())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_lifetime_pipe_and_recheck(
|
||||
expected_parent: PidfsProcessIdentityV1,
|
||||
) -> io::Result<RetainedProcess> {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,11 @@ pub fn parse_wrapper_arguments(
|
|||
Some("lifetime-pipe") => ParentEdge::LifetimePipe,
|
||||
_ => return Err(io::Error::other("wrapper parent action is invalid")),
|
||||
};
|
||||
let next = arguments.next();
|
||||
let mut next = arguments.next();
|
||||
let terminal = next.as_deref() == Some(OsStr::new("--target-terminal"));
|
||||
if terminal {
|
||||
next = arguments.next();
|
||||
}
|
||||
let stdin = if next.as_deref() == Some(OsStr::new("--target-stdin-fd")) {
|
||||
let descriptor = arguments
|
||||
.next()
|
||||
|
|
@ -117,6 +121,7 @@ pub fn parse_wrapper_arguments(
|
|||
parent_edge,
|
||||
executable: None,
|
||||
stdin,
|
||||
terminal,
|
||||
};
|
||||
let boot = parse_boot_hex(&boot)?;
|
||||
Ok((
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::os::fd::AsFd;
|
|||
use std::process::{ExitCode, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Barrier};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use lazaret_core::identity::{
|
||||
CoordinatorId, DomainId, GenerationId, HostPidLocator, PidfsProcessIdentityV1,
|
||||
|
|
@ -13,14 +14,14 @@ use lazaret_core::records::{
|
|||
HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::process::{
|
||||
AcknowledgedChildAcquisition, ChildStandardIo, ProcessOwnershipContext, TargetCommand,
|
||||
append_parent_identity, child_broker, current_process_identity, dispatch_private_process_role,
|
||||
verify_direct_child_pidfd,
|
||||
AcknowledgedChildAcquisition, ChildStandardIo, ProcessOwnershipContext, RetainedProcess,
|
||||
TargetCommand, append_parent_identity, child_broker, current_process_identity,
|
||||
dispatch_private_process_role, verify_direct_child_pidfd,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
||||
use rustix::pipe::{PipeFlags, pipe_with};
|
||||
use rustix::process::{PidfdFlags, pidfd_open};
|
||||
use rustix::process::{PidfdFlags, Signal, pidfd_open};
|
||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||
use rustix::termios::{Winsize, tcgetwinsize, tcsetwinsize};
|
||||
|
||||
|
|
@ -52,6 +53,8 @@ fn run_all() -> std::io::Result<()> {
|
|||
mapped_transient_uses_configured_subordinates()?;
|
||||
transient_captured_child_preserves_streams_and_exit()?;
|
||||
terminal_child_restores_slave_stdin_and_merges_output()?;
|
||||
terminal_children_isolate_window_signals()?;
|
||||
bootstrap_child_uses_a_distinct_process_group()?;
|
||||
target_stdin_does_not_gain_write_authority()?;
|
||||
acknowledged_transition_precedes_exact_wrapper_release()?;
|
||||
failed_acknowledged_transition_refuses_exact_wrapper_release()?;
|
||||
|
|
@ -64,6 +67,33 @@ fn run_all() -> std::io::Result<()> {
|
|||
concurrent_acquisitions_serialize()
|
||||
}
|
||||
|
||||
fn bootstrap_child_uses_a_distinct_process_group() -> std::io::Result<()> {
|
||||
let (reader, writer) = pipe_with(PipeFlags::CLOEXEC)?;
|
||||
let target = TargetCommand::new("/bin/sh")
|
||||
.argument("-c")
|
||||
.argument(
|
||||
"if [ \"$(ps -o pgid= -p $$)\" = \"$(ps -o pgid= -p $PPID)\" ]; then printf E; else printf R; fi; read value || exit 0",
|
||||
)
|
||||
.with_lifetime_pipe_parent_edge();
|
||||
let mut child = child_broker().spawn_bootstrap(&target, File::from(reader))?;
|
||||
if child.bootstrap_status()? != b'R' {
|
||||
return Err(std::io::Error::other(
|
||||
"bootstrap child shared the client process group",
|
||||
));
|
||||
}
|
||||
drop(writer);
|
||||
while !child.has_exited()? {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
let exit = child.reap()?;
|
||||
if exit.exit_code != Some(0) || exit.terminating_signal.is_some() {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"bootstrap process-group probe failed: {exit:?}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mapped_transient_uses_configured_subordinates() -> std::io::Result<()> {
|
||||
let map = UserNamespaceMap::resolve_current()?;
|
||||
child_broker()
|
||||
|
|
@ -149,7 +179,8 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
|
|||
.argument(
|
||||
"read value; printf 'stdin-fd\\n' >&0; printf 'out:%s\\n' \"$value\"; printf 'err\\n' >&2",
|
||||
)
|
||||
.with_target_stdin(slave.try_clone()?)?;
|
||||
.with_target_stdin(slave.try_clone()?)?
|
||||
.with_controlling_terminal();
|
||||
let expected_parent = current_process_identity()?;
|
||||
let ownership = ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([41; 32]),
|
||||
|
|
@ -193,6 +224,133 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn terminal_children_isolate_window_signals() -> std::io::Result<()> {
|
||||
let (mut first_master, first) = terminal_child("S1", 51)?;
|
||||
let (mut second_master, second) = terminal_child("S2", 52)?;
|
||||
let mut first_output = Vec::new();
|
||||
let mut second_output = Vec::new();
|
||||
wait_for_terminal_marker(&mut first_master, &mut first_output, b"S1_READY")?;
|
||||
wait_for_terminal_marker(&mut second_master, &mut second_output, b"S2_READY")?;
|
||||
let first_identity = terminal_identity(&first_output, "S1_READY")?;
|
||||
let second_identity = terminal_identity(&second_output, "S2_READY")?;
|
||||
if first_identity
|
||||
.iter()
|
||||
.any(|value| *value != first_identity[0])
|
||||
|| second_identity
|
||||
.iter()
|
||||
.any(|value| *value != second_identity[0])
|
||||
|| first_identity[0] == second_identity[0]
|
||||
{
|
||||
return Err(std::io::Error::other(
|
||||
"terminal targets did not receive distinct controlling sessions",
|
||||
));
|
||||
}
|
||||
first.send_signal(Signal::WINCH)?;
|
||||
wait_for_terminal_marker(&mut first_master, &mut first_output, b"S1_WINCH")?;
|
||||
second_master.write_all(b"after\n")?;
|
||||
wait_for_terminal_marker(&mut second_master, &mut second_output, b"S2:after")?;
|
||||
if second_output
|
||||
.windows(b"S2_WINCH".len())
|
||||
.any(|part| part == b"S2_WINCH")
|
||||
{
|
||||
return Err(std::io::Error::other(
|
||||
"window signal crossed terminal process groups",
|
||||
));
|
||||
}
|
||||
while !first.has_exited()? {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
let first_exit = child_broker().reap_status(first)?;
|
||||
second.send_signal(Signal::KILL)?;
|
||||
let second_exit = child_broker().reap_status(second)?;
|
||||
if first_exit.exit_code != Some(0)
|
||||
|| first_exit.terminating_signal.is_some()
|
||||
|| second_exit.terminating_signal.is_none()
|
||||
{
|
||||
return Err(std::io::Error::other(
|
||||
"terminal signal isolation produced unexpected exits",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn terminal_child(marker: &str, seed: u8) -> std::io::Result<(File, RetainedProcess)> {
|
||||
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)?;
|
||||
grantpt(&master)?;
|
||||
unlockpt(&master)?;
|
||||
let slave = File::from(ioctl_tiocgptpeer(
|
||||
&master,
|
||||
OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC,
|
||||
)?);
|
||||
let target = TargetCommand::new("/usr/bin/python3")
|
||||
.argument("-c")
|
||||
.argument(format!(
|
||||
"import os, signal, sys\nmarker = {marker:?}\ndef winch(*_):\n print(marker + '_WINCH', flush=True)\n sys.exit(0)\nsignal.signal(signal.SIGWINCH, winch)\nprint(f'{{marker}}_READY pid={{os.getpid()}} sid={{os.getsid(0)}} pgrp={{os.getpgrp()}} tty={{os.tcgetpgrp(0)}}', flush=True)\nfor line in sys.stdin:\n print(marker + ':' + line.strip(), flush=True)"
|
||||
))
|
||||
.with_target_stdin(slave.try_clone()?)?
|
||||
.with_controlling_terminal();
|
||||
let process = child_broker().acquire_with_stdio(
|
||||
current_process_identity()?,
|
||||
ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([seed; 32]),
|
||||
generation_id: GenerationId::new(u64::from(seed)),
|
||||
coordinator_id: CoordinatorId::new([seed.wrapping_add(1); 32]),
|
||||
slot: HostProcessOwnershipSlot::SessionTerminalExecClient(
|
||||
lazaret_core::identity::SessionId::new([seed.wrapping_add(2); 32]),
|
||||
),
|
||||
},
|
||||
&target,
|
||||
ChildStandardIo::terminal(slave)?,
|
||||
|_| Ok(()),
|
||||
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||
)?;
|
||||
let master = File::from(master);
|
||||
let flags = fcntl_getfl(&master)?;
|
||||
fcntl_setfl(&master, flags | OFlags::NONBLOCK)?;
|
||||
Ok((master, process))
|
||||
}
|
||||
|
||||
fn terminal_identity(output: &[u8], marker: &str) -> std::io::Result<[u32; 4]> {
|
||||
let text = String::from_utf8_lossy(output);
|
||||
let line = text
|
||||
.lines()
|
||||
.find(|line| line.starts_with(marker))
|
||||
.ok_or_else(|| std::io::Error::other("terminal identity marker is missing"))?;
|
||||
let values = line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.map(|field| {
|
||||
field
|
||||
.split_once('=')
|
||||
.and_then(|(_, value)| value.parse::<u32>().ok())
|
||||
.ok_or_else(|| std::io::Error::other("terminal identity field is invalid"))
|
||||
})
|
||||
.collect::<std::io::Result<Vec<_>>>()?;
|
||||
values
|
||||
.try_into()
|
||||
.map_err(|_| std::io::Error::other("terminal identity field count is invalid"))
|
||||
}
|
||||
|
||||
fn wait_for_terminal_marker(
|
||||
master: &mut File,
|
||||
output: &mut Vec<u8>,
|
||||
marker: &[u8],
|
||||
) -> std::io::Result<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while !output.windows(marker.len()).any(|part| part == marker) {
|
||||
read_available(master, output)?;
|
||||
if Instant::now() >= deadline {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"terminal marker {:?} deadline expired with {:?}",
|
||||
String::from_utf8_lossy(marker),
|
||||
String::from_utf8_lossy(output)
|
||||
)));
|
||||
}
|
||||
std::thread::yield_now();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transient_captured_child_preserves_streams_and_exit() -> std::io::Result<()> {
|
||||
let target = TargetCommand::new("/bin/sh")
|
||||
.argument("-c")
|
||||
|
|
|
|||
Loading…
Reference in a new issue