×

.NET 11 中 Process API 升级

独孤求败 独孤求败 发表于2026-05-18 22:05:47 浏览18 评论0

抢沙发发表评论

.NET 11 中 Process API 升级

Intro

.NET 11 Preview 4 中引入了一系列新的 API ,在调用命令行程序时变得更加方便了。

在 .NET 世界里,System.Diagnostics.Process 一直是个“能用,但不优雅”的 API。

它功能很强,但同时也:

  • • 容易死锁
  • • 配置繁琐
  • • 生命周期管理麻烦
  • • 输出捕获容易踩坑
  • NativeAOT 不友好

现在,.NET 11 终于对 Process API 动刀了,而且这次不是小修小补,而是一次“多年未见”的大升级。

New API

  namespace Microsoft.Win32.SafeHandles
  {
      public sealed class SafeProcessHandle : Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid
      {
+         public void Kill();

+         public bool Signal(System.Runtime.InteropServices.PosixSignal signal);

+         public static Microsoft.Win32.SafeHandles.SafeProcessHandle Start(System.Diagnostics.ProcessStartInfo startInfo);

+         public bool TryWaitForExit(System.TimeSpan timeout, out System.Diagnostics.ProcessExitStatus? exitStatus);

+         public System.Diagnostics.ProcessExitStatus WaitForExit();

+         public System.Threading.Tasks.Task<System.Diagnostics.ProcessExitStatus> WaitForExitAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

+         public System.Threading.Tasks.Task<System.Diagnostics.ProcessExitStatus> WaitForExitOrKillOnCancellationAsync(System.Threading.CancellationToken cancellationToken);

+         public System.Diagnostics.ProcessExitStatus WaitForExitOrKillOnTimeout(System.TimeSpan timeout);

+         public int ProcessId { get; }

      }
  }
  namespace System.Diagnostics
  {
      public class Process : System.ComponentModel.Component, System.IDisposable
      {
+         public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(System.TimeSpan? timeout = null);

+         public System.Threading.Tasks.Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

+         public System.Collections.Generic.IAsyncEnumerable<System.Diagnostics.ProcessOutputLine> ReadAllLinesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

+         public (string StandardOutput, string StandardError) ReadAllText(System.TimeSpan? timeout = null);

+         public System.Threading.Tasks.Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

+         public static System.Diagnostics.ProcessExitStatus Run(System.Diagnostics.ProcessStartInfo startInfo, System.TimeSpan? timeout = null);

+         public static System.Diagnostics.ProcessExitStatus Run(string fileName, System.Collections.Generic.IList<string>? arguments = null, System.TimeSpan? timeout = null);

+         public static System.Diagnostics.ProcessTextOutput RunAndCaptureText(System.Diagnostics.ProcessStartInfo startInfo, System.TimeSpan? timeout = null);

+         public static System.Diagnostics.ProcessTextOutput RunAndCaptureText(string fileName, System.Collections.Generic.IList<string>? arguments = null, System.TimeSpan? timeout = null);

+         public static System.Threading.Tasks.Task<System.Diagnostics.ProcessTextOutput> RunAndCaptureTextAsync(System.Diagnostics.ProcessStartInfo startInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

+         public static System.Threading.Tasks.Task<System.Diagnostics.ProcessTextOutput> RunAndCaptureTextAsync(string fileName, System.Collections.Generic.IList<string>? arguments = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

+         public static System.Threading.Tasks.Task<System.Diagnostics.ProcessExitStatus> RunAsync(System.Diagnostics.ProcessStartInfo startInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

+         public static System.Threading.Tasks.Task<System.Diagnostics.ProcessExitStatus> RunAsync(string fileName, System.Collections.Generic.IList<string>? arguments = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

+         public static int StartAndForget(System.Diagnostics.ProcessStartInfo startInfo);

+         public static int StartAndForget(string fileName, System.Collections.Generic.IList<string>? arguments = null);

      }
      public sealed class ProcessStartInfo
      {
+         public System.Collections.Generic.IList<System.Runtime.InteropServices.SafeHandle>? InheritedHandles { get; set; }

+         public bool KillOnParentExit { get; set; }

+         public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardErrorHandle { get; set; }

+         public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardInputHandle { get; set; }

+         public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardOutputHandle { get; set; }

+         public bool StartDetached { get; set; }

      }
+     public readonly struct ProcessOutputLine

+     {

+         public ProcessOutputLine(string content, bool standardError);

+         public string Content { get; }

+         public bool StandardError { get; }

+     }

+     public sealed class ProcessTextOutput

+     {

+         public ProcessTextOutput(System.Diagnostics.ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId);

+         public System.Diagnostics.ProcessExitStatus ExitStatus { get; }

+         public int ProcessId { get; }

+         public string StandardError { get; }

+         public string StandardOutput { get; }

+     }

  }

Sample

Run

Process 类型新增了 Run/RunAsync  静态方法,不需要再先实例化 Process 就可以直接调用命令行程序,示例如下:

var result = Process.Run("dotnet", ["--version"]);
ConsoleHelper.WriteLineWithColor(JsonSerializer.Serialize(result), ConsoleColor.Green);
result = await Process.RunAsync("dotnet", ["--info"]);
ConsoleHelper.WriteLineWithColor(JsonSerializer.Serialize(result), ConsoleColor.Green);

result = await Process.RunAsync(new ProcessStartInfo("dotnet", "--info"), ApplicationHelper.ExitToken);
ConsoleHelper.WriteLineWithColor(JsonSerializer.Serialize(result), ConsoleColor.Green);

using
 var canceledCts = new CancellationTokenSource();
canceledCts.Cancel();
result = await Process.RunAsync(new ProcessStartInfo("dotnet", "--info"), canceledCts.Token);
ConsoleHelper.WriteLineWithColor(JsonSerializer.Serialize(result), ConsoleColor.Green);

Process.Run/Process.RunAsync 在执行命令时也会输出对应的命令行输出

图片
`Run`
图片
`Run 2`
图片
`Run 3`


Run/RunAsync 也支持超时和取消, 它的返回值是一个新加的 ProcessExitStatus


public
 sealed class ProcessExitStatus
{
    public
 int ExitCode { get; }

    public
 PosixSignal? Signal { get; }

    public
 bool Canceled { get; }
}

当 CancellationToken 取消的时候,返回的结果里的 Canceled 就是 true 就像上面最后一个示例所示

StartAndForget

新增了 Process.StartAndForget  API,借助这个 API 可以方便地启动一个 Process 并忽略它的输出,对于不关心输出的场景比较适用,比如只是启动记事本 notebook 或者 code 等,示例如下:

var processId = Process.StartAndForget("code");
Console.WriteLine($"ProcessId: {processId}");

ReadlAllText/ReadAllBytes/ReadAllLinesAsync

Process 新增了 ReadAllText/ReadAllBytes 方法来方便地获取 Process 的输出

{
    // ReadAllText

    using
 var process = Process.Start(new ProcessStartInfo("dotnet", "--info")
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true,
    });
    Debug.Assert(process is not null);
    var
 output = process.ReadAllText();
    ConsoleHelper.WriteLineWithColor(output.StandardOutput, ConsoleColor.Green);
    ConsoleHelper.WriteLineWithColor(output.StandardError, ConsoleColor.Red);
}

{
    // ReadAllBytesAsync

    using
 var process = Process.Start(new ProcessStartInfo("dotnet", "--info")
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true,
    });
    Debug.Assert(process is not null);
    var
 output = await process.ReadAllBytesAsync();
    ConsoleHelper.WriteLineWithColor(output.StandardOutput.Length.ToString(), ConsoleColor.Green);
    ConsoleHelper.WriteLineWithColor(output.StandardError.Length.ToString(), ConsoleColor.Red);
}

对于输出较多较久的情况可以考虑使用 ReadAllLinesAsync 来流式地读取输出,可以参考下面的示例

using var process = Process.Start(new ProcessStartInfo("dotnet", "--info")
{
    RedirectStandardOutput = true,
    RedirectStandardError = true,
});
Debug.Assert(process is not null);
await
 foreach (var line in process.ReadAllLinesAsync())
{
    ConsoleHelper.WriteLineWithColor(line.Content, line.StandardError ? ConsoleColor.Red : ConsoleColor.Green);
}

流式返回的结果也是新增的 API - ProcessOutputLine

public readonly struct ProcessOutputLine
{
    public
 string Content { get; }
    public
 bool StandardError { get; }
}

Content 是输出的内容,而 StandardError 表示该内容是来自标准输出(StandardOutput)还是来自错误输出(StandardError)

RunAndCapture

前面的几种读取输出方式还是要先实例化 Process 并显式声明重定向输出,相对来说还是稍微麻烦一些

那就可以考虑下 Process.RunAndCapture/Process.RunAndCaptureAsync 方法,这个方法在 Run/RunAsync 的基础之上将输出结果不直接输出到 Console 收集到返回值中,示例如下:

var result = Process.RunAndCaptureText("dotnet", ["--version"]);
ConsoleHelper.WriteLineWithColor(JsonSerializer.Serialize(result), ConsoleColor.Green);
result = await Process.RunAndCaptureTextAsync("dotnet", ["--info"]);
ConsoleHelper.WriteLineWithColor(JsonSerializer.Serialize(result), ConsoleColor.Green);


图片
`Process.RunAndCapture`/`Process.RunAndCaptureAsync`

Process.RunAndCapture/Process.RunAndCaptureAsync 方法对于获取输出结果非常的方便,但是感觉缺少了对应的 ReadAllLinesAsync 对应的简洁方法,提了一个 issue 希望能增加一个对应的 API Process.RunAndCaptureLinesAsync 可以参考:https://github.com/dotnet/runtime/issues/128280

namespace System.Diagnostics;

public
 class Process
{
    public static IAsyncEnumerable<ProcessOutputLine> RunAndCaptureLinesAsync(string fileName, IList<string>? arguments = null, CancellationToken cancellationToken = default);
    public static IAsyncEnumerable<ProcessOutputLine> RunAndCaptureLinesAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default);
}

使用示例:

await foreach (var line in Process.RunAndCaptureLinesAsync("dotnet", ["--info"]))
{
    ConsoleHelper.WriteLineWithColor(line.Content, line.StandardError ? ConsoleColor.Red : ConsoleColor.Green);
}

大家觉得有帮助的话可以帮忙点个赞,希望在 .NET 11 可以一起添加上去

More

子进程生命周期管理终于现代化了

这部分其实是企业级场景最重要的升级。

新增:

KillOnParentExit

父进程退出时,自动终止子进程。

new ProcessStartInfo
{
    KillOnParentExit = true
};

这个功能在:

  • • IDE
  • • CLI Tool
  • • Agent Runtime
  • • Build System
  • • Game Launcher
  • Electron + .NET Hybrid App

里极其重要。

因为以前大量僵尸进程就是:

  • • 主程序崩了
  • • 子进程还活着

现在官方终于补齐了。

StartDetached

允许真正 detached child process。

适合:

  • • daemon
  • • service bootstrap
  • • local AI runtime
  • • background updater

对于 Agent 场景尤其关键

NativeAOT

新的 Process API 引入了  SafeProcessHandle以前 Process API 太重对于 AOT 并不友好,现在更加轻量、对于 AOT 更加友好,对于性能的提升也非常显著,并且有着更少的内存占用,可以参考官方博客的介绍 https://devblogs.microsoft.com/dotnet/process-api-improvements-in-dotnet-11


群贤毕至

访客