.NET 11 中 Process API 升级
.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/RunAsync也支持超时和取消, 它的返回值是一个新加的ProcessExitStatus
public sealed class ProcessExitStatus { public int ExitCode { get; } public PosixSignal? Signal { get; } public bool Canceled { get; } }当 CancellationToken 取消的时候,返回的结果里的Canceled就是true就像上面最后一个示例所示
StartAndForget
新增了Process.StartAndForgetAPI,借助这个 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方法对于获取输出结果非常的方便,但是感觉缺少了对应的ReadAllLinesAsync对应的简洁方法,提了一个 issue 希望能增加一个对应的 APIProcess.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以前ProcessAPI 太重对于 AOT 并不友好,现在更加轻量、对于 AOT 更加友好,对于性能的提升也非常显著,并且有着更少的内存占用,可以参考官方博客的介绍 https://devblogs.microsoft.com/dotnet/process-api-improvements-in-dotnet-11
References
• https://github.com/WeihanLi/SamplesInPractice/blob/main/net11sample/Net11Samples/ProcessSamples.cs
• https://devblogs.microsoft.com/dotnet/process-api-improvements-in-dotnet-11/
• https://github.com/dotnet/runtime/pull/126942
• https://github.com/dotnet/runtime/pull/127210
• https://github.com/dotnet/runtime/pull/125848
• https://github.com/dotnet/runtime/pull/126078
• https://github.com/dotnet/runtime/issues/125838
• https://github.com/dotnet/core/pull/10403
