Add coordinator-owned interactive session terminals

This commit is contained in:
ms2k 2026-07-17 05:33:22 +09:00
commit 51ff63a3aa
2 changed files with 2078 additions and 15 deletions

View file

@ -31,6 +31,7 @@ pub struct TargetCommand {
arguments: Vec<OsString>,
parent_edge: ParentEdge,
executable: Option<OwnedFd>,
stdin: Option<File>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -47,6 +48,7 @@ impl TargetCommand {
arguments: Vec::new(),
parent_edge: ParentEdge::ParentDeathKill,
executable: None,
stdin: None,
}
}
@ -64,6 +66,7 @@ impl TargetCommand {
arguments: arguments.into_iter().collect(),
parent_edge: ParentEdge::ParentDeathKill,
executable: Some(executable),
stdin: None,
})
}
@ -85,6 +88,16 @@ impl TargetCommand {
self
}
/// Attaches the target's standard input after the private wrapper consumes its barrier.
///
/// # Errors
/// Returns an error when the descriptor cannot be inherited by the wrapper.
pub fn with_target_stdin(mut self, stdin: File) -> io::Result<Self> {
fcntl_setfd(stdin.as_fd(), FdFlags::empty())?;
self.stdin = Some(stdin);
Ok(self)
}
#[must_use]
pub const fn parent_edge(&self) -> ParentEdge {
self.parent_edge
@ -109,6 +122,18 @@ impl TargetCommand {
ParentEdge::ParentDeathKill => "kill",
ParentEdge::LifetimePipe => "lifetime-pipe",
})
.args(
self.stdin
.as_ref()
.map(|stdin| {
[
OsString::from("--target-stdin-fd"),
OsString::from(stdin.as_raw_fd().to_string()),
]
})
.into_iter()
.flatten(),
)
.arg("--")
.arg(&self.program)
.args(&self.arguments);
@ -126,6 +151,7 @@ pub enum ChildStandardIoPolicy {
Inherit,
PrivateControlWithLifetimePipe,
Silenced,
Terminal,
}
impl ChildStandardIo {
@ -159,6 +185,17 @@ impl ChildStandardIo {
}
}
/// # Errors
/// Returns an error when the output descriptor cannot be duplicated for merged terminal I/O.
pub fn terminal(output: File) -> io::Result<Self> {
let error = output.try_clone()?;
Ok(Self {
stdout: Some(Stdio::from(output)),
stderr: Some(Stdio::from(error)),
policy: ChildStandardIoPolicy::Terminal,
})
}
#[must_use]
pub const fn policy(&self) -> ChildStandardIoPolicy {
self.policy
@ -296,7 +333,9 @@ fn require_parent_edge_standard_io(
(ParentEdge::LifetimePipe, ChildStandardIoPolicy::PrivateControlWithLifetimePipe)
| (
ParentEdge::ParentDeathKill,
ChildStandardIoPolicy::Inherit | ChildStandardIoPolicy::Silenced,
ChildStandardIoPolicy::Inherit
| ChildStandardIoPolicy::Silenced
| ChildStandardIoPolicy::Terminal,
) => Ok(()),
(ParentEdge::LifetimePipe, _) => Err(io::Error::other(
"lifetime-pipe parent edge requires private control stdout and lifetime-pipe stderr",
@ -778,6 +817,9 @@ fn run_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
require_lifetime_pipe_and_recheck(expected_parent)?;
}
}
if let Some(stdin) = target.stdin.as_ref() {
rustix::stdio::dup2_stdin(stdin)?;
}
Err(exec_target(target))
}
@ -1245,9 +1287,22 @@ pub fn parse_wrapper_arguments(
Some("lifetime-pipe") => ParentEdge::LifetimePipe,
_ => return Err(io::Error::other("wrapper parent action is invalid")),
};
if arguments.next().as_deref() != Some(OsStr::new("--")) {
let next = arguments.next();
let stdin = if next.as_deref() == Some(OsStr::new("--target-stdin-fd")) {
let descriptor = arguments
.next()
.and_then(|value| value.to_str().and_then(|value| value.parse::<i32>().ok()))
.filter(|descriptor| *descriptor > 2)
.ok_or_else(|| io::Error::other("wrapper target stdin descriptor is invalid"))?;
if arguments.next().as_deref() != Some(OsStr::new("--")) {
return Err(io::Error::other("wrapper target separator is missing"));
}
Some(File::open(format!("/proc/self/fd/{descriptor}"))?)
} else if next.as_deref() == Some(OsStr::new("--")) {
None
} else {
return Err(io::Error::other("wrapper target separator is missing"));
}
};
let program = arguments
.next()
.ok_or_else(|| io::Error::other("wrapper target is missing"))?;
@ -1256,6 +1311,7 @@ pub fn parse_wrapper_arguments(
arguments: arguments.collect(),
parent_edge,
executable: None,
stdin,
};
let boot = parse_boot_hex(&boot)?;
Ok((

View file

@ -17,8 +17,11 @@ use lazaret_linux::process::{
append_parent_identity, child_broker, current_process_identity, dispatch_private_process_role,
verify_direct_child_pidfd,
};
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
use rustix::pipe::{PipeFlags, pipe_with};
use rustix::process::{PidfdFlags, pidfd_open};
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
use rustix::termios::{Winsize, tcgetwinsize, tcsetwinsize};
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
@ -46,6 +49,7 @@ fn exit(result: std::io::Result<()>) -> ExitCode {
fn run_all() -> std::io::Result<()> {
successful_acquisition()?;
transient_captured_child_preserves_streams_and_exit()?;
terminal_child_restores_slave_stdin_and_merges_output()?;
acknowledged_transition_precedes_exact_wrapper_release()?;
failed_acknowledged_transition_refuses_exact_wrapper_release()?;
lifetime_pipe_descriptor_contract()?;
@ -57,6 +61,75 @@ fn run_all() -> std::io::Result<()> {
concurrent_acquisitions_serialize()
}
fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()> {
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)?;
grantpt(&master)?;
unlockpt(&master)?;
let slave = ioctl_tiocgptpeer(
&master,
OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC,
)?;
tcsetwinsize(
&master,
Winsize {
ws_row: 44,
ws_col: 132,
ws_xpixel: 0,
ws_ypixel: 0,
},
)?;
if tcgetwinsize(&master)?.ws_row != 44 || tcgetwinsize(&master)?.ws_col != 132 {
return Err(std::io::Error::other(
"PTY resize did not update the master",
));
}
let flags = fcntl_getfl(&master)?;
fcntl_setfl(&master, flags | OFlags::NONBLOCK)?;
let slave = File::from(slave);
let target = TargetCommand::new("/bin/sh")
.argument("-c")
.argument("read value; printf 'out:%s\\n' \"$value\"; printf 'err\\n' >&2")
.with_target_stdin(slave.try_clone()?)?;
let expected_parent = current_process_identity()?;
let ownership = ProcessOwnershipContext {
domain_id: DomainId::new([41; 32]),
generation_id: GenerationId::new(42),
coordinator_id: CoordinatorId::new([43; 32]),
slot: HostProcessOwnershipSlot::SessionTerminalExecClient(
lazaret_core::identity::SessionId::new([44; 32]),
),
};
let process = child_broker().acquire_with_stdio(
expected_parent,
ownership,
&target,
ChildStandardIo::terminal(slave)?,
|_| Ok(()),
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
)?;
let mut master = File::from(master);
master.write_all(b"hello\n")?;
let mut merged = Vec::new();
while !process.has_exited()? {
let _ = read_available(&mut master, &mut merged);
std::thread::yield_now();
}
let _ = read_available(&mut master, &mut merged);
let exit = child_broker().reap_status(process)?;
let text = String::from_utf8_lossy(&merged);
let out = text.find("out:hello");
let err = text.find("err");
if !matches!((out, err), (Some(out), Some(err)) if out < err)
|| exit.exit_code != Some(0)
|| exit.terminating_signal.is_some()
{
return Err(std::io::Error::other(format!(
"terminal stdio or merged ordering failed: {text:?}, {exit:?}"
)));
}
Ok(())
}
fn transient_captured_child_preserves_streams_and_exit() -> std::io::Result<()> {
let target = TargetCommand::new("/bin/sh")
.argument("-c")