Merge nucleic/nimble-umber-toad-20h7 into dev

This commit is contained in:
2026-08-11 20:11:26 -07:00
parent 959a80f5ec
commit 8e934d8996
16 changed files with 196 additions and 196 deletions
+7 -7
View File
@@ -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()?;
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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;
+4 -4
View File
@@ -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;
+1 -1
View File
@@ -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()
}
}