Skip to content

CliWrap trialled and rejected — close the gaps in ProcessTasks instead #583

Description

@ChrisonSimtian

Decision

CliWrap was trialled in Fallout.Migrate and rejected. Process execution stays on the in-house ProcessTasks (src/Fallout.Tooling/ProcessTasks.cs). No new package dependency.

Trialled and reverted in #576, which needed to shell out to dotnet tool uninstall / dotnet tool install from SwitchGlobalToolStep.

What the comparison showed

Both versions were built and run side by side against dotnet tool list --global, a deliberately failing dotnet tool uninstall, and a 1 ms timeout. Identical exit codes, identical stdout, identical stderr, and both return null on timeout. The difference is only in what the calling code costs to write.

Lines (non-blank)
CliWrap 18
Raw System.Diagnostics.Process, written correctly 47
ProcessTasks (what we shipped) 31

With CliWrap:

using var cancellation = new CancellationTokenSource(commandTimeout);

return await Cli.Wrap("dotnet")
    .WithArguments(arguments)
    .WithValidation(CommandResultValidation.None)
    .ExecuteBufferedAsync(cancellation.Token);

With ProcessTasks:

var argumentLine = arguments.Select(x => x.DoubleQuoteIfNeeded()).JoinSpace();

using var process = ProcessTasks.StartProcess(
    "dotnet", argumentLine, timeout: (int)commandTimeout.TotalMilliseconds,
    logOutput: false, logInvocation: false);

if (process == null || !process.WaitForExit())
    return null;

return new CommandResult(
    process.ExitCode,
    Join(process.Output, OutputType.Std),
    Join(process.Output, OutputType.Err));

What CliWrap actually solves

Worth recording, because these are the traps a hand-rolled runner falls into:

  1. The pipe-buffer deadlock. The obvious raw implementation is WaitForExit() then StandardOutput.ReadToEnd(). That hangs once the child fills the ~64KB pipe buffer while the parent is blocked in WaitForExit. Avoiding it needs event-based reads plus counting two EOF sentinels. ProcessTasks already handles this, which is the main reason it was a viable replacement.
  2. Cancellation that actually kills. WaitForExitAsync(token) returns on timeout but leaves the child running. An explicit Kill(entireProcessTree: true) is required or a timeout orphans the process.
  3. A single result object carrying exit code plus both streams.

What CliWrap does not solve any more

Its reputation is dated on two points:

  • Argument escapingProcessStartInfo.ArgumentList has handled quoting since .NET Core 2.1.
  • AsyncWaitForExitAsync has been in the BCL since .NET 5.

What using ProcessTasks instead costs

Honest list, since these are the gaps a future improvement would close:

  • No CancellationToken. Only an int millisecond timeout.
  • Synchronous only. No WaitForExitAsync, so a step that is otherwise async has to wrap it.
  • Arguments are one pre-quoted string. No ArgumentList equivalent, so every caller does its own DoubleQuoteIfNeeded(). This is the one place the raw BCL is now better than our own layer.
  • Output is one interleaved collection filtered by OutputType, rather than separate stdout and stderr.
  • Logs through Serilog by default. A tool that renders through Spectre must pass logOutput: false, logInvocation: false.
  • Asserts instead of returning null when the executable is not on PATH, so callers need a try/catch.
  • Pulls Serilog and NuGet.Packaging into anything referencing Fallout.Tooling. For Fallout.Migrate this swapped one direct dependency for two transitive ones.

Acceptance criteria

  • ProcessTasks (or its successor) accepts a CancellationToken alongside the millisecond timeout.
  • An async overload exists, so callers do not have to block a thread.
  • An argument-list overload exists that quotes each argument, instead of every caller calling DoubleQuoteIfNeeded().
  • Not finding the executable on PATH is reportable without a try/catch around the call.
  • docs/dependencies.md keeps pointing here as the reason there is no third-party process runner.

Notes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions