Skip to content

v2.7.0

Choose a tag to compare

@github-actions github-actions released this 24 Jul 17:11
· 192 commits to main since this release

Added

  • ProcessKit.Testing.FakeProcess now honours Command.KeepStdinOpen(): its built handle returns one writable stdin sink through TakeStdin(), and StdinBytes exposes the bytes written for assertions.
  • ProcessKit.Testing now replays Command.MergeStderr() with one merged stdout stream, matching the real runner's empty stderr result and stdout-only streaming events.

Changed

  • CliClient.WithDefaults now rejects one-shot stdin sources (FromStream, FromLines, and
  • RecordReplayRunner now writes cassette format v6, which explicitly records a signal whose number
  • RecordReplayRunner now writes cassette format v5: stdin digests are source-domain-prefixed, so a literal in-memory input can no longer alias inherited stdin or a path-only file source. Existing v1–v4 cassettes still replay with their legacy key scheme.
  • ProcessKit.Testing's public API (FakeProcess, Reply, ScriptedRunner) now validates null

Fixed

  • On Windows, graceful GUI shutdown now rechecks Job membership immediately before posting WM_CLOSE, so a recycled process ID cannot close an unrelated application's window.
  • POSIX graceful shutdown and kill-on-dispose retain the tracked process-group identity through their full poll, preventing a concurrent teardown from weakening reuse protection before the final signal.
  • LineBuffer cumulative byte counters and OutputTooLarge diagnostics now saturate at Int32.MaxValue instead of wrapping to negative on overflow.
  • ProcessGroup.UpdateLimits now honours its documented contract when a live limit re-apply fails partway. The caps land through several sequential native writes (on Windows the Job's memory/active-process block then its CPU rate cap; on Linux cgroup v2 memory.max then pids.max then cpu.max), so a later write could fail after an earlier one already changed the live container — previously leaving the container on a silent mix of the old and new caps while Options.Limits still reported the old set (e.g. memory tightened to 256 MB on the kernel but Options.Limits.MemoryMax still reading 1 GB, so a consumer dosing work by Options misdiagnosed the resulting OOM-kills). A failed UpdateLimits now best-effort restores the previous set to the live container before returning the error, so it leaves both the container and the Options.Limits snapshot on the previous caps — they can no longer diverge silently. In the rare case that restore itself also fails, the returned ProcessError.ResourceLimit says so explicitly (its message notes the limits may be partially applied) rather than pretending nothing changed. A successful UpdateLimits is unchanged.
  • ProcessKitOptions.DefaultWorkingDirectory now rejects empty or whitespace-only values during DI setup/binding instead of stamping an invalid directory onto a command and failing later at spawn.
  • RecordReplayRunner now preserves Outcome.Signalled None when recording and replaying a cassette,
  • CliClient.WithDefaults now rejects a configure callback that returns null at the API boundary,
  • RecordReplayRunner now keeps inherited stdin, path-only file stdin, and in-memory stdin in distinct
  • Pipeline.StartAsync now preserves a pipeline deadline that fires while its stages are being spawned: the call returns ProcessError.Timeout with the configured duration instead of silently downgrading the failure to ProcessError.Cancelled.
  • Exec.outputAll and Exec.outputAllBytes now reject a null runner, command sequence, or command
  • On Windows, ProcessGroup.Signal(Signal.Int) / Signal(Signal.Term) can no longer deliver a console CTRL+BREAK to an unrelated process in a narrow race: when a run in a shared group was torn down concurrently with the signal, that run's console process-group id (its pid) could be released and reused by the OS midway through the signal's delivery loop, so the CTRL+BREAK landed on whatever process had recycled the pid on the same console. The per-run teardown now runs under the same group lock the signal-delivery loop holds, so the two are serialized and a targeted console-group id can never go stale mid-delivery. Delivery to live members, and teardown timing, are otherwise unchanged.
  • Command.CancelOn now interrupts a pending retry backoff immediately, returning ProcessError.Cancelled instead of waiting for the configured delay to elapse.
  • On Windows, spawning a process no longer risks corrupting the managed command-line string. CreateProcessW may modify its lpCommandLine argument in place while probing executable candidates; the binding now hands the OS a private, writable unmanaged copy of the command line instead of a marshalled managed string. Previously a single-token, argument-less command (e.g. Command.Create("ping")) forwarded its program name — often an interned literal shared process-wide — directly to the OS as a writable buffer, so a native write could corrupt that literal for every unrelated reader of it (a memory-corruption-class bug). Spawn behaviour (quoting, .cmd/.bat BatBadBut wrapping, PATHEXT resolution) is unchanged.
  • Calling StopAsync concurrently with an in-flight capturing verb (OutputStringAsync/OutputBytesAsync/WaitAsync/ProfileAsync) on the same handle no longer fails that verb with a spurious ProcessError.Io when the child still has a large buffered output tail. StopAsync reaps as soon as the shared exit wait resolves and then disposes the pipes, which could race a buffered pump still draining the tail; that routine teardown dispose is now told apart from a genuine mid-run read failure (the streaming verbs already drew this distinction), so the verb returns its honestly-captured output instead of a false error — and, through the supervision layer, a liveness-triggered graceful stop no longer faults SupervisionSession.Completion. A genuine mid-run I/O read fault still surfaces as ProcessError.Io.
  • A HostedProcessService.Dispose() that races the very start of its background supervision no longer publishes a spurious LastOutcome = Error (nor logs "supervision failed"). Dispose() disposes its lifetime CancellationTokenSource without awaiting the supervision task; the background start now reads a snapshot of that token taken before disposal instead of the live getter, so a routine teardown that overlaps startup is reported as a clean cancellation, exactly like a non-racing Dispose().
  • The stdout streaming session's stderr capture (StdoutLinesAsync() + FinishAsync().Stderr) now honours OutputBufferPolicy.MaxBytes as an in-flight cap the same way the buffered verbs do, so a newline-free stderr flood can no longer grow the pump's assembly buffer past the configured cap.
  • ProcessKit.Testing cassette replay for OutputBytesAsync now decodes its text projections (Combined, OutputContainsAny, and error text in Exit/Signalled/Timeout) with the command's configured StdoutEncoding instead of hardcoded UTF-8, restoring record/replay parity for non-UTF-8 byte captures and matching RunningProcess.OutputBytesAsync.
  • Cancelling a fully spawned pipeline now stops a pending stage-0 asynchronous stdin source, disposing its enumerator instead of leaving it parked after the run completes.
  • RunningProcess.ResizeAsync on a torn-down PTY run — one whose handle was disposed, or whose child a terminal verb (WaitAsync/OutputStringAsync/…) already reaped — now returns a typed, non-transient ProcessError.Unsupported instead of resizing through the closed pty master fd / pseudoconsole handle. On POSIX the fd number is reusable the instant the run is torn down, so a late resize could ioctl(TIOCSWINSZ) a concurrent run's pty/socketpair and deliver SIGWINCH to a recycled pid — a wrong-target mutation of an unrelated process; on Windows the pseudoconsole handle could likewise be reused. The resize now passes through the same lifecycle gate the kill/signal verbs use, so it fires only while the run is live and refuses cleanly afterward. Resizing a live PTY run is unchanged.
  • A readiness probe (WaitForHttpAsync/WaitForPortAsync/WaitForSocketAsync/WaitForAsync) racing its shared, memoized exit wait no longer risks an unobserved task exception at finalization when that exit wait later faults and the handle is disposed without any verb ever awaiting it (probe → dispose, no terminal verb). Verbs that do await the same wait still see and re-throw the original fault unchanged.