Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
+6
-2
@@ -14,6 +14,10 @@ Every fork change is marked with a `// nash:` comment. Current patches:
|
|||||||
| `brush-core/src/expansion.rs` | corpus divergence D1: added `ExpansionPiece::LiteralText` — literal unquoted text is never field-split (bash splits only expansion results) but keeps glob characters active. Parameter-expansion substitutions (`${v:-word}` etc.) convert back to splittable at the expansion boundary; `ExpanderOptions.field_split_literal_text` lets data-string callers opt in. Upstream candidate. |
|
| `brush-core/src/expansion.rs` | corpus divergence D1: added `ExpansionPiece::LiteralText` — literal unquoted text is never field-split (bash splits only expansion results) but keeps glob characters active. Parameter-expansion substitutions (`${v:-word}` etc.) convert back to splittable at the expansion boundary; `ExpanderOptions.field_split_literal_text` lets data-string callers opt in. Upstream candidate. |
|
||||||
| `brush-core/src/completion.rs` | `compgen -W` word list opts into `field_split_literal_text` (the -W string is data) |
|
| `brush-core/src/completion.rs` | `compgen -W` word list opts into `field_split_literal_text` (the -W string is data) |
|
||||||
| `brush-shell/tests/cases/compat/ifs.yaml` | removed `known_failure` from 3 IFS tests the D1 fix repairs |
|
| `brush-shell/tests/cases/compat/ifs.yaml` | removed `known_failure` from 3 IFS tests the D1 fix repairs |
|
||||||
|
| `brush-core/src/gate.rs` (new) + `lib.rs` | the `Gate` trait (docs/NASH.md §4.2): process-global, verdict-shaped hook; allow-all default so an unconfigured shell behaves exactly like upstream |
|
||||||
|
| `brush-core/src/commands.rs` | `SimpleCommand::execute` split into gate wrapper + `execute_inner`: `on_exec` before dispatch (Deny short-circuits), completion correlated through all three spawn-result variants |
|
||||||
|
| `brush-core/src/processes.rs` | `ChildProcess.gate_token` + `on_exit` reporting from `wait()`/`poll()` (128+signal for signal deaths) |
|
||||||
|
| `brush-shell/src/entry.rs` | `set_exit_hook` seam so nash can flush its event buffer before `process::exit` |
|
||||||
|
|
||||||
Verification (this container): brush compat suite `2067 ran: 1684 succeeded, 6
|
Verification (this container): brush compat suite `2067 ran: 1684 succeeded, 6
|
||||||
failed, 377 known to fail, 38 skipped` — the 6 failures are identical to a
|
failed, 377 known to fail, 38 skipped` — the 6 failures are identical to a
|
||||||
@@ -22,8 +26,8 @@ pristine `brush-shell-v0.4.0` baseline run (environmental: ANSI-C quote and
|
|||||||
zero fork-caused regressions; +3 successes vs baseline are the repaired IFS
|
zero fork-caused regressions; +3 successes vs baseline are the repaired IFS
|
||||||
tests. nash corpus: 94/94.
|
tests. nash corpus: 94/94.
|
||||||
|
|
||||||
Planned (M1, per docs/NASH.md §4.2): `brush-core` `Gate` trait (verdict-shaped
|
Planned (M2, per docs/NASH.md §5.2–5.3): redirect/pipe tap hooks on the `Gate`
|
||||||
observation hooks) — the only substantial divergence from upstream.
|
trait (`on_redirect`/`on_pipe`) in `interp.rs`/`openfiles.rs`.
|
||||||
|
|
||||||
Updating: diff against the upstream tag, re-vendor, re-apply `// nash:` patches
|
Updating: diff against the upstream tag, re-vendor, re-apply `// nash:` patches
|
||||||
(grep for the marker), then re-run `shell/corpus/replay.py` and the brush compat
|
(grep for the marker), then re-run `shell/corpus/replay.py` and the brush compat
|
||||||
|
|||||||
@@ -348,11 +348,76 @@ impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
|
|||||||
/// The command may be a builtin, a shell function, or an externally
|
/// The command may be a builtin, a shell function, or an externally
|
||||||
/// executed command. This function's implementation is responsible for
|
/// executed command. This function's implementation is responsible for
|
||||||
/// dispatching it appropriately according to the context provided.
|
/// dispatching it appropriately according to the context provided.
|
||||||
|
///
|
||||||
|
/// nash: this is the gate choke point (docs/NASH.md §4.2). Every simple
|
||||||
|
/// command passes through the process-global [`crate::gate::Gate`] before
|
||||||
|
/// dispatch; completion is reported back with the issued token, however the
|
||||||
|
/// command runs (inline, spawned process, or spawned task).
|
||||||
|
pub async fn execute(self) -> Result<ExecutionSpawnResult, error::Error> {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let gate = crate::gate::gate();
|
||||||
|
|
||||||
|
let mut argv: Vec<String> = self.args.iter().map(ToString::to_string).collect();
|
||||||
|
if argv.is_empty() {
|
||||||
|
argv.push(self.command_name.clone());
|
||||||
|
}
|
||||||
|
let start_event = crate::gate::ExecStart {
|
||||||
|
argv,
|
||||||
|
cwd: self.shell.working_dir().to_path_buf(),
|
||||||
|
};
|
||||||
|
let token = match gate.on_exec(start_event) {
|
||||||
|
crate::gate::Verdict::Allow(token) => token,
|
||||||
|
crate::gate::Verdict::Deny { status, message } => {
|
||||||
|
let mut stderr = self.params.stderr(&self.shell);
|
||||||
|
let _ = writeln!(stderr, "{}: {message}", self.command_name);
|
||||||
|
return Ok(ExecutionResult::new(status).into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.execute_inner().await {
|
||||||
|
Ok(ExecutionSpawnResult::Completed(result)) => {
|
||||||
|
gate.on_exit(
|
||||||
|
token,
|
||||||
|
crate::gate::ExecEnd {
|
||||||
|
exit_code: i32::from(u8::from(&result.exit_code)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(ExecutionSpawnResult::Completed(result))
|
||||||
|
}
|
||||||
|
Ok(ExecutionSpawnResult::StartedProcess(mut child)) => {
|
||||||
|
child.gate_token = Some(token);
|
||||||
|
Ok(ExecutionSpawnResult::StartedProcess(child))
|
||||||
|
}
|
||||||
|
Ok(ExecutionSpawnResult::StartedTask(handle)) => {
|
||||||
|
let wrapped = tokio::task::spawn(async move {
|
||||||
|
let result = handle.await??;
|
||||||
|
crate::gate::gate().on_exit(
|
||||||
|
token,
|
||||||
|
crate::gate::ExecEnd {
|
||||||
|
exit_code: i32::from(u8::from(&result.exit_code)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(result)
|
||||||
|
});
|
||||||
|
Ok(ExecutionSpawnResult::StartedTask(wrapped))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let exit_code = match err.kind() {
|
||||||
|
error::ErrorKind::CommandNotFound(_) => 127,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
gate.on_exit(token, crate::gate::ExecEnd { exit_code });
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(
|
#[allow(
|
||||||
clippy::missing_panics_doc,
|
clippy::missing_panics_doc,
|
||||||
reason = "these unwrap calls should not panic"
|
reason = "these unwrap calls should not panic"
|
||||||
)]
|
)]
|
||||||
pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
|
async fn execute_inner(mut self) -> Result<ExecutionSpawnResult, error::Error> {
|
||||||
// First see if it's the name of a builtin.
|
// First see if it's the name of a builtin.
|
||||||
let builtin = self.shell.builtins().get(&self.command_name).cloned();
|
let builtin = self.shell.builtins().get(&self.command_name).cloned();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! nash: the observation/enforcement gate (docs/NASH.md §4.2).
|
||||||
|
//!
|
||||||
|
//! A process-global, verdict-shaped hook at the shell's command choke point.
|
||||||
|
//! The default implementation allows everything and observes nothing, so an
|
||||||
|
//! unconfigured shell behaves exactly like upstream brush. nash installs a
|
||||||
|
//! recording gate at startup (`nash-observe`); a future enforcement mode may
|
||||||
|
//! return `Deny` (and, later, hold pending a host policy round-trip) without
|
||||||
|
//! any further changes to this crate.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
/// Details about a simple command about to be executed (builtin, function, or
|
||||||
|
/// external), captured after expansion.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ExecStart {
|
||||||
|
/// Full argv, argv[0] first.
|
||||||
|
pub argv: Vec<String>,
|
||||||
|
/// The shell's working directory at execution time.
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Details about a completed simple command.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ExecEnd {
|
||||||
|
/// Exit code (128+signal for signal deaths).
|
||||||
|
pub exit_code: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The gate's decision for a command about to execute.
|
||||||
|
pub enum Verdict {
|
||||||
|
/// Run the command; the token is echoed back via `on_exit`.
|
||||||
|
Allow(u64),
|
||||||
|
/// Refuse to run the command; the shell reports `message` on stderr and
|
||||||
|
/// the command evaluates to `status`.
|
||||||
|
Deny {
|
||||||
|
/// Exit status the denied command evaluates to.
|
||||||
|
status: u8,
|
||||||
|
/// Message reported to stderr.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process-global hook invoked around every simple command.
|
||||||
|
pub trait Gate: Send + Sync {
|
||||||
|
/// Called before a simple command runs. Must not block in observe-only
|
||||||
|
/// implementations.
|
||||||
|
fn on_exec(&self, ev: ExecStart) -> Verdict;
|
||||||
|
/// Called when a simple command completes, with the token issued by
|
||||||
|
/// `on_exec`.
|
||||||
|
fn on_exit(&self, token: u64, ev: ExecEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AllowAll;
|
||||||
|
|
||||||
|
impl Gate for AllowAll {
|
||||||
|
fn on_exec(&self, _ev: ExecStart) -> Verdict {
|
||||||
|
Verdict::Allow(0)
|
||||||
|
}
|
||||||
|
fn on_exit(&self, _token: u64, _ev: ExecEnd) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
static GATE: OnceLock<Box<dyn Gate>> = OnceLock::new();
|
||||||
|
static ALLOW_ALL: AllowAll = AllowAll;
|
||||||
|
|
||||||
|
/// Installs the process-global gate. May be called at most once, before any
|
||||||
|
/// shell runs; later calls are ignored.
|
||||||
|
pub fn set_gate(gate: Box<dyn Gate>) {
|
||||||
|
let _ = GATE.set(gate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the installed gate, or the allow-all default.
|
||||||
|
pub(crate) fn gate() -> &'static dyn Gate {
|
||||||
|
GATE.get().map_or(&ALLOW_ALL as &dyn Gate, AsRef::as_ref)
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ pub mod expansion;
|
|||||||
mod extendedtests;
|
mod extendedtests;
|
||||||
pub mod extensions;
|
pub mod extensions;
|
||||||
pub mod functions;
|
pub mod functions;
|
||||||
|
// nash: observation/enforcement gate (docs/NASH.md §4.2).
|
||||||
|
pub mod gate;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod int_utils;
|
pub mod int_utils;
|
||||||
pub mod interfaces;
|
pub mod interfaces;
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ pub struct ChildProcess {
|
|||||||
pid: Option<sys::process::ProcessId>,
|
pid: Option<sys::process::ProcessId>,
|
||||||
/// If available, the process group ID of the child.
|
/// If available, the process group ID of the child.
|
||||||
pgid: Option<sys::process::ProcessId>,
|
pgid: Option<sys::process::ProcessId>,
|
||||||
|
// nash: gate token issued by `Gate::on_exec` for this command, reported
|
||||||
|
// back via `Gate::on_exit` when the process completes.
|
||||||
|
pub(crate) gate_token: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChildProcess {
|
impl ChildProcess {
|
||||||
@@ -30,6 +33,7 @@ impl ChildProcess {
|
|||||||
exec_future: Box::pin(child.wait_with_output()),
|
exec_future: Box::pin(child.wait_with_output()),
|
||||||
pid,
|
pid,
|
||||||
pgid,
|
pgid,
|
||||||
|
gate_token: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +58,15 @@ impl ChildProcess {
|
|||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
output = &mut self.exec_future => {
|
output = &mut self.exec_future => {
|
||||||
break Ok(ProcessWaitResult::Completed(output?))
|
let output = output?;
|
||||||
|
// nash: report process completion to the gate exactly once.
|
||||||
|
if let Some(token) = self.gate_token.take() {
|
||||||
|
crate::gate::gate().on_exit(
|
||||||
|
token,
|
||||||
|
crate::gate::ExecEnd { exit_code: exit_code_of(&output.status) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break Ok(ProcessWaitResult::Completed(output))
|
||||||
},
|
},
|
||||||
_ = sigtstp.recv() => {
|
_ = sigtstp.recv() => {
|
||||||
break Ok(ProcessWaitResult::Stopped)
|
break Ok(ProcessWaitResult::Stopped)
|
||||||
@@ -75,10 +87,37 @@ impl ChildProcess {
|
|||||||
|
|
||||||
pub(crate) fn poll(&mut self) -> Option<Result<std::process::Output, error::Error>> {
|
pub(crate) fn poll(&mut self) -> Option<Result<std::process::Output, error::Error>> {
|
||||||
let checkable_future = &mut self.exec_future;
|
let checkable_future = &mut self.exec_future;
|
||||||
checkable_future
|
let result: Option<Result<std::process::Output, error::Error>> = checkable_future
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.map(|result| result.map_err(Into::into))
|
.map(|result| result.map_err(Into::into));
|
||||||
|
// nash: completion can also be observed via poll (e.g. job checks).
|
||||||
|
if let Some(Ok(output)) = &result {
|
||||||
|
if let Some(token) = self.gate_token.take() {
|
||||||
|
crate::gate::gate().on_exit(
|
||||||
|
token,
|
||||||
|
crate::gate::ExecEnd {
|
||||||
|
exit_code: exit_code_of(&output.status),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nash: numeric exit code for gate reporting (128+signal for signal deaths).
|
||||||
|
fn exit_code_of(status: &std::process::ExitStatus) -> i32 {
|
||||||
|
if let Some(code) = status.code() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
if let Some(signal) = status.signal() {
|
||||||
|
return 128 + signal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the result of waiting for an executing process.
|
/// Represents the result of waiting for an executing process.
|
||||||
|
|||||||
@@ -193,9 +193,26 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
run_exit_hook();
|
||||||
std::process::exit(i32::from(exit_code));
|
std::process::exit(i32::from(exit_code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nash: a hook invoked right before the process exits, so an embedding shell
|
||||||
|
// (nash) can flush its observation event buffer (docs/NASH.md §6.1).
|
||||||
|
static EXIT_HOOK: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
/// nash: registers a hook run immediately before the shell process exits.
|
||||||
|
/// May be called at most once; later calls are ignored.
|
||||||
|
pub fn set_exit_hook(hook: Box<dyn Fn() + Send + Sync>) {
|
||||||
|
let _ = EXIT_HOOK.set(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_exit_hook() {
|
||||||
|
if let Some(hook) = EXIT_HOOK.get() {
|
||||||
|
hook();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Installs panic handlers to report our panic and cleanly exit on panic.
|
/// Installs panic handlers to report our panic and cleanly exit on panic.
|
||||||
fn install_panic_handlers() {
|
fn install_panic_handlers() {
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user