.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.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 方法对于获取输出结果非常的方便,但是感觉缺少了对应的 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