From 87afe03de7bb224ebe47bdf9590966864b382af3 Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Sun, 30 Aug 2026 14:05:10 +0700 Subject: [PATCH] fix(fspy): bundle osh with the RunProcess getpgid ESRCH fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stock oils 0.37.0 calls getpgid(child) after fork even with job control disabled, and on macOS getpgid of an already-exited child fails with ESRCH, so a fast external command intermittently killed the substituted shell with "oils I/O error (main): No such process" and exit 2 — a few percent of runs under CPU contention. Bundle a build of 0.37.0 plus the upstream fix (oils-for-unix/oils#2689, gate the getpgid on job control being enabled) until an oils release carries it. crates/fspy/tests/oils_esrch.rs pins the fix: it fails against a stock binary and passes against the patched one. Fixes #702. --- CHANGELOG.md | 1 + crates/fspy/build.rs | 14 +++++++--- crates/fspy/tests/oils_esrch.rs | 45 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 crates/fspy/tests/oils_esrch.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d93dcf2..bd0363e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** The bundled shell that runs cached task commands on macOS no longer intermittently dies with exit 2 (`oils I/O error (main): No such process`) when a fast command finishes before the shell gets scheduled — the bundled osh now carries the upstream fix for its post-fork `getpgid` race ([#702](https://github.com/voidzero-dev/vite-task/issues/702), [oils#2689](https://github.com/oils-for-unix/oils/pull/2689)). - **Fixed** `vp run` no longer hangs or fails when a task leaves a process running behind it, such as a dev server or a background helper, or when one of a task's processes is killed. The run finishes as soon as the task itself does, and the files the task used are still recorded ([#544](https://github.com/voidzero-dev/vite-task/issues/544), [#675](https://github.com/voidzero-dev/vite-task/pull/675)). - **Fixed** A task that reads or writes an unusually large number of files now runs to the end instead of being killed partway through. Vite+ reports the run as not cached, because it could not record every file the task used ([#533](https://github.com/voidzero-dev/vite-task/issues/533), [#675](https://github.com/voidzero-dev/vite-task/pull/675)). - **Fixed** Vite+ diagnostics now display individual paths and working directories without Rust debug formatting such as quoted paths or escaped Windows backslashes ([#534](https://github.com/voidzero-dev/vite-task/pull/534)). diff --git a/crates/fspy/build.rs b/crates/fspy/build.rs index 031251828..74197a120 100644 --- a/crates/fspy/build.rs +++ b/crates/fspy/build.rs @@ -72,13 +72,16 @@ const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ ( "aarch64", &[ + // Carries the Process::RunProcess getpgid ESRCH fix on top of stock + // 0.37.0 until an upstream release includes it + // (https://github.com/oils-for-unix/oils/pull/2689). Stock builds: // https://github.com/wan9chi/oils-for-unix-build/releases/tag/oils-for-unix-0.37.0 BinaryDownload { name: "oils_for_unix", env_var: "FSPY_MACOS_ARTIFACT_OILS_FOR_UNIX", - url: "https://github.com/wan9chi/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-arm64.tar.gz", + url: "https://github.com/lifeiscontent/oils-for-unix-build/releases/download/oils-for-unix-0.37.0-fspy.1/oils-for-unix-0.37.0-fspy.1-darwin-arm64.tar.gz", path_in_targz: "oils-for-unix", - expected_sha256: "ce4bb80b15f0a0371af08b19b65bfa5ea17d30429ebb911f487de3d2bcc7a07d", + expected_sha256: "b61c45ee8c240d58fc75d6d2ae20f6a89790036362c30aa5f2c21c8f328643f7", }, // https://github.com/uutils/coreutils/releases/tag/0.4.0 BinaryDownload { @@ -93,13 +96,16 @@ const MACOS_BINARY_DOWNLOADS: &[(&str, &[BinaryDownload])] = &[ ( "x86_64", &[ + // Carries the Process::RunProcess getpgid ESRCH fix on top of stock + // 0.37.0 until an upstream release includes it + // (https://github.com/oils-for-unix/oils/pull/2689). Stock builds: // https://github.com/wan9chi/oils-for-unix-build/releases/tag/oils-for-unix-0.37.0 BinaryDownload { name: "oils_for_unix", env_var: "FSPY_MACOS_ARTIFACT_OILS_FOR_UNIX", - url: "https://github.com/wan9chi/oils-for-unix-build/releases/download/oils-for-unix-0.37.0/oils-for-unix-0.37.0-darwin-x86_64.tar.gz", + url: "https://github.com/lifeiscontent/oils-for-unix-build/releases/download/oils-for-unix-0.37.0-fspy.1/oils-for-unix-0.37.0-fspy.1-darwin-x86_64.tar.gz", path_in_targz: "oils-for-unix", - expected_sha256: "cf1a95993127770e2a5fff277cd256a2bb28cf97d7f83ae42fdccc172cdb540d", + expected_sha256: "bf570d54a78dbe0a55712620f746c2b90bbc2aedc633c80d41ca9bbd934424d3", }, // https://github.com/uutils/coreutils/releases/tag/0.4.0 BinaryDownload { diff --git a/crates/fspy/tests/oils_esrch.rs b/crates/fspy/tests/oils_esrch.rs new file mode 100644 index 000000000..6dcc6f20e --- /dev/null +++ b/crates/fspy/tests/oils_esrch.rs @@ -0,0 +1,45 @@ +//! The bundled osh substitute must not lose the getpgid race against a fast +//! child: stock oils 0.37.0 calls getpgid(child) after fork even when job +//! control is disabled, and on macOS getpgid of an already-exited child fails +//! with ESRCH, killing the shell with "oils I/O error (main)" and exit 2 +//! (oils-for-unix/oils#2689). Under CPU contention a few percent of runs died. +//! +//! The race is gone (not just rarer) with a fixed osh, so this fails +//! deterministically if the bundled artifact regresses to a stock build. +#![cfg(target_os = "macos")] + +use std::{fs, path::Path}; + +use test_log::test; + +#[test(tokio::test(flavor = "multi_thread", worker_threads = 8))] +async fn fast_external_commands_under_contention() -> anyhow::Result<()> { + let input = Path::new(env!("CARGO_TARGET_TMPDIR")).join("fspy-oils-esrch-input.txt"); + fs::write(&input, "hello\n")?; + + let mut failures = Vec::new(); + for _round in 0..25 { + let mut handles = Vec::new(); + for _ in 0..8 { + handles.push(tokio::spawn({ + let input = input.clone(); + async move { + let mut cmd = fspy::Command::new("/bin/sh"); + cmd.arg("-c").arg(format!("cat {}", input.display())); + cmd.env("PATH", "/usr/bin:/bin"); + let child = cmd.spawn(tokio_util::sync::CancellationToken::new()).await?; + let termination = child.wait_handle.await?; + anyhow::Ok(termination.status.code()) + } + })); + } + for h in handles { + let code = h.await??; + if code != Some(0) { + failures.push(code); + } + } + } + assert!(failures.is_empty(), "osh exited non-zero {} times: {failures:?}", failures.len()); + Ok(()) +}