关于 process-api-net11
面向 .NET 11 及更高版本应用的进程管理 skill,介绍启动、等待和读取外部进程的新增 API。重点是并行捕获标准输出与错误流以避免管道死锁,并提供子进程生命周期及句柄控制选项。
主要功能
- 01运行进程并等待退出
- 02捕获文本、字节或输出行
- 03超时或取消时终止进程
- 04管理子进程退出行为
- 05控制句柄继承或分离启动
适用场景
- 01安全捕获 CLI 命令输出
- 02父进程退出时终止子进程
- 03启动无需跟踪的辅助程序
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill process-api-net11安装后在 Claude Code 中输入/process-api-net11确认已加载。
详细介绍
Process API 改进 — .NET 11
.NET 11 在 System.Diagnostics.Process 中新增的 API 简化了进程管理、消除了样板代码,并能在 .NET 11 或更高版本的应用程序中捕获输出时避免常见的死锁模式。
适用场景
- 在 .NET 11(或更高版本)项目中运行或编排外部进程。
- 需要启动进程、等待其退出并捕获其输出和错误流,同时避免死锁(
Process.RunAndCaptureText[Async])。 - 希望确保父进程退出时自动终止子进程(
KillOnParentExit)。 - 需要在受支持的平台上通过
SafeProcessHandle以轻量、低开销的方式创建进程。 - 需要精细控制句柄继承(
InheritedHandles)或启动分离进程(StartDetached)。
不适用场景
- 项目面向 .NET 10 或更早版本——这些 API 在 .NET 11 之前不可用。
- 默认的
Process.Start()已经满足需求,无需捕获输出或使用高级生命周期规则。
目标框架
<TargetFramework>net11.0</TargetFramework>
新增 API
类型
使用新的便捷方法之前,请先了解以下类型。请直接使用框架类型:
| 类型 | 种类 | 只读属性 |
|---|---|---|
ProcessExitStatus | 密封类 | int ExitCode、bool Canceled、PosixSignal? Signal |
ProcessTextOutput | 密封类 | ProcessExitStatus ExitStatus、string StandardOutput、string StandardError、int ProcessId |
ProcessOutputLine | 只读结构体 | string Content、bool StandardError |
ProcessExitStatus 描述已完成进程的结果。请根据外部命令的约定解读 ExitCode;不存在通用的 Success 属性。ProcessTextOutput 包含退出状态、捕获的输出和错误文本,以及进程 ID。ProcessOutputLine 表示一行输出,并标明其是否来自标准错误流。
高级便捷 API
静态方法
以下 ProcessStartInfo 重载要求 UseShellExecute = false,这是 .NET 的默认值。
Process.Run / Process.RunAsync
启动进程并等待其退出,返回退出状态。不会捕获标准输出或错误。传入 silent: true 时,会在内部将标准句柄重定向到 NUL 设备,从而丢弃标准输出和错误。超时或取消时,进程会被终止。
public static ProcessExitStatus Run(string fileName, IEnumerable<string>? arguments = null, bool silent = false, TimeSpan? timeout = null)
public static Task<ProcessExitStatus> RunAsync(string fileName, IEnumerable<string>? arguments = null, bool silent = false, CancellationToken cancellationToken = default)
public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = null)
public static Task<ProcessExitStatus> RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default)
Process.RunAndCaptureText / Process.RunAndCaptureTextAsync
启动进程,捕获标准输出和错误,并等待其退出。对于避免重定向流导致的死锁非常有用。超时或取消时,进程会被终止。
使用 ProcessStartInfo 重载时,调用方需负责在 ProcessStartInfo 上设置 RedirectStandardOutput = true 和 RedirectStandardError = true(因为 BCL API 无法修改传入的参数)。
public static ProcessTextOutput RunAndCaptureText(string fileName, IEnumerable<string>? arguments = null, TimeSpan? timeout = null)
public static Task<ProcessTextOutput> RunAndCaptureTextAsync(string fileName, IEnumerable<string>? arguments = null, CancellationToken cancellationToken = default)
public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = null)
public static Task<ProcessTextOutput> RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default)
Process.StartAndForget
一个常见的误解是,释放进程对象时也会终止进程。事实并非如此,因为 Process.Dispose 只会释放与进程关联的资源,不会终止进程。
为了让启动进程时无需担心释放进程对象,新增了 Process.StartAndForget。该方法启动可执行文件,返回其 ID,并立即释放与之关联的所有句柄资源。未通过 StandardInputHandle、StandardOutputHandle 或 StandardErrorHandle 提供的标准句柄默认会指向空设备。
如果 UseShellExecute 或 RedirectStandardInput、RedirectStandardOutput、RedirectStandardError 中任一标志为 true,StartAndForget 会抛出 InvalidOperationException。如果需要读取输出,请使用捕获或流式 API。
public static int StartAndForget(string fileName, IEnumerable<string>? arguments = null)
public static int StartAndForget(ProcessStartInfo startInfo)
实例方法
在 Process 实例上调用这些方法,可直接读取 stdout 和 stderr,确保不会因 OS 管道缓冲区溢出而死锁。
注意:调用这些方法时,需要在传递给 Process.Start 的 ProcessStartInfo 上设置 RedirectStandardOutput = true 和 RedirectStandardError = true。
public (string StandardOutput, string StandardError) ReadAllText(TimeSpan? timeout = null)
public Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(CancellationToken cancellationToken = default)
public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(TimeSpan? timeout = null)
public Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(CancellationToken cancellationToken = default)
public IEnumerable<ProcessOutputLine> ReadAllLines(TimeSpan? timeout = null)
public IAsyncEnumerable<ProcessOutputLine> ReadAllLinesAsync(CancellationToken cancellationToken = default)
元组返回值的解构示例:
var (stdout, stderr) = await process.ReadAllTextAsync();
ProcessStartInfo 属性
KillOnParentExit
确保当前(父)进程退出时(包括发生致命崩溃或被强制终止),生成的子进程也会终止。支持 Windows、Linux 和 Android。
public bool KillOnParentExit { get; set; }
在跨平台代码中,应使用受支持平台检查来保护属性赋值,而不只是检查赋予的值。如果需要自动清理,应在不受支持的平台上明确报错,而不是启动不受保护的子进程。
KillOnParentExit 要求 UseShellExecute = false。
InheritedHandles
精确控制子进程继承哪些句柄(文件描述符),以防止意外的资源泄漏。
- 标准句柄(
stdin、stdout、stderr)始终会被包含(无需将其添加到列表中)。 - 将列表设为空列表意味着只继承标准句柄。
- 目前只允许使用
SafeFileHandle和SafePipeHandle实例。 - 在 Windows 上启动新进程时不会使用全局锁(这对需要调整并行生成多个进程的项目而言很重要)。
- 并发启动进程时,不得在
InheritedHandles中传入同一个句柄:运行时会临时更改其继承标志。共享句柄的启动操作应串行执行,或者为每次并发启动使用单独的句柄。 - 传入句柄前,不要启用其继承功能;否则其他进程启动 API 可能会意外继承这些句柄。
- 在不支持原生句柄继承控制的 Unix 系统上,设置此属性可能会大幅降低进程启动性能。
- 非空列表要求
UseShellExecute = false,且不能与非空的UserName同时使用。
public IList<SafeHandle>? InheritedHandles { get; set; }
StartDetached
启动进程时使其脱离父进程的终端或作业会话,确保父进程退出后该进程仍会继续运行。
public bool StartDetached { get; set; }
StartDetached 要求 UseShellExecute = false。
示例
1. 单行运行并捕获输出
运行进程并安全读取所有输出文本,不必担心流死锁风险。对于不需要取消或异步扩展能力的简单 CLI 操作,优先使用同步重载:
using System;
using System.Diagnostics;
// Run 'git status' and capture output
ProcessTextOutput result = Process.RunAndCaptureText("git", ["status"]);
if (result.ExitStatus.ExitCode == 0)
{
Console.WriteLine($"Git Output: {result.StandardOutput}");
}
else
{
Console.WriteLine($"Failed with exit code: {result.ExitStatus.ExitCode}");
Console.WriteLine($"Error: {result.StandardError}");
}
2. 父进程退出时自动终止子进程
在 Windows、Linux 和 Android 上,确保主应用程序终止时,长时间运行的后台工作进程也会被终止。在其他平台上,此示例会在启动子进程前停止:
using System;
using System.Diagnostics;
ProcessStartInfo startInfo = new("dotnet", ["run", "--project", "BackgroundWorker.csproj"]);
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsAndroid())
{
startInfo.KillOnParentExit = true;
}
else
{
throw new PlatformNotSupportedException("This example requires Windows, Linux, or Android.");
}
using Process process = Process.Start(startInfo)!;
// The background worker is now tied to this process's lifecycle
3. 读取输出中的所有行
启动进程并安全读取其输出行:
using System;
using System.Diagnostics;
using System.Threading.Tasks;
ProcessStartInfo startInfo = new("ping", ["127.0.0.1"])
{
RedirectStandardOutput = true,
RedirectStandardError = true
};
using Process process = Process.Start(startInfo)!;
// Read all output lines safely and asynchronously
await foreach (ProcessOutputLine line in process.ReadAllLinesAsync())
{
string prefix = line.StandardError ? "[Err]" : "[Out]";
Console.WriteLine($"{prefix} > {line.Content}");
}
4. 无死锁地读取全部输出(元组返回值)
使用自定义 ProcessStartInfo 启动进程,并将 stdout 和 stderr 读入解构后的元组,避免缓冲区死锁:
using System;
using System.Diagnostics;
using System.Threading.Tasks;
ProcessStartInfo startInfo = new("git", ["diff"])
{
RedirectStandardOutput = true,
RedirectStandardError = true
};
using Process process = Process.Start(startInfo)!;
var (stdout, stderr) = await process.ReadAllTextAsync();
Console.WriteLine($"Diff output: {stdout}");
5. 启动后即忘(即发即弃)
启动辅助可执行文件,而无需持有系统句柄结构:
using System;
using System.Diagnostics;
// Fire and forget, getting back only the process ID
int pid = Process.StartAndForget("notepad.exe");
Console.WriteLine($"Notepad started with PID: {pid}");