Preserve Session Terminal TTY through causal launch
This commit is contained in:
parent
2af8a4f09f
commit
39307f4281
4 changed files with 87 additions and 8 deletions
|
|
@ -48,7 +48,7 @@ fn run_session_exec(arguments: &[OsString]) -> io::Result<()> {
|
||||||
})
|
})
|
||||||
.ok_or_else(|| io::Error::other("session exec requires -- and a nonempty argv"))?;
|
.ok_or_else(|| io::Error::other("session exec requires -- and a nonempty argv"))?;
|
||||||
let _ = separator;
|
let _ = separator;
|
||||||
rustix::process::setsid()?;
|
establish_process_group_leader()?;
|
||||||
let target = argv[1..]
|
let target = argv[1..]
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|
@ -56,6 +56,26 @@ fn run_session_exec(arguments: &[OsString]) -> io::Result<()> {
|
||||||
Err(exec_target(target))
|
Err(exec_target(target))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn establish_process_group_leader() -> io::Result<()> {
|
||||||
|
let pid = rustix::process::getpid();
|
||||||
|
if rustix::process::getpgrp() != pid {
|
||||||
|
rustix::process::setsid()?;
|
||||||
|
}
|
||||||
|
require_process_group_leader(pid, rustix::process::getpgrp())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_process_group_leader(
|
||||||
|
pid: rustix::process::Pid,
|
||||||
|
process_group: rustix::process::Pid,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
if pid != process_group {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"session exec is not its process-group leader",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn report_session_identity(arguments: &[OsString]) -> io::Result<()> {
|
fn report_session_identity(arguments: &[OsString]) -> io::Result<()> {
|
||||||
match arguments {
|
match arguments {
|
||||||
[pid] => report_process_identity(parse_positive_pid(pid)?),
|
[pid] => report_process_identity(parse_positive_pid(pid)?),
|
||||||
|
|
@ -179,6 +199,14 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_group_leader_validation_is_fail_closed() {
|
||||||
|
let leader = rustix::process::Pid::from_raw(41).unwrap();
|
||||||
|
let other_group = rustix::process::Pid::from_raw(42).unwrap();
|
||||||
|
require_process_group_leader(leader, leader).unwrap();
|
||||||
|
assert!(require_process_group_leader(leader, other_group).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn group_presence_is_independent_of_a_designated_leader() {
|
fn group_presence_is_independent_of_a_designated_leader() {
|
||||||
let (_, _, group) = read_process_identity(std::process::id()).unwrap().unwrap();
|
let (_, _, group) = read_process_identity(std::process::id()).unwrap().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@ fn run_session_terminal_matrix() -> Result<(), MatrixError> {
|
||||||
session.session_phase = SessionPhase::Attached;
|
session.session_phase = SessionPhase::Attached;
|
||||||
fixture
|
fixture
|
||||||
.store
|
.store
|
||||||
.apply(&[DomainStoreMutation::PutSession(session)])
|
.apply(&[DomainStoreMutation::PutSession(session.clone())])
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
let command = fixture.bundle.command();
|
let command = fixture.bundle.command();
|
||||||
let run = command
|
let run = command
|
||||||
|
|
@ -383,7 +383,35 @@ fn run_session_terminal_matrix() -> Result<(), MatrixError> {
|
||||||
)
|
)
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
}
|
}
|
||||||
wait_for_signal_lines(&signals, 4)?;
|
wait_for_signal_lines(&mut engine, session_id, &signals, 4)?;
|
||||||
|
let mut forwarded_before_prompt = 0_usize;
|
||||||
|
for _ in 0..2_000 {
|
||||||
|
let messages = engine
|
||||||
|
.poll(session_id, GenerationId::new(7))
|
||||||
|
.map_err(failure)?;
|
||||||
|
if messages.is_empty() {
|
||||||
|
if forwarded_before_prompt > 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(5));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for message in messages {
|
||||||
|
if let SessionTerminalMessage::Output(chunk) = message {
|
||||||
|
forwarded_before_prompt += chunk.bytes.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if forwarded_before_prompt == 0 {
|
||||||
|
return Err(failure(
|
||||||
|
"sandbox route did not forward buffered output before Prompt",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
session.session_phase = SessionPhase::Prompting;
|
||||||
|
fixture
|
||||||
|
.store
|
||||||
|
.apply(&[DomainStoreMutation::PutSession(session.clone())])
|
||||||
|
.map_err(failure)?;
|
||||||
engine
|
engine
|
||||||
.set_route(session_id, GenerationId::new(7), TerminalRoute::Prompt)
|
.set_route(session_id, GenerationId::new(7), TerminalRoute::Prompt)
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
|
|
@ -394,6 +422,11 @@ fn run_session_terminal_matrix() -> Result<(), MatrixError> {
|
||||||
if before != after {
|
if before != after {
|
||||||
return Err(failure("prompt route did not apply PTY backpressure"));
|
return Err(failure("prompt route did not apply PTY backpressure"));
|
||||||
}
|
}
|
||||||
|
session.session_phase = SessionPhase::Attached;
|
||||||
|
fixture
|
||||||
|
.store
|
||||||
|
.apply(&[DomainStoreMutation::PutSession(session)])
|
||||||
|
.map_err(failure)?;
|
||||||
engine
|
engine
|
||||||
.set_route(session_id, GenerationId::new(7), TerminalRoute::Sandbox)
|
.set_route(session_id, GenerationId::new(7), TerminalRoute::Sandbox)
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
|
|
@ -470,8 +503,16 @@ fn wait_for_path(path: &Path) -> Result<(), MatrixError> {
|
||||||
Err(failure("Session Terminal guest did not become ready"))
|
Err(failure("Session Terminal guest did not become ready"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_signal_lines(path: &Path, expected: usize) -> Result<(), MatrixError> {
|
fn wait_for_signal_lines(
|
||||||
|
engine: &mut SessionTerminalEngine,
|
||||||
|
session_id: SessionId,
|
||||||
|
path: &Path,
|
||||||
|
expected: usize,
|
||||||
|
) -> Result<(), MatrixError> {
|
||||||
for _ in 0..2_000 {
|
for _ in 0..2_000 {
|
||||||
|
engine
|
||||||
|
.poll(session_id, GenerationId::new(7))
|
||||||
|
.map_err(failure)?;
|
||||||
if fs::read_to_string(path)
|
if fs::read_to_string(path)
|
||||||
.ok()
|
.ok()
|
||||||
.is_some_and(|value| value.lines().count() >= expected)
|
.is_some_and(|value| value.lines().count() >= expected)
|
||||||
|
|
|
||||||
|
|
@ -1376,7 +1376,12 @@ pub fn parse_wrapper_arguments(
|
||||||
if arguments.next().as_deref() != Some(OsStr::new("--")) {
|
if arguments.next().as_deref() != Some(OsStr::new("--")) {
|
||||||
return Err(io::Error::other("wrapper target separator is missing"));
|
return Err(io::Error::other("wrapper target separator is missing"));
|
||||||
}
|
}
|
||||||
Some(File::open(format!("/proc/self/fd/{descriptor}"))?)
|
Some(
|
||||||
|
File::options()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(format!("/proc/self/fd/{descriptor}"))?,
|
||||||
|
)
|
||||||
} else if next.as_deref() == Some(OsStr::new("--")) {
|
} else if next.as_deref() == Some(OsStr::new("--")) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,9 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
|
||||||
let slave = File::from(slave);
|
let slave = File::from(slave);
|
||||||
let target = TargetCommand::new("/bin/sh")
|
let target = TargetCommand::new("/bin/sh")
|
||||||
.argument("-c")
|
.argument("-c")
|
||||||
.argument("read value; printf 'out:%s\\n' \"$value\"; printf 'err\\n' >&2")
|
.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()?)?;
|
||||||
let expected_parent = current_process_identity()?;
|
let expected_parent = current_process_identity()?;
|
||||||
let ownership = ProcessOwnershipContext {
|
let ownership = ProcessOwnershipContext {
|
||||||
|
|
@ -117,10 +119,13 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
|
||||||
let _ = read_available(&mut master, &mut merged);
|
let _ = read_available(&mut master, &mut merged);
|
||||||
let exit = child_broker().reap_status(process)?;
|
let exit = child_broker().reap_status(process)?;
|
||||||
let text = String::from_utf8_lossy(&merged);
|
let text = String::from_utf8_lossy(&merged);
|
||||||
|
let stdin = text.find("stdin-fd");
|
||||||
let out = text.find("out:hello");
|
let out = text.find("out:hello");
|
||||||
let err = text.find("err");
|
let err = text.find("err");
|
||||||
if !matches!((out, err), (Some(out), Some(err)) if out < err)
|
if !matches!(
|
||||||
|| exit.exit_code != Some(0)
|
(stdin, out, err),
|
||||||
|
(Some(stdin), Some(out), Some(err)) if stdin < out && out < err
|
||||||
|
) || exit.exit_code != Some(0)
|
||||||
|| exit.terminating_signal.is_some()
|
|| exit.terminating_signal.is_some()
|
||||||
{
|
{
|
||||||
return Err(std::io::Error::other(format!(
|
return Err(std::io::Error::other(format!(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue