process: signal PTY foreground group
This commit is contained in:
parent
40da07538a
commit
f9f8dd0c54
|
|
@ -110,6 +110,7 @@ jobs:
|
||||||
m6-perf-gates:
|
m6-perf-gates:
|
||||||
name: M6 Perf Gates
|
name: M6 Perf Gates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,21 @@ impl ChildHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn signal_target(proc: &ManagedProcess, pid: u32) -> Result<Pid, String> {
|
||||||
|
if let Some(runtime) = proc.runtime.as_ref()
|
||||||
|
&& let ChildHandle::Pty {
|
||||||
|
_master: master, ..
|
||||||
|
} = &runtime.child
|
||||||
|
&& let Some(pgrp) = master.process_group_leader()
|
||||||
|
&& pgrp > 0
|
||||||
|
{
|
||||||
|
return Ok(Pid::from_raw(-pgrp));
|
||||||
|
}
|
||||||
|
Ok(Pid::from_raw(
|
||||||
|
i32::try_from(pid).map_err(|e| e.to_string())?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Termination status of one generation. Internal --- the supervisor
|
/// Termination status of one generation. Internal --- the supervisor
|
||||||
/// translates this into a [`Termination`] with timing.
|
/// translates this into a [`Termination`] with timing.
|
||||||
enum TermStatus {
|
enum TermStatus {
|
||||||
|
|
@ -673,8 +688,10 @@ impl ProcessSupervisor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send `signal` to `id`. Errors if the id is unknown or the
|
/// Send `signal` to `id`. Errors if the id is unknown or the
|
||||||
/// process is not currently running. The signal is applied to
|
/// process is not currently running. Pipe-mode children are
|
||||||
/// the OS pid via [`nix::sys::signal::kill`]; nothing about the
|
/// signaled by OS pid; PTY-mode children are signaled via the
|
||||||
|
/// foreground process group when the kernel reports one, matching
|
||||||
|
/// terminal C-c behavior for shells and REPLs. Nothing about the
|
||||||
/// supervisor's state changes synchronously --- the lifecycle
|
/// supervisor's state changes synchronously --- the lifecycle
|
||||||
/// transition happens when the supervisor next observes the
|
/// transition happens when the supervisor next observes the
|
||||||
/// child's exit through `tick`.
|
/// child's exit through `tick`.
|
||||||
|
|
@ -687,8 +704,8 @@ impl ProcessSupervisor {
|
||||||
else {
|
else {
|
||||||
return Err(format!("process {id} is not running"));
|
return Err(format!("process {id} is not running"));
|
||||||
};
|
};
|
||||||
let nix_pid = Pid::from_raw(i32::try_from(pid).map_err(|e| e.to_string())?);
|
let target = signal_target(proc, pid)?;
|
||||||
nix::sys::signal::kill(nix_pid, Some(signal)).map_err(|e| format!("kill: {e}"))?;
|
nix::sys::signal::kill(target, Some(signal)).map_err(|e| format!("kill: {e}"))?;
|
||||||
if matches!(signal, Signal::SIGTERM | Signal::SIGKILL | Signal::SIGHUP) {
|
if matches!(signal, Signal::SIGTERM | Signal::SIGKILL | Signal::SIGHUP) {
|
||||||
proc.state = ProcessState::Exiting {
|
proc.state = ProcessState::Exiting {
|
||||||
pid,
|
pid,
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ fn m6_5_ctrl_d_on_nonempty_input_deletes_char_forward() {
|
||||||
"#);
|
"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acceptance bullet 3: C-c sends SIGINT. We spawn `cat` (which
|
/// Acceptance bullet 3: C-c sends SIGINT. We spawn `sleep` (which
|
||||||
/// terminates on SIGINT) and verify the exit marker reports the
|
/// terminates on SIGINT) and verify the exit marker reports the
|
||||||
/// expected signal. The signal name in the marker is symbolic
|
/// expected signal. The signal name in the marker is symbolic
|
||||||
/// ("SIGINT") rather than a number, per the M6.5 design (the libc
|
/// ("SIGINT") rather than a number, per the M6.5 design (the libc
|
||||||
|
|
@ -285,12 +285,20 @@ fn m6_5_ctrl_d_on_nonempty_input_deletes_char_forward() {
|
||||||
/// profile under `cargo test`'s default parallelism).
|
/// profile under `cargo test`'s default parallelism).
|
||||||
#[test]
|
#[test]
|
||||||
fn m6_5_ctrl_c_sends_sigint() {
|
fn m6_5_ctrl_c_sends_sigint() {
|
||||||
run_with_pump(
|
let Some(sleep) = locate_shell("sleep") else {
|
||||||
|
eprintln!("skipping: sleep not on PATH (set PMACS_TEST_SLEEP to override)");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let setup = format!(
|
||||||
r#"
|
r#"
|
||||||
_G.h = pmacs.repl.spawn { argv = { "cat" } }
|
_G.h = pmacs.repl.spawn {{ argv = {{ "{sleep}", "30" }} }}
|
||||||
_G.sigint_sent = false
|
_G.sigint_sent = false
|
||||||
_G.first_seen_running_at = nil
|
_G.first_seen_running_at = nil
|
||||||
"#,
|
"#,
|
||||||
|
sleep = sleep.display(),
|
||||||
|
);
|
||||||
|
run_with_pump(
|
||||||
|
&setup,
|
||||||
r#"
|
r#"
|
||||||
local h = _G.h
|
local h = _G.h
|
||||||
if not _G.sigint_sent then
|
if not _G.sigint_sent then
|
||||||
|
|
@ -324,13 +332,21 @@ fn m6_5_ctrl_c_sends_sigint() {
|
||||||
/// The exit marker uses `basename(argv[0])` (so `/usr/bin/cat`
|
/// The exit marker uses `basename(argv[0])` (so `/usr/bin/cat`
|
||||||
/// renders as `cat`), leads with `\n` (so a process exiting mid-line
|
/// renders as `cat`), leads with `\n` (so a process exiting mid-line
|
||||||
/// stays readable), and uses symbolic signal names. Verified by
|
/// stays readable), and uses symbolic signal names. Verified by
|
||||||
/// spawning `/bin/false`, which exits with code 1.
|
/// spawning `false`, which exits with code 1.
|
||||||
#[test]
|
#[test]
|
||||||
fn m6_5_exit_marker_uses_basename_with_leading_newline() {
|
fn m6_5_exit_marker_uses_basename_with_leading_newline() {
|
||||||
run_with_pump(
|
let Some(false_bin) = locate_shell("false") else {
|
||||||
|
eprintln!("skipping: false not on PATH (set PMACS_TEST_FALSE to override)");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let setup = format!(
|
||||||
r#"
|
r#"
|
||||||
_G.h = pmacs.repl.spawn { argv = { "/bin/false" } }
|
_G.h = pmacs.repl.spawn {{ argv = {{ "{false_bin}" }} }}
|
||||||
"#,
|
"#,
|
||||||
|
false_bin = false_bin.display(),
|
||||||
|
);
|
||||||
|
run_with_pump(
|
||||||
|
&setup,
|
||||||
r#"
|
r#"
|
||||||
local h = _G.h
|
local h = _G.h
|
||||||
local buf = h:buffer_id()
|
local buf = h:buffer_id()
|
||||||
|
|
|
||||||
|
|
@ -86,15 +86,13 @@
|
||||||
//! `yes` at the M6.6 ingest rate). Both signals fire in the same
|
//! `yes` at the M6.6 ingest rate). Both signals fire in the same
|
||||||
//! tick, so the choice is purely a measurement-cost decision.
|
//! tick, so the choice is purely a measurement-cost decision.
|
||||||
//!
|
//!
|
||||||
//! - **Why we cancel `yes` directly, not a shell.** `pmacs.process.
|
//! - **Why we cancel `yes` directly, not a shell.** PTY-mode
|
||||||
//! signal(_proc_id, "INT")` signals the *spawned PID*. For a
|
//! `pmacs.process.signal(_proc_id, "INT")` targets the foreground
|
||||||
//! shell, that's bash itself, not bash's child (which is what
|
//! process group. For a shell, that group can include the shell's
|
||||||
//! `find /` would be). Real-terminal SIGINT semantics involve
|
//! current foreground job rather than only the shell process. For
|
||||||
//! foreground-pgrp routing through the PTY layer, which M6.5
|
//! the M6.6 gate, the cleanest measurement is a single-process
|
||||||
//! does not implement. A shell-foreground-pgrp aware C-c is M6.5+
|
//! target (`yes`), so the foreground group contains the producer
|
||||||
//! work; for the M6.6 gate, the cleanest measurement is a
|
//! being measured and no shell/job-control policy enters the timing.
|
||||||
//! single-process target (`yes`), which catches SIGINT and exits
|
|
||||||
//! directly.
|
|
||||||
//!
|
//!
|
||||||
//! - **Percentile computation.** Sort the latency samples; p99 is
|
//! - **Percentile computation.** Sort the latency samples; p99 is
|
||||||
//! `samples[(len * 99) / 100]`, matching M5.9c's exact-integer
|
//! `samples[(len * 99) / 100]`, matching M5.9c's exact-integer
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue