Merge nucleic/nimble-umber-toad-20h7 into dev
This commit is contained in:
+15
-15
@@ -2,42 +2,42 @@
|
||||
|
||||
Vendored copy of [reubeno/brush](https://github.com/reubeno/brush), pinned at
|
||||
tag **`brush-shell-v0.4.0`** (upstream commit `96a26d0`), consumed by the
|
||||
`shell/` workspace to build **nash**, the Nucleic agent shell
|
||||
([docs/NASH.md](../../docs/NASH.md)).
|
||||
`shell/` workspace to build **hash**, the Hydrangea agent shell
|
||||
([docs/HYDRASHELL.md](../../docs/HYDRASHELL.md)).
|
||||
|
||||
Every fork change is marked with a `// nash:` comment. Current patches:
|
||||
Every fork change is marked with a `// hydrashell:` comment. Current patches:
|
||||
|
||||
| File | Change |
|
||||
| --- | --- |
|
||||
| `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `nash`; display string identifies the fork |
|
||||
| `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `hydrashell`; display string identifies the fork |
|
||||
| `brush-shell/src/args.rs` | usage/version strings rebranded |
|
||||
| `brush-core/src/prompt.rs` | `\$` (`PromptPiece::DollarOrPound`) renders the atom sign `⚛` (U+269B) for non-root instead of `$`; root still shows `#` |
|
||||
| `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-shell/tests/cases/compat/ifs.yaml` | removed `known_failure` from 3 IFS tests the D1 fix repairs |
|
||||
| `brush-builtins/src/dot.rs` + `brush-shell/tests/cases/compat/builtins/dot.yaml` | the `.` / `source` builtin searches `PATH` for slashless operands when `sourcepath` is enabled, as Bash/POSIX require; fixes `git-subtree` sourcing `git-sh-setup` |
|
||||
| `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/gate.rs` (new) + `lib.rs` | the `Gate` trait (docs/HYDRASHELL.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` |
|
||||
| `brush-core/src/gate.rs` | M2 (docs/NASH.md §5.2–5.3): `RedirectRecord`/`RedirectReadback` + `ExecStart.redirects` + `Gate::on_cmdsub` default hook |
|
||||
| `brush-core/src/interp.rs` | `ExecutionParameters.nash_redirects` accumulator; `setup_redirect` records file (`>`,`>>`,`<`,`2>`,`&>`,`<>`), heredoc, and here-string redirects |
|
||||
| `brush-core/src/commands.rs` | gate wrapper drains `nash_redirects` into `ExecStart` for post-run read-back |
|
||||
| `brush-shell/src/entry.rs` | `set_exit_hook` seam so hash can flush its event buffer before `process::exit` |
|
||||
| `brush-core/src/gate.rs` | M2 (docs/HYDRASHELL.md §5.2–5.3): `RedirectRecord`/`RedirectReadback` + `ExecStart.redirects` + `Gate::on_cmdsub` default hook |
|
||||
| `brush-core/src/interp.rs` | `ExecutionParameters.hydrashell_redirects` accumulator; `setup_redirect` records file (`>`,`>>`,`<`,`2>`,`&>`,`<>`), heredoc, and here-string redirects |
|
||||
| `brush-core/src/commands.rs` | gate wrapper drains `hydrashell_redirects` into `ExecStart` for post-run read-back |
|
||||
| `brush-core/src/expansion.rs` | command-substitution arm calls `gate().on_cmdsub` with the trimmed output |
|
||||
| `brush-core/src/gate.rs` | pipe tap (docs/NASH.md §5.3): `PipeEvent` + `Gate::on_pipe` default hook + `is_active()`/`next_pipeline_id()` gating |
|
||||
| `brush-core/src/interp.rs` | `spawn_pipeline_processes` interposes a tee on each `a \| b` link when observation is active (`nash_spawn_pipe_tee`): a copier thread mirrors a bounded prefix, preserving backpressure and SIGPIPE, reports before closing the write end. No tee on the unobserved fast path. |
|
||||
| `brush-core/src/gate.rs` | pipe tap (docs/HYDRASHELL.md §5.3): `PipeEvent` + `Gate::on_pipe` default hook + `is_active()`/`next_pipeline_id()` gating |
|
||||
| `brush-core/src/interp.rs` | `spawn_pipeline_processes` interposes a tee on each `a \| b` link when observation is active (`hydrashell_spawn_pipe_tee`): a copier thread mirrors a bounded prefix, preserving backpressure and SIGPIPE, reports before closing the write end. No tee on the unobserved fast path. |
|
||||
|
||||
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
|
||||
pristine `brush-shell-v0.4.0` baseline run (environmental: ANSI-C quote and
|
||||
`echo -e` oracle differences, syntax-error formatting, missing `hexdump`), i.e.
|
||||
zero fork-caused regressions; +3 successes vs baseline are the repaired IFS
|
||||
tests. nash corpus: 94/94.
|
||||
tests. hash corpus: 94/94.
|
||||
|
||||
Planned (M3, per docs/NASH.md / NAROS.md): image-side delivery — nash baked and
|
||||
forced on every Linux surface via narOS. No further brush-core patches expected
|
||||
Planned (M3, per docs/HYDRASHELL.md / HYDRANGEAOS.md): image-side delivery — hash baked and
|
||||
forced on every Linux surface via hydrangeaOS. No further brush-core patches expected
|
||||
for M3; enforcement (`Hold`/`Deny` verdicts) remains post-M6.
|
||||
|
||||
Updating: diff against the upstream tag, re-vendor, re-apply `// nash:` patches
|
||||
Updating: diff against the upstream tag, re-vendor, re-apply `// hydrashell:` patches
|
||||
(grep for the marker), then re-run `shell/corpus/replay.py` and the brush compat
|
||||
suite (`cargo test -p brush-shell --test brush-compat-tests`).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# nucleic-brush
|
||||
|
||||
A fork of [**brush**](https://github.com/reubeno/brush) — the Bo(u)rn(e) RUsty SHell by
|
||||
reuben olinsky — carrying the patches Nucleic needs to build **nash**, its agent shell.
|
||||
reuben olinsky — carrying the patches Nucleic needs to build **hash**, its agent shell.
|
||||
|
||||
Pinned at upstream tag **`brush-shell-v0.4.0`** (commit `96a26d0`).
|
||||
|
||||
@@ -18,11 +18,11 @@ commands, their exits, and the data crossing pipes and redirections — from ins
|
||||
shell rather than by wrapping it. That is what these patches provide. Everything else
|
||||
is upstream brush, unmodified.
|
||||
|
||||
Every divergence carries a `// nash:` comment, so the complete diff against upstream is
|
||||
Every divergence carries a `// hydrashell:` comment, so the complete diff against upstream is
|
||||
one grep away:
|
||||
|
||||
```console
|
||||
$ grep -rn "nash:" --include="*.rs" --include="*.yaml" .
|
||||
$ grep -rn "hydrashell:" --include="*.rs" --include="*.yaml" .
|
||||
```
|
||||
|
||||
### 1. The observation gate
|
||||
@@ -65,12 +65,12 @@ and belong upstream:
|
||||
|
||||
### 3. Branding
|
||||
|
||||
The binary this workspace builds is still named `brush`, but identifies itself as nash
|
||||
The binary this workspace builds is still named `brush`, but identifies itself as hash
|
||||
so an agent-facing shell doesn't misrepresent what it is:
|
||||
|
||||
| File | Change |
|
||||
| --- | --- |
|
||||
| `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `nash`; display string reads `nash (Nucleic agent shell, brush fork) …` |
|
||||
| `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `hydrashell`; display string reads `hydrashell (hash — Hydrangea agent shell, brush fork) …` |
|
||||
| `brush-shell/src/args.rs` | usage and version strings rebranded |
|
||||
| `brush-core/src/prompt.rs` | `\$` renders the atom sign `⚛` (U+269B) for non-root; root still shows `#` |
|
||||
|
||||
@@ -80,8 +80,8 @@ current verification status.
|
||||
## How it's consumed
|
||||
|
||||
Nucleic's `shell/` workspace depends on `brush-shell` and `brush-parser` by path. The
|
||||
`nash` binary is a thin wrapper that loads a root-owned operator policy, installs its
|
||||
`Gate` implementation (`nash-observe`), flushes events at exit, and otherwise defers
|
||||
`hydrashell` binary is a thin wrapper that loads a root-owned operator policy, installs its
|
||||
`Gate` implementation (`hydrashell-observe`), flushes events at exit, and otherwise defers
|
||||
entirely to brush's bash-compatible CLI. None of the fork's crates are published; the
|
||||
`brush` binary built from here is a development artifact.
|
||||
|
||||
@@ -105,7 +105,7 @@ ones a baseline run reproduces, plus the 3 IFS tests the expansion fix repairs.
|
||||
|
||||
1. Diff this tree against the pinned upstream tag.
|
||||
2. Re-vendor the new tag.
|
||||
3. Re-apply the fork patches — `grep -rn "nash:"` on the old tree enumerates all of
|
||||
3. Re-apply the fork patches — `grep -rn "hydrashell:"` on the old tree enumerates all of
|
||||
them.
|
||||
4. Re-run the brush compat suite and Nucleic's transcript-replay corpus.
|
||||
5. Update the pin and the patch table in [`NUCLEIC_FORK.md`](NUCLEIC_FORK.md).
|
||||
|
||||
@@ -22,7 +22,7 @@ impl builtins::Command for DotCommand {
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
// TODO(dot): Handle trap inheritance.
|
||||
// nash: POSIX requires `.` to search PATH when its operand has no slash.
|
||||
// hydrashell: POSIX requires `.` to search PATH when its operand has no slash.
|
||||
// Git's `git-subtree` relies on this with `. git-sh-setup`; treating the
|
||||
// operand as cwd-relative leaves `require_work_tree` undefined.
|
||||
let script_path = Path::new(&self.script_path);
|
||||
|
||||
@@ -298,16 +298,16 @@ struct InputReader {
|
||||
deadline: Option<Instant>,
|
||||
/// Single-byte read buffer.
|
||||
///
|
||||
/// nash(D2): multi-byte UTF-8 sequences are buffered and decoded incrementally in
|
||||
/// hydrashell(D2): multi-byte UTF-8 sequences are buffered and decoded incrementally in
|
||||
/// `read_event` (up to 4 bytes via `std::str::from_utf8`); invalid sequences degrade
|
||||
/// to per-byte Latin-1 chars (the pre-fix behavior for every byte). bash's `-n`
|
||||
/// counts bytes, which is preserved because the char-limit check measures
|
||||
/// `line.len()` — the accumulated String's UTF-8 byte length.
|
||||
buffer: [u8; 1],
|
||||
/// nash(D2): a byte consumed while expecting a UTF-8 continuation that turned out to
|
||||
/// hydrashell(D2): a byte consumed while expecting a UTF-8 continuation that turned out to
|
||||
/// start the next character; re-consumed before the stream is read again.
|
||||
pushback: Option<u8>,
|
||||
/// nash(D2): decoded-but-undelivered chars from an invalid sequence flushed as
|
||||
/// hydrashell(D2): decoded-but-undelivered chars from an invalid sequence flushed as
|
||||
/// Latin-1; drained before the stream is read again.
|
||||
pending: VecDeque<char>,
|
||||
/// Terminal mode guard - kept alive for RAII cleanup on drop.
|
||||
@@ -356,7 +356,7 @@ impl InputReader {
|
||||
brush_core::sys::poll::poll_for_input(&self.input, Duration::ZERO).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// nash(D2): reads one byte, honoring the pushback slot and the read deadline.
|
||||
/// hydrashell(D2): reads one byte, honoring the pushback slot and the read deadline.
|
||||
/// Returns `Ok(None)` on EOF; timeout is reported via `TimedOut` in errors' stead by
|
||||
/// the caller checking the deadline, so this only polls when a deadline exists.
|
||||
fn read_byte(&mut self) -> Result<Option<u8>, brush_core::Error> {
|
||||
@@ -372,10 +372,10 @@ impl InputReader {
|
||||
|
||||
/// Reads the next input event, handling timeout and control characters.
|
||||
///
|
||||
/// nash(D2): decodes UTF-8 incrementally instead of casting each byte to `char`
|
||||
/// hydrashell(D2): decodes UTF-8 incrementally instead of casting each byte to `char`
|
||||
/// (which was a Latin-1 decode and re-encoded input bytes — divergence D2 in
|
||||
/// shell/corpus/DIVERGENCES.md, found when ca-certificates' postinst mangled a
|
||||
/// UTF-8 filename under narOS). Valid multi-byte sequences round-trip
|
||||
/// UTF-8 filename under hydrangeaOS). Valid multi-byte sequences round-trip
|
||||
/// byte-identically through the accumulated String; invalid sequences degrade to
|
||||
/// the pre-fix per-byte Latin-1 mapping (a String cannot hold raw invalid bytes —
|
||||
/// full byte-fidelity for invalid UTF-8 would need Vec<u8> plumbing shell-wide).
|
||||
|
||||
@@ -349,7 +349,7 @@ impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
|
||||
/// executed command. This function's implementation is responsible for
|
||||
/// dispatching it appropriately according to the context provided.
|
||||
///
|
||||
/// nash: this is the gate choke point (docs/NASH.md §4.2). Every simple
|
||||
/// hydrashell: this is the gate choke point (docs/HYDRASHELL.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).
|
||||
@@ -362,13 +362,13 @@ impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
|
||||
if argv.is_empty() {
|
||||
argv.push(self.command_name.clone());
|
||||
}
|
||||
// nash: hand this command's redirects to the observer for post-run read-back.
|
||||
let redirects = std::mem::take(&mut self.params.nash_redirects);
|
||||
// hydrashell: hand this command's redirects to the observer for post-run read-back.
|
||||
let redirects = std::mem::take(&mut self.params.hydrashell_redirects);
|
||||
let start_event = crate::gate::ExecStart {
|
||||
argv,
|
||||
cwd: self.shell.working_dir().to_path_buf(),
|
||||
redirects,
|
||||
pipeline: self.params.nash_pipeline,
|
||||
pipeline: self.params.hydrashell_pipeline,
|
||||
};
|
||||
let token = match gate.on_exec(start_event) {
|
||||
crate::gate::Verdict::Allow(token) => token,
|
||||
@@ -836,11 +836,11 @@ pub(crate) async fn invoke_command_in_subshell_and_get_output(
|
||||
// Get our own set of parameters we can customize and use.
|
||||
let mut params = params.clone();
|
||||
params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
|
||||
// nash: a command substitution is an *argument* to the stage, not the stage itself, and it
|
||||
// hydrashell: a command substitution is an *argument* to the stage, not the stage itself, and it
|
||||
// runs during expansion — i.e. before the stage's own command. Letting it inherit the slot
|
||||
// made `grep "$(printf x)" f | wc -l` report `printf x` as the producing stage
|
||||
// (docs/NASH.md §5.1).
|
||||
params.nash_pipeline = None;
|
||||
// (docs/HYDRASHELL.md §5.1).
|
||||
params.hydrashell_pipeline = None;
|
||||
|
||||
// Set up pipe so we can read the output.
|
||||
let (reader, writer) = std::io::pipe()?;
|
||||
|
||||
@@ -308,7 +308,7 @@ impl Spec {
|
||||
// and field splitting but NOT pathname expansion (globbing).
|
||||
let options = crate::expansion::ExpanderOptions {
|
||||
pathname_expand: false,
|
||||
// nash: the -W string is data — its literal text is split too.
|
||||
// hydrashell: the -W string is data — its literal text is split too.
|
||||
field_split_literal_text: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
+14
-14
@@ -37,7 +37,7 @@ pub(crate) struct ExpanderOptions {
|
||||
/// Whether to perform pathname expansion (globbing). If disabled, glob patterns
|
||||
/// are returned as literal strings.
|
||||
pub pathname_expand: bool,
|
||||
// nash: whether literal (unquoted, non-expansion) text is also subject to
|
||||
// hydrashell: whether literal (unquoted, non-expansion) text is also subject to
|
||||
// field splitting. Off by default per POSIX/bash (only expansion results
|
||||
// split); enabled by callers whose input string is *data*, e.g. the
|
||||
// `compgen -W` word list.
|
||||
@@ -275,7 +275,7 @@ impl From<String> for WordField {
|
||||
enum ExpansionPiece {
|
||||
Unsplittable(String),
|
||||
Splittable(String),
|
||||
// nash: literal unquoted word text. Bash field-splits only the *results of
|
||||
// hydrashell: literal unquoted word text. Bash field-splits only the *results of
|
||||
// expansions*, never literal text — but glob characters in literal text stay
|
||||
// active. This third state fixes `IFS=,; echo a,b,c` printing `a b c`
|
||||
// (corpus divergence D1) without breaking `echo *.txt`.
|
||||
@@ -297,7 +297,7 @@ impl From<ExpansionPiece> for patterns::PatternPiece {
|
||||
match piece {
|
||||
ExpansionPiece::Unsplittable(s) => Self::Literal(s),
|
||||
ExpansionPiece::Splittable(s) => Self::Pattern(s),
|
||||
// nash: unquoted literal text keeps glob chars active.
|
||||
// hydrashell: unquoted literal text keeps glob chars active.
|
||||
ExpansionPiece::LiteralText(s) => Self::Pattern(s),
|
||||
}
|
||||
}
|
||||
@@ -308,7 +308,7 @@ impl From<ExpansionPiece> for crate::regex::RegexPiece {
|
||||
match piece {
|
||||
ExpansionPiece::Unsplittable(s) => Self::Literal(s),
|
||||
ExpansionPiece::Splittable(s) => Self::Pattern(s),
|
||||
// nash: unquoted literal text keeps pattern chars active.
|
||||
// hydrashell: unquoted literal text keeps pattern chars active.
|
||||
ExpansionPiece::LiteralText(s) => Self::Pattern(s),
|
||||
}
|
||||
}
|
||||
@@ -335,12 +335,12 @@ impl ExpansionPiece {
|
||||
match self {
|
||||
Self::Unsplittable(_) => self,
|
||||
Self::Splittable(s) => Self::Unsplittable(s),
|
||||
// nash: quoting literal text also deactivates its glob chars.
|
||||
// hydrashell: quoting literal text also deactivates its glob chars.
|
||||
Self::LiteralText(s) => Self::Unsplittable(s),
|
||||
}
|
||||
}
|
||||
|
||||
// nash: opt literal text into field splitting (see
|
||||
// hydrashell: opt literal text into field splitting (see
|
||||
// `ExpanderOptions::field_split_literal_text`). Quoted pieces stay intact.
|
||||
fn make_literal_splittable(self) -> Self {
|
||||
match self {
|
||||
@@ -547,7 +547,7 @@ struct WordExpander<'a, SE: extensions::ShellExtensions> {
|
||||
in_double_quotes: bool,
|
||||
/// Whether to use heredoc expansion semantics (literal quotes, no brace expansion).
|
||||
heredoc_mode: bool,
|
||||
// nash: see `ExpanderOptions::field_split_literal_text`.
|
||||
// hydrashell: see `ExpanderOptions::field_split_literal_text`.
|
||||
field_split_literal_text: bool,
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
|
||||
&['$', '`', '\\', '\'', '\"', '~', '{']
|
||||
};
|
||||
if !word.contains(expansion_chars) {
|
||||
// nash: a word with no expansion characters is pure literal text —
|
||||
// hydrashell: a word with no expansion characters is pure literal text —
|
||||
// glob-active but not subject to field splitting.
|
||||
return Ok(Expansion::from(ExpansionPiece::LiteralText(
|
||||
word.to_owned(),
|
||||
@@ -821,7 +821,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
|
||||
// Go through the fields we have so far.
|
||||
for existing_field in expansion.fields {
|
||||
for piece in existing_field.0 {
|
||||
// nash: callers whose input is data (e.g. compgen -W) opt
|
||||
// hydrashell: callers whose input is data (e.g. compgen -W) opt
|
||||
// literal text into splitting.
|
||||
let piece = if self.field_split_literal_text {
|
||||
piece.make_literal_splittable()
|
||||
@@ -829,7 +829,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
|
||||
piece
|
||||
};
|
||||
match piece {
|
||||
// nash: literal text is never field-split (only expansion
|
||||
// hydrashell: literal text is never field-split (only expansion
|
||||
// results are), but stays a distinct piece so its glob
|
||||
// characters remain active downstream.
|
||||
ExpansionPiece::Unsplittable(_) | ExpansionPiece::LiteralText(_) => {
|
||||
@@ -913,7 +913,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
|
||||
) -> Result<Expansion, error::Error> {
|
||||
let expansion: Expansion = match word_piece {
|
||||
brush_parser::word::WordPiece::Text(s) => {
|
||||
// nash: literal word text — not field-split, glob-active.
|
||||
// hydrashell: literal word text — not field-split, glob-active.
|
||||
Expansion::from(ExpansionPiece::LiteralText(s))
|
||||
}
|
||||
brush_parser::word::WordPiece::SingleQuotedText(s) => {
|
||||
@@ -963,7 +963,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
|
||||
ExpansionPiece::Unsplittable(self.expand_tilde_expression(&tilde_expr)?),
|
||||
),
|
||||
brush_parser::word::WordPiece::ParameterExpansion(p) => {
|
||||
// nash: whatever a parameter expansion substitutes (including
|
||||
// hydrashell: whatever a parameter expansion substitutes (including
|
||||
// literal text from ${v:-default} / ${v:+alt} words) is an
|
||||
// expansion *result*, so it is subject to field splitting.
|
||||
// Quoted pieces inside remain Unsplittable.
|
||||
@@ -999,7 +999,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
|
||||
let trimmed_len = cmd_output.trim_end_matches('\n').len();
|
||||
cmd_output.truncate(trimmed_len);
|
||||
|
||||
// nash: capture command-substitution output (docs/NASH.md §5.3) —
|
||||
// hydrashell: capture command-substitution output (docs/HYDRASHELL.md §5.3) —
|
||||
// invisible to the transcript, often carries decisions/secrets.
|
||||
if !self.disable_command_substitutions {
|
||||
crate::gate::gate().on_cmdsub(cmd_output.as_str());
|
||||
@@ -1012,7 +1012,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
|
||||
// If we are *not* in a double-quoted context and we were requested to skip
|
||||
// unquoted backslash removal, then we need to skip removing backslashes here.
|
||||
if !self.in_double_quotes && self.disable_unquoted_backslash_removal {
|
||||
// nash: retained-backslash text is literal, not an
|
||||
// hydrashell: retained-backslash text is literal, not an
|
||||
// expansion result — no field splitting.
|
||||
return Ok(Expansion::from(ExpansionPiece::LiteralText(s)));
|
||||
}
|
||||
|
||||
+14
-14
@@ -1,9 +1,9 @@
|
||||
//! nash: the observation/enforcement gate (docs/NASH.md §4.2).
|
||||
//! hydrashell: the observation/enforcement gate (docs/HYDRASHELL.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
|
||||
//! unconfigured shell behaves exactly like upstream brush. hash installs a
|
||||
//! recording gate at startup (`hydrashell-observe`); a future enforcement mode may
|
||||
//! return `Deny` (and, later, hold pending a host policy round-trip) without
|
||||
//! any further changes to this crate.
|
||||
|
||||
@@ -19,15 +19,15 @@ pub struct ExecStart {
|
||||
pub argv: Vec<String>,
|
||||
/// The shell's working directory at execution time.
|
||||
pub cwd: PathBuf,
|
||||
/// nash: the redirections applied to this command (docs/NASH.md §5.2). The
|
||||
/// hydrashell: the redirections applied to this command (docs/HYDRASHELL.md §5.2). The
|
||||
/// observer reads back the affected byte range after the command completes.
|
||||
pub redirects: Vec<RedirectRecord>,
|
||||
/// nash: the pipeline stage this command occupies, when it runs as part of
|
||||
/// `a | b | c` (docs/NASH.md §5.1). `None` for a standalone command.
|
||||
/// hydrashell: the pipeline stage this command occupies, when it runs as part of
|
||||
/// `a | b | c` (docs/HYDRASHELL.md §5.1). `None` for a standalone command.
|
||||
pub pipeline: Option<PipelineSlot>,
|
||||
}
|
||||
|
||||
/// Where a command sits in the pipeline it belongs to (docs/NASH.md §5.1).
|
||||
/// Where a command sits in the pipeline it belongs to (docs/HYDRASHELL.md §5.1).
|
||||
///
|
||||
/// Shares its `id` with the [`PipeEvent`]s of the same pipeline, which is what
|
||||
/// lets an observer name the commands either side of a link (`curl … | sh`)
|
||||
@@ -42,7 +42,7 @@ pub struct PipelineSlot {
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
/// How a redirection's data is captured after the command runs (docs/NASH.md §5.2).
|
||||
/// How a redirection's data is captured after the command runs (docs/HYDRASHELL.md §5.2).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RedirectReadback {
|
||||
/// A truncating write (`>`, `>|`, `&>`): read the head of the final file.
|
||||
@@ -55,7 +55,7 @@ pub enum RedirectReadback {
|
||||
Inline,
|
||||
}
|
||||
|
||||
/// One redirection observed on a command. For regular files nash hands the child
|
||||
/// One redirection observed on a command. For regular files hash hands the child
|
||||
/// the *real* fd (semantics untouched) and reads back a bounded range afterward;
|
||||
/// for heredoc/here-string the body is captured inline at setup.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -81,7 +81,7 @@ pub struct ExecEnd {
|
||||
pub exit_code: i32,
|
||||
}
|
||||
|
||||
/// Data observed flowing across one pipe link `a | b` (docs/NASH.md §5.3).
|
||||
/// Data observed flowing across one pipe link `a | b` (docs/HYDRASHELL.md §5.3).
|
||||
/// Reported once the link drains (producer closed or consumer went away).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PipeEvent {
|
||||
@@ -133,10 +133,10 @@ pub trait Gate: Send + Sync {
|
||||
/// Called when a simple command completes, with the token issued by
|
||||
/// `on_exec`.
|
||||
fn on_exit(&self, token: u64, ev: ExecEnd);
|
||||
/// nash: called with the (trimmed) output of a command substitution
|
||||
/// `$(…)` / backticks (docs/NASH.md §5.3). Default no-op.
|
||||
/// hydrashell: called with the (trimmed) output of a command substitution
|
||||
/// `$(…)` / backticks (docs/HYDRASHELL.md §5.3). Default no-op.
|
||||
fn on_cmdsub(&self, _output: &str) {}
|
||||
/// nash: called once a pipe link `a | b` drains (docs/NASH.md §5.3).
|
||||
/// hydrashell: called once a pipe link `a | b` drains (docs/HYDRASHELL.md §5.3).
|
||||
/// Invoked from the tee copier thread. Default no-op.
|
||||
fn on_pipe(&self, _ev: PipeEvent) {}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ pub(crate) fn gate() -> &'static dyn Gate {
|
||||
}
|
||||
|
||||
/// Whether a real (non-default) gate is installed. Pipe teeing — which adds a
|
||||
/// thread + extra pipe per link — is only done when this is true (docs/NASH.md §5.3).
|
||||
/// thread + extra pipe per link — is only done when this is true (docs/HYDRASHELL.md §5.3).
|
||||
pub(crate) fn is_active() -> bool {
|
||||
GATE.get().is_some()
|
||||
}
|
||||
|
||||
+111
-111
@@ -37,16 +37,16 @@ pub struct ExecutionParameters {
|
||||
/// Whether `errexit` (exit on error) behavior should be
|
||||
/// suppressed in this execution context. Defaults to `false`.
|
||||
pub suppress_errexit: bool,
|
||||
// nash: redirections applied to the command being built, drained into its
|
||||
// `gate::ExecStart` for post-run read-back (docs/NASH.md §5.2). Empty unless
|
||||
// hydrashell: redirections applied to the command being built, drained into its
|
||||
// `gate::ExecStart` for post-run read-back (docs/HYDRASHELL.md §5.2). Empty unless
|
||||
// observation is active and the command has redirects.
|
||||
pub(crate) nash_redirects: Vec<crate::gate::RedirectRecord>,
|
||||
// nash: the pipeline stage the command being built belongs to, stamped onto
|
||||
pub(crate) hydrashell_redirects: Vec<crate::gate::RedirectRecord>,
|
||||
// hydrashell: the pipeline stage the command being built belongs to, stamped onto
|
||||
// its `gate::ExecStart` so the observer can name the stages either side of a
|
||||
// pipe link (docs/NASH.md §5.1). Inherited by nested commands — a command
|
||||
// pipe link (docs/HYDRASHELL.md §5.1). Inherited by nested commands — a command
|
||||
// inside a stage's subshell or brace group belongs to that same stage — and
|
||||
// overwritten only when a nested pipeline starts its own.
|
||||
pub(crate) nash_pipeline: Option<crate::gate::PipelineSlot>,
|
||||
pub(crate) hydrashell_pipeline: Option<crate::gate::PipelineSlot>,
|
||||
}
|
||||
|
||||
impl ExecutionParameters {
|
||||
@@ -494,11 +494,11 @@ async fn spawn_pipeline_processes(
|
||||
let mut spawn_results = VecDeque::new();
|
||||
let mut process_group_id: Option<i32> = None;
|
||||
|
||||
// nash: when observation is active, one id per multi-stage pipeline correlates
|
||||
// hydrashell: when observation is active, one id per multi-stage pipeline correlates
|
||||
// both its tapped links and the exec events of the stages either side of them
|
||||
// (docs/NASH.md §5.3). `None` on the unobserved fast path and for a lone
|
||||
// (docs/HYDRASHELL.md §5.3). `None` on the unobserved fast path and for a lone
|
||||
// command, where there is no link to tap and nothing to correlate.
|
||||
let nash_pipeline_id = if pipeline_len > 1 && crate::gate::is_active() {
|
||||
let hydrashell_pipeline_id = if pipeline_len > 1 && crate::gate::is_active() {
|
||||
Some(crate::gate::next_pipeline_id())
|
||||
} else {
|
||||
None
|
||||
@@ -508,9 +508,9 @@ async fn spawn_pipeline_processes(
|
||||
// command.
|
||||
if pipeline_len > 1 {
|
||||
for link in 0..(pipeline_len - 1) {
|
||||
if let Some(pipeline_id) = nash_pipeline_id {
|
||||
// nash: interpose a tee on the link so the bytes flowing `a | b` are
|
||||
// captured (docs/NASH.md §5.3).
|
||||
if let Some(pipeline_id) = hydrashell_pipeline_id {
|
||||
// hydrashell: interpose a tee on the link so the bytes flowing `a | b` are
|
||||
// captured (docs/HYDRASHELL.md §5.3).
|
||||
// Producer writes `a_write`; a copier moves bytes to `b_write`;
|
||||
// consumer reads `b_read`. Slots stay identical to the untapped
|
||||
// case, so the pop-ordering below is unchanged.
|
||||
@@ -521,13 +521,13 @@ async fn spawn_pipeline_processes(
|
||||
// Name both ends from their AST *here*, where the stage is unambiguous. Deriving
|
||||
// the name later from exec events cannot distinguish which command inside a
|
||||
// compound stage owned the pipe, and cannot name either end until it exits.
|
||||
nash_spawn_pipe_tee(
|
||||
hydrashell_spawn_pipe_tee(
|
||||
a_read,
|
||||
b_write,
|
||||
pipeline_id,
|
||||
from_index,
|
||||
nash_stage_text(pipeline, from_index),
|
||||
nash_stage_text(pipeline, from_index + 1),
|
||||
hydrashell_stage_text(pipeline, from_index),
|
||||
hydrashell_stage_text(pipeline, from_index + 1),
|
||||
);
|
||||
pipe_readers.push(Some(openfiles::OpenFile::PipeReader(b_read)));
|
||||
pipe_writers.push(Some(openfiles::OpenFile::PipeWriter(a_write)));
|
||||
@@ -559,11 +559,11 @@ async fn spawn_pipeline_processes(
|
||||
// Set up parameters appropriate for this command.
|
||||
let mut cmd_params = params.clone();
|
||||
|
||||
// nash: stamp this stage's slot so its exec event can be joined to the links
|
||||
// hydrashell: stamp this stage's slot so its exec event can be joined to the links
|
||||
// either side of it. Only when this pipeline is itself tapped — a lone command
|
||||
// must keep any slot inherited from the stage that contains it.
|
||||
if let Some(id) = nash_pipeline_id {
|
||||
cmd_params.nash_pipeline = Some(crate::gate::PipelineSlot {
|
||||
if let Some(id) = hydrashell_pipeline_id {
|
||||
cmd_params.hydrashell_pipeline = Some(crate::gate::PipelineSlot {
|
||||
id,
|
||||
index: current_pipeline_index,
|
||||
len: pipeline_len,
|
||||
@@ -1728,11 +1728,11 @@ pub(crate) async fn setup_redirect(
|
||||
}
|
||||
|
||||
let expanded_file_path = expanded_fields.remove(0);
|
||||
// nash: `&>` / `&>>` writes stdout+stderr to one file — record fd 1
|
||||
// for read-back (docs/NASH.md §5.2).
|
||||
// hydrashell: `&>` / `&>>` writes stdout+stderr to one file — record fd 1
|
||||
// for read-back (docs/HYDRASHELL.md §5.2).
|
||||
let abs = shell.absolute_path(Path::new(expanded_file_path.as_str()));
|
||||
let size_before = std::fs::metadata(&abs).map(|m| m.len()).unwrap_or(0);
|
||||
params.nash_redirects.push(crate::gate::RedirectRecord {
|
||||
params.hydrashell_redirects.push(crate::gate::RedirectRecord {
|
||||
op: if *append { "&>>".to_string() } else { "&>".to_string() },
|
||||
fd: 1,
|
||||
path: Some(abs),
|
||||
@@ -1820,9 +1820,9 @@ pub(crate) async fn setup_redirect(
|
||||
)
|
||||
})?;
|
||||
|
||||
// nash: record the redirect so the observer can read back the
|
||||
// affected byte range after the command runs (docs/NASH.md §5.2).
|
||||
nash_record_file_redirect(params, fd_num, kind, &expanded_file_path);
|
||||
// hydrashell: record the redirect so the observer can read back the
|
||||
// affected byte range after the command runs (docs/HYDRASHELL.md §5.2).
|
||||
hydrashell_record_file_redirect(params, fd_num, kind, &expanded_file_path);
|
||||
|
||||
params.open_files.set_fd(fd_num, opened_file);
|
||||
}
|
||||
@@ -1946,8 +1946,8 @@ pub(crate) async fn setup_redirect(
|
||||
|
||||
let f = setup_open_file_with_contents(io_here_doc.as_str())?;
|
||||
|
||||
// nash: heredoc body is known at setup — capture inline (docs/NASH.md §5.2b).
|
||||
nash_record_inline_redirect(params, fd_num, "<<", &io_here_doc);
|
||||
// hydrashell: heredoc body is known at setup — capture inline (docs/HYDRASHELL.md §5.2b).
|
||||
hydrashell_record_inline_redirect(params, fd_num, "<<", &io_here_doc);
|
||||
|
||||
params.open_files.set_fd(fd_num, f);
|
||||
}
|
||||
@@ -1961,8 +1961,8 @@ pub(crate) async fn setup_redirect(
|
||||
|
||||
let f = setup_open_file_with_contents(expanded_word.as_str())?;
|
||||
|
||||
// nash: here-string body is known at setup — capture inline.
|
||||
nash_record_inline_redirect(params, fd_num, "<<<", &expanded_word);
|
||||
// hydrashell: here-string body is known at setup — capture inline.
|
||||
hydrashell_record_inline_redirect(params, fd_num, "<<<", &expanded_word);
|
||||
|
||||
params.open_files.set_fd(fd_num, f);
|
||||
}
|
||||
@@ -1971,64 +1971,64 @@ pub(crate) async fn setup_redirect(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// nash: bounded prefix captured per pipe link before reporting (docs/NASH.md §5.3).
|
||||
const NASH_PIPE_CAPTURE_CAP: usize = 64 * 1024;
|
||||
// hydrashell: bounded prefix captured per pipe link before reporting (docs/HYDRASHELL.md §5.3).
|
||||
const HYDRASHELL_PIPE_CAPTURE_CAP: usize = 64 * 1024;
|
||||
|
||||
// nash: longest stage label carried on a pipe event. A stage can be an entire `while` loop, and
|
||||
// this rides on every link of every pipeline — one of the highest-volume kinds nash emits.
|
||||
const NASH_STAGE_TEXT_CAP: usize = 200;
|
||||
// hydrashell: longest stage label carried on a pipe event. A stage can be an entire `while` loop, and
|
||||
// this rides on every link of every pipeline — one of the highest-volume kinds hydrashell emits.
|
||||
const HYDRASHELL_STAGE_TEXT_CAP: usize = 200;
|
||||
|
||||
// nash: the pipeline stage at `index`, rendered back to shell syntax to label a pipe link
|
||||
// (docs/NASH.md §5.3).
|
||||
// hydrashell: the pipeline stage at `index`, rendered back to shell syntax to label a pipe link
|
||||
// (docs/HYDRASHELL.md §5.3).
|
||||
//
|
||||
// Collapsing and capping happen here rather than in the observer because this is a *label*, and
|
||||
// only brush-core has the AST to render one; redaction still happens observer-side with every
|
||||
// other outbound string. `Display` re-renders the parsed command, so the result is the stage as
|
||||
// parsed rather than byte-identical source — which is what a one-line label wants anyway.
|
||||
fn nash_stage_text(pipeline: &ast::Pipeline, index: usize) -> String {
|
||||
fn hydrashell_stage_text(pipeline: &ast::Pipeline, index: usize) -> String {
|
||||
let Some(command) = pipeline.seq.get(index) else {
|
||||
return String::new();
|
||||
};
|
||||
// A compound stage renders multi-line; a label has one line.
|
||||
let collapsed = command.to_string();
|
||||
let mut text = collapsed.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if text.chars().count() > NASH_STAGE_TEXT_CAP {
|
||||
text = text.chars().take(NASH_STAGE_TEXT_CAP).collect::<String>();
|
||||
if text.chars().count() > HYDRASHELL_STAGE_TEXT_CAP {
|
||||
text = text.chars().take(HYDRASHELL_STAGE_TEXT_CAP).collect::<String>();
|
||||
text.push('…');
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
// nash: bytes moved per userspace copy on the portable tee path. 128 KiB rather than the 32 KiB
|
||||
// hydrashell: bytes moved per userspace copy on the portable tee path. 128 KiB rather than the 32 KiB
|
||||
// this started with: the same stream costs a quarter of the read/write pairs, and a pipe link's
|
||||
// tee cost is almost entirely syscalls and the context switches around them
|
||||
// (docs/NASH_STREAM_PERF_PLAN.md §P2.2).
|
||||
const NASH_TEE_BUF: usize = 128 * 1024;
|
||||
// (docs/HYDRASHELL_STREAM_PERF_PLAN.md §P2.2).
|
||||
const HYDRASHELL_TEE_BUF: usize = 128 * 1024;
|
||||
|
||||
// nash: how large both ends of a tapped link are grown to, where the platform allows it. A tee
|
||||
// hydrashell: how large both ends of a tapped link are grown to, where the platform allows it. A tee
|
||||
// puts *two* pipes where the shell asked for one, so a producer that outruns the default 64 KiB
|
||||
// buffer pays for it twice; growing them cuts the wakeups on both sides.
|
||||
#[cfg(target_os = "linux")]
|
||||
const NASH_TEE_PIPE_SIZE: libc::c_int = 256 * 1024;
|
||||
const HYDRASHELL_TEE_PIPE_SIZE: libc::c_int = 256 * 1024;
|
||||
|
||||
// nash: bytes asked of one `splice` call. The kernel moves at most a pipe-buffer's worth per call
|
||||
// hydrashell: bytes asked of one `splice` call. The kernel moves at most a pipe-buffer's worth per call
|
||||
// regardless, so this only has to be comfortably larger than the pipe.
|
||||
#[cfg(target_os = "linux")]
|
||||
const NASH_TEE_SPLICE_CHUNK: usize = 1024 * 1024;
|
||||
const HYDRASHELL_TEE_SPLICE_CHUNK: usize = 1024 * 1024;
|
||||
|
||||
// nash: how many finished tee threads stay parked waiting for the next pipeline. Thread creation
|
||||
// hydrashell: how many finished tee threads stay parked waiting for the next pipeline. Thread creation
|
||||
// is the bulk of the ~0.13 ms a tapped pipeline costs to *set up*, and shell-heavy builds run
|
||||
// pipelines in the thousands (docs/NASH_STREAM_PERF_PLAN.md §P2.3). A shell runs its pipelines
|
||||
// pipelines in the thousands (docs/HYDRASHELL_STREAM_PERF_PLAN.md §P2.3). A shell runs its pipelines
|
||||
// mostly one at a time, so a small pool covers the common case; a burst of concurrent pipelines
|
||||
// simply spawns beyond it and lets the extra workers retire when they finish.
|
||||
const NASH_TEE_POOL_MAX_IDLE: usize = 4;
|
||||
const HYDRASHELL_TEE_POOL_MAX_IDLE: usize = 4;
|
||||
|
||||
// nash: stack for a tee worker. It copies through a heap buffer and hands the gate an already-built
|
||||
// hydrashell: stack for a tee worker. It copies through a heap buffer and hands the gate an already-built
|
||||
// event, so its own frames are shallow — a quarter of the 2 MiB default is ample and maps less.
|
||||
const NASH_TEE_STACK: usize = 512 * 1024;
|
||||
const HYDRASHELL_TEE_STACK: usize = 512 * 1024;
|
||||
|
||||
// nash: one link waiting to be copied — what a tee worker is handed.
|
||||
struct NashTeeJob {
|
||||
// hydrashell: one link waiting to be copied — what a tee worker is handed.
|
||||
struct HydrashellTeeJob {
|
||||
src: std::io::PipeReader,
|
||||
dst: std::io::PipeWriter,
|
||||
pipeline_id: u64,
|
||||
@@ -2037,62 +2037,62 @@ struct NashTeeJob {
|
||||
to_text: String,
|
||||
}
|
||||
|
||||
// nash: senders of the tee workers currently parked, newest last.
|
||||
static NASH_TEE_POOL: std::sync::Mutex<Vec<std::sync::mpsc::Sender<NashTeeJob>>> =
|
||||
// hydrashell: senders of the tee workers currently parked, newest last.
|
||||
static HYDRASHELL_TEE_POOL: std::sync::Mutex<Vec<std::sync::mpsc::Sender<HydrashellTeeJob>>> =
|
||||
std::sync::Mutex::new(Vec::new());
|
||||
|
||||
// nash: the process the parked workers belong to. A `fork` copies the pool's *memory* but none of
|
||||
// hydrashell: the process the parked workers belong to. A `fork` copies the pool's *memory* but none of
|
||||
// its threads, so a child that inherited it would hand jobs to workers that do not exist — and a
|
||||
// dropped job silently breaks the pipeline it was tapping. Checked (and reset) on every dispatch.
|
||||
static NASH_TEE_POOL_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
static HYDRASHELL_TEE_POOL_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
|
||||
// nash: take a parked worker, if this process has one to spare.
|
||||
// hydrashell: take a parked worker, if this process has one to spare.
|
||||
//
|
||||
// `try_lock`, never `lock`: this runs on the shell's own pipeline-setup path, so waiting on the
|
||||
// pool would put observation in front of a command — and after a `fork` the mutex can be left
|
||||
// locked by a thread that no longer exists. Failing to take a worker just means spawning one.
|
||||
fn nash_tee_take_idle() -> Option<std::sync::mpsc::Sender<NashTeeJob>> {
|
||||
let mut pool = NASH_TEE_POOL.try_lock().ok()?;
|
||||
fn hydrashell_tee_take_idle() -> Option<std::sync::mpsc::Sender<HydrashellTeeJob>> {
|
||||
let mut pool = HYDRASHELL_TEE_POOL.try_lock().ok()?;
|
||||
let pid = std::process::id();
|
||||
if NASH_TEE_POOL_PID.swap(pid, std::sync::atomic::Ordering::Relaxed) != pid {
|
||||
if HYDRASHELL_TEE_POOL_PID.swap(pid, std::sync::atomic::Ordering::Relaxed) != pid {
|
||||
pool.clear(); // inherited across a fork: those workers are not in this process
|
||||
}
|
||||
pool.pop()
|
||||
}
|
||||
|
||||
// nash: park a worker that just finished a link. Returns whether it was taken — a worker the pool
|
||||
// hydrashell: park a worker that just finished a link. Returns whether it was taken — a worker the pool
|
||||
// has no room for (or cannot reach) simply exits.
|
||||
fn nash_tee_park(tx: &std::sync::mpsc::Sender<NashTeeJob>) -> bool {
|
||||
let Ok(mut pool) = NASH_TEE_POOL.try_lock() else {
|
||||
fn hydrashell_tee_park(tx: &std::sync::mpsc::Sender<HydrashellTeeJob>) -> bool {
|
||||
let Ok(mut pool) = HYDRASHELL_TEE_POOL.try_lock() else {
|
||||
return false;
|
||||
};
|
||||
if pool.len() >= NASH_TEE_POOL_MAX_IDLE {
|
||||
if pool.len() >= HYDRASHELL_TEE_POOL_MAX_IDLE {
|
||||
return false;
|
||||
}
|
||||
pool.push(tx.clone());
|
||||
true
|
||||
}
|
||||
|
||||
// nash: a parked worker's loop — copy a link, park again, wait for the next one.
|
||||
fn nash_tee_worker(
|
||||
rx: &std::sync::mpsc::Receiver<NashTeeJob>,
|
||||
parked: &std::sync::mpsc::Sender<NashTeeJob>,
|
||||
// hydrashell: a parked worker's loop — copy a link, park again, wait for the next one.
|
||||
fn hydrashell_tee_worker(
|
||||
rx: &std::sync::mpsc::Receiver<HydrashellTeeJob>,
|
||||
parked: &std::sync::mpsc::Sender<HydrashellTeeJob>,
|
||||
) {
|
||||
while let Ok(job) = rx.recv() {
|
||||
nash_run_pipe_tee(job);
|
||||
if !nash_tee_park(parked) {
|
||||
hydrashell_run_pipe_tee(job);
|
||||
if !hydrashell_tee_park(parked) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nash: hand a copier the link `src` (producer's output) → `dst` (consumer's input): it mirrors a
|
||||
// bounded prefix, then reports the link to the gate (docs/NASH.md §5.3). The synchronous copy
|
||||
// hydrashell: hand a copier the link `src` (producer's output) → `dst` (consumer's input): it mirrors a
|
||||
// bounded prefix, then reports the link to the gate (docs/HYDRASHELL.md §5.3). The synchronous copy
|
||||
// preserves pipe backpressure; EOF on `src` or EPIPE on `dst` (consumer gone, e.g. `yes | head`)
|
||||
// ends it and closes `dst` so the consumer sees EOF.
|
||||
// `to_index` is always `from_index + 1` — a link joins adjacent stages — so it is derived rather
|
||||
// than passed; `from_text`/`to_text` are the two stages already rendered by `nash_stage_text`.
|
||||
fn nash_spawn_pipe_tee(
|
||||
// than passed; `from_text`/`to_text` are the two stages already rendered by `hydrashell_stage_text`.
|
||||
fn hydrashell_spawn_pipe_tee(
|
||||
src: std::io::PipeReader,
|
||||
dst: std::io::PipeWriter,
|
||||
pipeline_id: u64,
|
||||
@@ -2100,7 +2100,7 @@ fn nash_spawn_pipe_tee(
|
||||
from_text: String,
|
||||
to_text: String,
|
||||
) {
|
||||
let mut job = NashTeeJob {
|
||||
let mut job = HydrashellTeeJob {
|
||||
src,
|
||||
dst,
|
||||
pipeline_id,
|
||||
@@ -2110,30 +2110,30 @@ fn nash_spawn_pipe_tee(
|
||||
};
|
||||
// A worker that retired between being parked and being handed this job returns it, so try the
|
||||
// next one down rather than losing the link.
|
||||
while let Some(tx) = nash_tee_take_idle() {
|
||||
while let Some(tx) = hydrashell_tee_take_idle() {
|
||||
match tx.send(job) {
|
||||
Ok(()) => return,
|
||||
Err(std::sync::mpsc::SendError(returned)) => job = returned,
|
||||
}
|
||||
}
|
||||
let (tx, rx) = std::sync::mpsc::channel::<NashTeeJob>();
|
||||
let (tx, rx) = std::sync::mpsc::channel::<HydrashellTeeJob>();
|
||||
let parked = tx.clone();
|
||||
// If the spawn fails there is nobody to copy this link and the job is dropped, closing both
|
||||
// ends — the same outcome an unspawnable tee thread has always had, in a shell that is out of
|
||||
// threads either way.
|
||||
if std::thread::Builder::new()
|
||||
.name("nash-pipe-tee".into())
|
||||
.stack_size(NASH_TEE_STACK)
|
||||
.spawn(move || nash_tee_worker(&rx, &parked))
|
||||
.name("hydrashell-pipe-tee".into())
|
||||
.stack_size(HYDRASHELL_TEE_STACK)
|
||||
.spawn(move || hydrashell_tee_worker(&rx, &parked))
|
||||
.is_ok()
|
||||
{
|
||||
let _ = tx.send(job);
|
||||
}
|
||||
}
|
||||
|
||||
// nash: copy one link through to its consumer, then report it.
|
||||
fn nash_run_pipe_tee(job: NashTeeJob) {
|
||||
let NashTeeJob {
|
||||
// hydrashell: copy one link through to its consumer, then report it.
|
||||
fn hydrashell_run_pipe_tee(job: HydrashellTeeJob) {
|
||||
let HydrashellTeeJob {
|
||||
mut src,
|
||||
mut dst,
|
||||
pipeline_id,
|
||||
@@ -2141,15 +2141,15 @@ fn nash_run_pipe_tee(job: NashTeeJob) {
|
||||
from_text,
|
||||
to_text,
|
||||
} = job;
|
||||
nash_grow_pipe(&src);
|
||||
nash_grow_pipe(&dst);
|
||||
hydrashell_grow_pipe(&src);
|
||||
hydrashell_grow_pipe(&dst);
|
||||
|
||||
let mut captured: Vec<u8> = Vec::new();
|
||||
let mut total: u64 = 0;
|
||||
// Only the prefix is ever copied through userspace; the rest of the stream — which is all of
|
||||
// it, for the transfers that actually cost something — is handed to the kernel.
|
||||
if nash_tee_prefix(&mut src, &mut dst, &mut captured, &mut total) {
|
||||
nash_tee_passthrough(&mut src, &mut dst, &mut total);
|
||||
if hydrashell_tee_prefix(&mut src, &mut dst, &mut captured, &mut total) {
|
||||
hydrashell_tee_passthrough(&mut src, &mut dst, &mut total);
|
||||
}
|
||||
|
||||
// Report BEFORE closing `dst`: dropping it unblocks the consumer, which
|
||||
@@ -2169,17 +2169,17 @@ fn nash_run_pipe_tee(job: NashTeeJob) {
|
||||
drop(dst);
|
||||
}
|
||||
|
||||
// nash: copy until the capture cap is reached, mirroring what goes past. Returns whether the link
|
||||
// hydrashell: copy until the capture cap is reached, mirroring what goes past. Returns whether the link
|
||||
// is still open (false on EOF, or once either end gives up).
|
||||
fn nash_tee_prefix(
|
||||
fn hydrashell_tee_prefix(
|
||||
src: &mut std::io::PipeReader,
|
||||
dst: &mut std::io::PipeWriter,
|
||||
captured: &mut Vec<u8>,
|
||||
total: &mut u64,
|
||||
) -> bool {
|
||||
use std::io::{Read, Write};
|
||||
let mut buf = vec![0u8; NASH_TEE_BUF];
|
||||
while captured.len() < NASH_PIPE_CAPTURE_CAP {
|
||||
let mut buf = vec![0u8; HYDRASHELL_TEE_BUF];
|
||||
while captured.len() < HYDRASHELL_PIPE_CAPTURE_CAP {
|
||||
let n = match src.read(&mut buf) {
|
||||
Ok(0) => return false, // producer closed → done
|
||||
Ok(n) => n,
|
||||
@@ -2187,7 +2187,7 @@ fn nash_tee_prefix(
|
||||
Err(_) => return false,
|
||||
};
|
||||
*total += n as u64;
|
||||
let room = NASH_PIPE_CAPTURE_CAP - captured.len();
|
||||
let room = HYDRASHELL_PIPE_CAPTURE_CAP - captured.len();
|
||||
captured.extend_from_slice(&buf[..room.min(n)]);
|
||||
// Pass through; if the consumer went away, stop (its EOF is enough).
|
||||
if dst.write_all(&buf[..n]).is_err() {
|
||||
@@ -2197,11 +2197,11 @@ fn nash_tee_prefix(
|
||||
true
|
||||
}
|
||||
|
||||
// nash: the portable passthrough — the same read/write loop as the prefix phase, minus the
|
||||
// hydrashell: the portable passthrough — the same read/write loop as the prefix phase, minus the
|
||||
// capture. Used on platforms without `splice`, and as the fallback when the kernel refuses it.
|
||||
fn nash_tee_copy(src: &mut std::io::PipeReader, dst: &mut std::io::PipeWriter, total: &mut u64) {
|
||||
fn hydrashell_tee_copy(src: &mut std::io::PipeReader, dst: &mut std::io::PipeWriter, total: &mut u64) {
|
||||
use std::io::{Read, Write};
|
||||
let mut buf = vec![0u8; NASH_TEE_BUF];
|
||||
let mut buf = vec![0u8; HYDRASHELL_TEE_BUF];
|
||||
loop {
|
||||
let n = match src.read(&mut buf) {
|
||||
Ok(0) => return,
|
||||
@@ -2216,12 +2216,12 @@ fn nash_tee_copy(src: &mut std::io::PipeReader, dst: &mut std::io::PipeWriter, t
|
||||
}
|
||||
}
|
||||
|
||||
// nash: move the rest of the link pipe-to-pipe inside the kernel (docs/NASH_STREAM_PERF_PLAN.md
|
||||
// hydrashell: move the rest of the link pipe-to-pipe inside the kernel (docs/HYDRASHELL_STREAM_PERF_PLAN.md
|
||||
// §P2.1). Past the captured prefix the tee has nothing to look at, so there is no reason for the
|
||||
// bytes to enter this process at all — and the byte count comes back from the same call that
|
||||
// moves them.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn nash_tee_passthrough(
|
||||
fn hydrashell_tee_passthrough(
|
||||
src: &mut std::io::PipeReader,
|
||||
dst: &mut std::io::PipeWriter,
|
||||
total: &mut u64,
|
||||
@@ -2237,7 +2237,7 @@ fn nash_tee_passthrough(
|
||||
std::ptr::null_mut(),
|
||||
out_fd,
|
||||
std::ptr::null_mut(),
|
||||
NASH_TEE_SPLICE_CHUNK,
|
||||
HYDRASHELL_TEE_SPLICE_CHUNK,
|
||||
libc::SPLICE_F_MOVE,
|
||||
)
|
||||
};
|
||||
@@ -2254,7 +2254,7 @@ fn nash_tee_passthrough(
|
||||
// A kernel (or a seccomp policy) that will not splice these descriptors: finish the
|
||||
// link the portable way rather than dropping it.
|
||||
Some(libc::EINVAL | libc::ENOSYS | libc::EPERM) => {
|
||||
nash_tee_copy(src, dst, total);
|
||||
hydrashell_tee_copy(src, dst, total);
|
||||
return;
|
||||
}
|
||||
// Consumer gone (EPIPE) or a link that broke: its EOF is enough.
|
||||
@@ -2264,29 +2264,29 @@ fn nash_tee_passthrough(
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn nash_tee_passthrough(
|
||||
fn hydrashell_tee_passthrough(
|
||||
src: &mut std::io::PipeReader,
|
||||
dst: &mut std::io::PipeWriter,
|
||||
total: &mut u64,
|
||||
) {
|
||||
nash_tee_copy(src, dst, total);
|
||||
hydrashell_tee_copy(src, dst, total);
|
||||
}
|
||||
|
||||
// nash: grow a tapped pipe's buffer, best-effort. The kernel caps unprivileged growth at
|
||||
// hydrashell: grow a tapped pipe's buffer, best-effort. The kernel caps unprivileged growth at
|
||||
// `/proc/sys/fs/pipe-max-size` and simply refuses anything larger, which costs one failed syscall
|
||||
// per link and leaves the default in place.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn nash_grow_pipe<F: std::os::fd::AsRawFd>(pipe: &F) {
|
||||
fn hydrashell_grow_pipe<F: std::os::fd::AsRawFd>(pipe: &F) {
|
||||
// SAFETY: `fcntl` with `F_SETPIPE_SZ` takes an int and only reads the descriptor's pipe size.
|
||||
let _ = unsafe { libc::fcntl(pipe.as_raw_fd(), libc::F_SETPIPE_SZ, NASH_TEE_PIPE_SIZE) };
|
||||
let _ = unsafe { libc::fcntl(pipe.as_raw_fd(), libc::F_SETPIPE_SZ, HYDRASHELL_TEE_PIPE_SIZE) };
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn nash_grow_pipe<F>(_pipe: &F) {}
|
||||
fn hydrashell_grow_pipe<F>(_pipe: &F) {}
|
||||
|
||||
// nash: record a regular-file redirect for post-run read-back (docs/NASH.md §5.2a).
|
||||
// hydrashell: record a regular-file redirect for post-run read-back (docs/HYDRASHELL.md §5.2a).
|
||||
// The child still gets the real fd; this only notes what to read back afterward.
|
||||
fn nash_record_file_redirect(
|
||||
fn hydrashell_record_file_redirect(
|
||||
params: &mut ExecutionParameters,
|
||||
fd: ShellFd,
|
||||
kind: &ast::IoFileRedirectKind,
|
||||
@@ -2303,7 +2303,7 @@ fn nash_record_file_redirect(
|
||||
// Size at setup: append ranges start here; only regular files are read back.
|
||||
let size_before = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
|
||||
let op = if fd == 2 && op == ">" { "2>".to_string() } else { op.to_string() };
|
||||
params.nash_redirects.push(crate::gate::RedirectRecord {
|
||||
params.hydrashell_redirects.push(crate::gate::RedirectRecord {
|
||||
op,
|
||||
fd: u32::try_from(fd).unwrap_or(0),
|
||||
path: Some(path.to_path_buf()),
|
||||
@@ -2313,14 +2313,14 @@ fn nash_record_file_redirect(
|
||||
});
|
||||
}
|
||||
|
||||
// nash: record an inline (heredoc / here-string) redirect — body known at setup.
|
||||
fn nash_record_inline_redirect(
|
||||
// hydrashell: record an inline (heredoc / here-string) redirect — body known at setup.
|
||||
fn hydrashell_record_inline_redirect(
|
||||
params: &mut ExecutionParameters,
|
||||
fd: ShellFd,
|
||||
op: &str,
|
||||
body: &str,
|
||||
) {
|
||||
params.nash_redirects.push(crate::gate::RedirectRecord {
|
||||
params.hydrashell_redirects.push(crate::gate::RedirectRecord {
|
||||
op: op.to_string(),
|
||||
fd: u32::try_from(fd).unwrap_or(0),
|
||||
path: None,
|
||||
|
||||
@@ -14,7 +14,7 @@ pub mod expansion;
|
||||
mod extendedtests;
|
||||
pub mod extensions;
|
||||
pub mod functions;
|
||||
// nash: observation/enforcement gate (docs/NASH.md §4.2).
|
||||
// hydrashell: observation/enforcement gate (docs/HYDRASHELL.md §4.2).
|
||||
pub mod gate;
|
||||
pub mod history;
|
||||
pub mod int_utils;
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct ChildProcess {
|
||||
pid: Option<sys::process::ProcessId>,
|
||||
/// If available, the process group ID of the child.
|
||||
pgid: Option<sys::process::ProcessId>,
|
||||
// nash: gate token issued by `Gate::on_exec` for this command, reported
|
||||
// hydrashell: 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>,
|
||||
}
|
||||
@@ -59,7 +59,7 @@ impl ChildProcess {
|
||||
tokio::select! {
|
||||
output = &mut self.exec_future => {
|
||||
let output = output?;
|
||||
// nash: report process completion to the gate exactly once.
|
||||
// hydrashell: report process completion to the gate exactly once.
|
||||
if let Some(token) = self.gate_token.take() {
|
||||
crate::gate::gate().on_exit(
|
||||
token,
|
||||
@@ -90,7 +90,7 @@ impl ChildProcess {
|
||||
let result: Option<Result<std::process::Output, error::Error>> = checkable_future
|
||||
.now_or_never()
|
||||
.map(|result| result.map_err(Into::into));
|
||||
// nash: completion can also be observed via poll (e.g. job checks).
|
||||
// hydrashell: 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(
|
||||
@@ -105,7 +105,7 @@ impl ChildProcess {
|
||||
}
|
||||
}
|
||||
|
||||
// nash: numeric exit code for gate reporting (128+signal for signal deaths).
|
||||
// hydrashell: 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;
|
||||
|
||||
@@ -87,7 +87,7 @@ fn format_prompt_piece(
|
||||
if users::is_root() {
|
||||
"#".to_owned()
|
||||
} else {
|
||||
// nash: the interactive shell symbol is the atom sign (U+269B) rather than `$`.
|
||||
// hydrashell: the interactive shell symbol is the atom sign (U+269B) rather than `$`.
|
||||
"\u{269B}".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ brush is distributed under the terms of the MIT license. If you encounter any is
|
||||
|
||||
For more information, visit https://brush.sh.";
|
||||
|
||||
// nash: rebranded usage/version strings for the Nucleic agent shell fork.
|
||||
// hydrashell: rebranded usage/version strings for the Nucleic agent shell fork.
|
||||
const USAGE: &str = color_print::cstr!(
|
||||
"<bold>nash</bold> <italics>[OPTIONS]</italics>... <italics>[SCRIPT_PATH [SCRIPT_ARGS]...]</italics>"
|
||||
"<bold>hydrashell</bold> <italics>[OPTIONS]</italics>... <italics>[SCRIPT_PATH [SCRIPT_ARGS]...]</italics>"
|
||||
);
|
||||
|
||||
const VERSION: &str = const_format::concatcp!(
|
||||
productinfo::PRODUCT_VERSION,
|
||||
" (Nucleic agent shell, brush fork; ",
|
||||
" (Hydrangea agent shell, brush fork; ",
|
||||
productinfo::PRODUCT_GIT_VERSION,
|
||||
")"
|
||||
);
|
||||
|
||||
@@ -268,11 +268,11 @@ pub fn run() {
|
||||
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).
|
||||
// hydrashell: a hook invoked right before the process exits, so an embedding shell
|
||||
// (hash) can flush its observation event buffer (docs/HYDRASHELL.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.
|
||||
/// hydrashell: 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);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Information about this shell project.
|
||||
|
||||
// nash: rebranded from "brush" for the Nucleic agent shell fork (docs/NASH.md §2).
|
||||
// hydrashell: rebranded from "brush" for the Nucleic agent shell fork (docs/HYDRASHELL.md §2).
|
||||
/// The formal name of this product.
|
||||
pub const PRODUCT_NAME: &str = "nash";
|
||||
pub const PRODUCT_NAME: &str = "hydrashell";
|
||||
|
||||
const PRODUCT_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
|
||||
const PRODUCT_REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
@@ -27,8 +27,8 @@ pub const PRODUCT_GIT_VERSION: &str = git_version::git_version!(
|
||||
);
|
||||
|
||||
pub(crate) fn get_product_display_str() -> String {
|
||||
// nash: identify as the Nucleic agent shell fork of brush.
|
||||
// hydrashell: identify as the Nucleic agent shell fork of brush.
|
||||
std::format!(
|
||||
"{PRODUCT_NAME} (Nucleic agent shell, brush fork) {PRODUCT_VERSION} ({PRODUCT_GIT_VERSION}) - {PRODUCT_DISPLAY_URI}"
|
||||
"{PRODUCT_NAME} (hash — Hydrangea agent shell, brush fork) {PRODUCT_VERSION} ({PRODUCT_GIT_VERSION}) - {PRODUCT_DISPLAY_URI}"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ cases:
|
||||
echo "After: [$IFS]"
|
||||
|
||||
- name: "IFS does not affect for loop literal words"
|
||||
# nash: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
|
||||
# hydrashell: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
|
||||
stdin: |
|
||||
IFS=':'
|
||||
for word in a:b:c; do
|
||||
@@ -249,7 +249,7 @@ cases:
|
||||
echo "Quoted: $var"
|
||||
|
||||
- name: "IFS with partially quoted expansion"
|
||||
# nash: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
|
||||
# hydrashell: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
|
||||
stdin: |
|
||||
IFS=':'
|
||||
var="a:b:c"
|
||||
@@ -309,7 +309,7 @@ cases:
|
||||
echo "[$1]"
|
||||
|
||||
- name: "IFS with glob pattern literal"
|
||||
# nash: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
|
||||
# hydrashell: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
|
||||
stdin: |
|
||||
IFS=':'
|
||||
# Glob patterns as literals shouldn't split
|
||||
|
||||
Reference in New Issue
Block a user