ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C#启动外部EXE并传参:从ProcessStartInfo到进程交互的完整指南

2026/8/25 8:08:45 拓冰建站 浏览量
C#启动外部EXE并传参:从ProcessStartInfo到进程交互的完整指南 1. 项目概述从命令行到程序间通信在C#开发中尤其是开发桌面应用、工具软件或者需要模块化协作的系统时一个非常高频且基础的需求就是让我的主程序去启动另一个独立的可执行文件exe并且不是简单地“打开它”而是要把一些关键信息“告诉”它。这个“告诉”的过程就是传递参数。听起来简单不就是把几个字符串扔过去吗但实际做起来从基础的启动到参数的正确格式化、特殊字符的转义再到进程间的交互、错误处理每一步都可能藏着让你调试半天的“坑”。我自己就经历过一个自动化测试工具需要调用外部的图像处理exe并传递一个包含空格和中文路径的文件名。直接在资源管理器里双击运行没问题但用我的C#程序一调用那边就报“文件未找到”。折腾了半天才发现是参数中的空格没有被正确处理。这还只是冰山一角。随着项目复杂度提升你可能还需要等待被调用程序执行完毕、获取它的输出结果、甚至实时与其进行标准输入输出的交互。这些需求都远远超出了简单的Process.Start(“notepad.exe”)。所以今天我们就来彻底拆解这个“C#程序启动另一个exe并传参”的课题。这不仅是语法问题更是一套关于进程间通信、资源管理和鲁棒性设计的实践。无论你是正在写一个需要调用FFmpeg进行音视频转码的工具还是开发一个集成多个第三方命令行工具的上位机或者是构建一个模块化的插件系统这篇文章里的内容都能直接派上用场。我们会从最基础的ProcessStartInfo和Arguments讲起逐步深入到异步处理、输出捕获、错误流处理以及那些官方文档里不会写的实战避坑指南。2. 核心原理与ProcessStartInfo深度解析为什么我们不能直接用Process.Start(“myapp.exe arg1 arg2”)这种看似直观的方式呢在早期版本的 .NET 中确实有这种重载但它对复杂场景的控制力太弱。现代C#程序启动外部进程核心是围绕System.Diagnostics.Process类和它的搭档ProcessStartInfo展开的。你可以把ProcessStartInfo看作一份详细的“启动任务说明书”而Process类是执行这份说明书并管理后续进程生命的“管家”。2.1ProcessStartInfo关键属性拆解创建一个ProcessStartInfo对象就相当于你在填写这份说明书。下面这些属性是你必须了解的FileName (string) 这是要启动的exe的完整路径。这是唯一一个必须设置的属性。可以是绝对路径如“C:\Tools\ffmpeg.exe”也可以是相对路径或者如果该exe在系统环境变量PATH中可以直接写文件名如“python”。注意 使用相对路径时其基准目录WorkingDirectory至关重要否则程序可能找不到依赖的DLL或配置文件。Arguments (string) 这就是我们要传递的参数列表。它是一个字符串而不是字符串数组。这意味着你需要把多个参数按照命令行规则拼接成一个字符串。这是所有问题的焦点我们会在下一章专门深入。UseShellExecute (bool) 这是一个极其重要的开关默认为true。它决定了启动进程的方式。true 通过操作系统Shell如Windows的explorer.exe来启动进程。这种方式可以打开文档、URL如“http://...”或者关联了默认程序的文件。但是在这种模式下你无法重定向进程的标准输入、输出和错误流StandardInput, Output, Error。false 直接创建新进程。这是我们需要与外部程序进行数据交互读取其输出、向其输入命令时的必须设置。只有设置为false才能重定向流。RedirectStandardOutput / RedirectStandardError / RedirectStandardInput (bool) 当UseShellExecute false时这些属性才有效。设置为true后你就可以通过Process.StandardOutput等流对象来读取被调用程序的输出或向其发送输入。CreateNoWindow (bool) 当UseShellExecute false时有效。设置为true可以阻止被调用程序创建控制台窗口。这对于后台静默运行命令行工具非常有用。WorkingDirectory (string) 设置新进程的初始工作目录。很多程序会基于当前目录寻找配置文件或处理相对路径的文件。如果不设置默认继承当前调用程序的目录。WindowStyle (ProcessWindowStyle) 控制启动后窗口的状态如Normal,Hidden,Minimized,Maximized。注意如果CreateNoWindow true这个设置可能不生效。2.2 基础启动流程与代码骨架理解了核心属性一个最基础的、带有参数传递的启动流程代码如下所示。这是你后续所有复杂操作的起点。using System.Diagnostics; public void StartExeWithArgs() { // 1. 创建并配置“启动任务说明书” ProcessStartInfo startInfo new ProcessStartInfo(); startInfo.FileName C:\MyTools\Converter.exe; // 目标exe路径 startInfo.Arguments -input D:\test file.txt -output D:\result.txt -overwrite; // 参数字符串 startInfo.UseShellExecute false; // 如需交互必须设为false startInfo.CreateNoWindow true; // 不显示黑框窗口 startInfo.WorkingDirectory C:\MyTools\; // 设置工作目录 // 2. 创建“进程管家”并关联说明书 Process process new Process(); process.StartInfo startInfo; try { // 3. 启动进程 bool started process.Start(); if (started) { Console.WriteLine($进程已启动ID: {process.Id}); // 此处可以添加等待结束、读取输出等操作见后续章节 process.WaitForExit(); // 等待进程结束 int exitCode process.ExitCode; Console.WriteLine($进程结束退出代码: {exitCode}); } } catch (Exception ex) { // 4. 异常处理例如文件不存在、权限不足 Console.WriteLine($启动进程失败: {ex.Message}); } finally { // 5. 释放资源 process?.Dispose(); } }这个骨架涵盖了从配置、启动到基础资源管理的完整链条。接下来我们要攻克其中最易出错的部分Arguments字符串的构建。3. 参数字符串构建的艺术与陷阱Arguments属性是一个字符串但命令行解析有其古老而复杂的规则。构建不当轻则参数传递错误重则引发安全漏洞如命令注入。我们的目标是让我们的C#程序构建出的参数字符串与用户在CMD中手动输入的效果完全一致。3.1 基础规则与拼接假设我们要调用一个假想的工具Processor.exe它接受两个参数一个输入文件和一个输出目录。在CMD中我们这样写Processor.exe C:\My Documents\input.dat D:\Output Folder在C#中Arguments就应该设置为startInfo.Arguments C:\My Documents\input.dat D:\Output Folder;注意路径两边的双引号是参数字符串的一部分用来告诉命令行解析器“这是一个整体参数即使内部有空格”。在C#字符串中双引号需要用另一个双引号进行转义所以看起来是...。对于简单的、不含空格的参数直接拼接即可string mode encode; int quality 90; startInfo.Arguments $-mode {mode} -quality {quality}; // 生成-mode encode -quality 903.2 处理特殊字符与安全转义当参数值来自用户输入或变量时直接拼接是危险的。例如string userInput “filename.txt”; // 用户输入了 startInfo.Arguments $-input {userInput}; // 生成-input filename.txt在Windows命令提示符中是命令分隔符这会导致name.txt被当作一个新命令解析可能引发意外执行。解决方案是使用系统提供的转义方法System.Security.SecurityElement.Escape虽然常用于XML但对于简单的转义并非最佳。更通用的做法是对于可能包含空格、引号、、|、、 等特殊字符的参数始终为其加上双引号。但更稳健的方式是使用 .NET 提供的专用类。从 .NET Core 3.0 / .NET 5 开始强烈推荐使用System.CommandLine命名空间外的CommandLineBuilder或更底层的System.Diagnostics.Process相关的辅助方法实际上.NET Framework 和 .NET Core 早期版本没有内置完美的解决方案。一个广泛接受的实践是模仿System.CommandLine的内部逻辑或使用社区库但对于大多数场景遵循以下规则手动处理是可行的参数值本身不含双引号用双引号包裹整个值。value-value。参数值本身包含双引号这是最复杂的情况。Windows命令行解析器使用反斜杠\来转义双引号。规则是用双引号包裹整个值并将值内部的所有双引号替换为\。例如要传递参数He said, Hello World.正确的Arguments字符串应为He said, \Hello World\.在C#代码中startInfo.Arguments He said, \Hello World\.;(看起来复杂但遵循规则即可)。为了简化我们可以编写一个辅助方法public static string EscapeCommandLineArgument(string argument) { // 空参数直接返回空字符串 if (string.IsNullOrEmpty(argument)) return string.Empty; // 如果参数不含空格、制表符、双引号可以直接返回可选优化但为了安全统一加引号更简单 // 这里采用更安全的策略总是用双引号包裹并转义内部的双引号 StringBuilder sb new StringBuilder(); sb.Append(); foreach (char c in argument) { if (c ) { sb.Append(\\); // 在双引号前添加反斜杠 sb.Append(); } else if (c \\) { // 处理反斜杠在参数末尾的连续反斜杠需要特殊处理这里简化处理 sb.Append(\\); } else { sb.Append(c); } } sb.Append(); return sb.ToString(); } // 使用 string arg1 EscapeCommandLineArgument(C:\My Files\data.txt); string arg2 EscapeCommandLineArgument(Text with \quotes\ inside.); startInfo.Arguments ${arg1} {arg2};3.3 使用System.CommandLine进行现代化构建.NET 5如果你的项目基于较新的.NET版本.NET 5, 6, 7, 8等处理命令行参数有一个更现代、更强大的官方方案System.CommandLine。虽然它主要用于构建你自己的命令行应用程序但其底层用于转义和拼接参数的逻辑是可靠且经过充分测试的。我们可以“借用”它的CommandLineBuilder来安全地构建参数字符串。首先通过NuGet安装System.CommandLine包。using System.CommandLine; public static string BuildCommandLineArguments(params string[] args) { var commandLineBuilder new CommandLineBuilder(); foreach (var arg in args) { commandLineBuilder.AddArgument(arg); } // CommandLineBuilder 内部会正确处理转义 return commandLineBuilder.Build().Arguments; } // 使用示例 string safeArguments BuildCommandLineArguments( -input, C:\My Files\input.txt, -message, He said, Hello World. ); // safeArguments 将是: -input C:\\My Files\\input.txt -message He said, \Hello World\. startInfo.Arguments safeArguments;这种方法将转义的复杂性交给了成熟可靠的库极大地减少了出错的可能尤其是在处理用户提供的、不可预知的输入时。4. 高级交互捕获输出、输入与异步控制仅仅启动进程并传递参数往往不够。我们通常需要获取外部程序执行后的结果控制台输出。在外部程序运行时向其发送指令标准输入。不阻塞主线程地等待进程结束异步操作。处理标准错误流以区分正常日志和错误信息。这些功能都要求将ProcessStartInfo.UseShellExecute设置为false。4.1 同步读取输出与错误这是最常见的场景启动一个命令行工具等它跑完然后读取它打印的所有内容。public (string output, string error, int exitCode) RunCommandSync(string fileName, string arguments) { var outputBuilder new StringBuilder(); var errorBuilder new StringBuilder(); var startInfo new ProcessStartInfo { FileName fileName, Arguments arguments, UseShellExecute false, RedirectStandardOutput true, RedirectStandardError true, CreateNoWindow true, StandardOutputEncoding Encoding.UTF8, // 重要指定输出编码避免中文乱码 StandardErrorEncoding Encoding.UTF8 }; using (var process new Process { StartInfo startInfo }) { // 设置输出/错误数据接收事件 process.OutputDataReceived (sender, e) { if (!string.IsNullOrEmpty(e.Data)) outputBuilder.AppendLine(e.Data); }; process.ErrorDataReceived (sender, e) { if (!string.IsNullOrEmpty(e.Data)) errorBuilder.AppendLine(e.Data); }; process.Start(); // 开始异步读取输出和错误流 process.BeginOutputReadLine(); process.BeginErrorReadLine(); // 等待进程退出 process.WaitForExit(); // 确保所有异步读取完成 process.WaitForExit(); // 第二次调用WaitForExit以确保所有数据被接收某些场景下需要 // 或者使用 process.CancelOutputRead(); process.CancelErrorRead(); 来结束读取 return (outputBuilder.ToString(), errorBuilder.ToString(), process.ExitCode); } }关键点RedirectStandardOutput和RedirectStandardError必须设为true。通过事件OutputDataReceived和ErrorDataReceived来异步接收数据。如果使用process.StandardOutput.ReadToEnd()同步读取在输出量很大时可能导致死锁如果子进程同时向错误流填充大量数据而父进程未读取。指定编码 (StandardOutputEncoding)至关重要特别是被调用程序输出中文等非ASCII字符时。默认编码可能是系统活动代码页导致乱码。通常设为Encoding.UTF8是安全的选择前提是外部程序也使用UTF8输出。WaitForExit()必须在BeginOutputReadLine()之后调用以确保进程结束后事件仍能处理完缓冲区中的数据。4.2 实现异步等待与实时交互对于执行时间较长或需要实时查看进度、进行交互的程序同步等待会阻塞UI线程导致程序“卡死”。我们需要异步操作。public async Task(string output, string error, int exitCode) RunCommandAsync(string fileName, string arguments, CancellationToken cancellationToken default) { var outputBuilder new StringBuilder(); var errorBuilder new StringBuilder(); var startInfo new ProcessStartInfo { FileName fileName, Arguments arguments, UseShellExecute false, RedirectStandardOutput true, RedirectStandardError true, CreateNoWindow true, StandardOutputEncoding Encoding.UTF8, StandardErrorEncoding Encoding.UTF8 }; using (var process new Process { StartInfo startInfo }) { var tcs new TaskCompletionSourceint(); process.Exited (sender, args) { tcs.TrySetResult(process.ExitCode); }; process.EnableRaisingEvents true; // 必须设置为true才能触发Exited事件 process.OutputDataReceived (sender, e) { if (!string.IsNullOrEmpty(e.Data)) { outputBuilder.AppendLine(e.Data); // 可以在这里实时处理每一行输出例如更新UI进度条 OnOutputDataReceived?.Invoke(this, e.Data); } }; process.ErrorDataReceived (sender, e) { if (!string.IsNullOrEmpty(e.Data)) errorBuilder.AppendLine(e.Data); }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); // 使用Task.WhenAny来同时等待进程结束和取消令牌 var exitTask tcs.Task; var completedTask await Task.WhenAny(exitTask, Task.Delay(Timeout.Infinite, cancellationToken)); if (completedTask exitTask) { // 进程正常结束 await exitTask; // 确保获取ExitCode return (outputBuilder.ToString(), errorBuilder.ToString(), process.ExitCode); } else { // 被取消 try { process.Kill(); } catch { /* 忽略杀死进程时的异常 */ } cancellationToken.ThrowIfCancellationRequested(); return (outputBuilder.ToString(), errorBuilder.ToString(), -1); // 或用特定代码表示取消 } } } // 定义事件用于实时回调输出 public event EventHandlerstring OnOutputDataReceived;关键点使用process.EnableRaisingEvents true和process.Exited事件来感知进程结束而不是阻塞的WaitForExit()。将进程结束封装为一个Task便于使用async/await进行异步等待。整合了CancellationToken允许用户取消长时间运行的任务并优雅地终止外部进程。通过事件OnOutputDataReceived实现了输出数据的实时回调这对于需要显示实时日志的GUI应用非常有用。4.3 向进程发送输入标准输入有些交互式命令行工具如mysql客户端、某些配置脚本需要从标准输入读取命令。我们可以通过RedirectStandardInput并向process.StandardInput流写入数据来实现。public void SendInputToProcess() { var startInfo new ProcessStartInfo { FileName python, Arguments -i, // 以交互模式启动Python UseShellExecute false, RedirectStandardInput true, RedirectStandardOutput true, RedirectStandardError true, CreateNoWindow false, // 这里可以显示窗口观察 StandardOutputEncoding Encoding.UTF8 }; using (var process Process.Start(startInfo)) using (var writer process.StandardInput) using (var reader process.StandardOutput) { if (writer.BaseStream.CanWrite) { // 向Python交互环境发送命令 writer.WriteLine(print(Hello from C#)); writer.WriteLine(x 5 3); writer.WriteLine(print(fx {x})); writer.WriteLine(exit()); // 发送退出命令 writer.Flush(); } // 读取Python的输出 string output reader.ReadToEnd(); Console.WriteLine(Python输出:); Console.WriteLine(output); process.WaitForExit(); } }重要警告 向标准输入写入后必须关闭输入流writer.Close()或process.StandardInput.Close()以告知子进程输入已结束。否则子进程可能会一直等待更多输入导致ReadToEnd()挂起。5. 实战避坑指南与高级技巧掌握了基本方法和高级交互后我们来看看那些只有踩过坑才知道的细节和优化技巧。5.1 路径、环境变量与工作目录的坑相对路径的陷阱 当FileName是相对路径如“tool.exe”时系统会在PATH环境变量中查找但也会受到WorkingDirectory的影响。最稳妥的方式是如果可能始终使用可执行文件的绝对路径。如果必须用相对路径明确设置startInfo.WorkingDirectory到该exe所在的目录或其预期的上下文目录。使用Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “tools”, “myapp.exe”)来构建基于你应用程序启动目录的绝对路径。环境变量继承 默认情况下子进程会继承父进程的所有环境变量。你可以通过startInfo.EnvironmentVariables字典来添加、修改或删除特定的环境变量。startInfo.EnvironmentVariables[MY_CUSTOM_VAR] SomeValue; // 如果需要也可以清除继承的变量 // startInfo.EnvironmentVariables.Clear();中文路径与编码 除了之前提到的输出流编码参数字符串本身也可能包含中文。确保你的C#源文件保存的编码通常是UTF-8 with BOM与系统控制台代码页匹配不是必须的因为参数是通过进程创建API传递的不是通过控制台字符串。但为了最大兼容性在构建参数字符串时使用正常的C#字符串即可.NET会处理Unicode到ANSI如果需要的转换。更复杂的情况涉及被调用程序是原生Win32程序且期望ANSI字符串这时可能需要使用Encoding.Default进行转换但现代程序大多能处理Unicode路径。5.2 进程生命周期管理与资源释放using语句是必须的Process类实现了IDisposable。务必将其包裹在using语句中或在finally块中调用Dispose()。否则即使进程退出一些系统句柄可能仍被占用导致资源泄漏。等待超时与强制终止process.WaitForExit(int milliseconds)可以指定超时时间。如果超时你可能需要决定是否强制终止进程 (process.Kill())。Kill()是强制性的可能阻止子进程进行清理工作。更好的做法是先尝试友好地关闭例如向标准输入发送退出命令或发送关闭消息给GUI程序仅在超时后使用Kill。if (!process.WaitForExit(30000)) // 等待30秒 { Console.WriteLine(进程未在指定时间内结束尝试强制终止。); process.Kill(); // 注意Kill之后可能需要再WaitForExit一下确保进程完全退出 process.WaitForExit(); }处理子进程的子进程process.Kill()通常只杀死直接启动的进程。如果这个进程又创建了子进程例如你的C#程序启动了cmd.execmd.exe又启动了ping.exeKill可能不会杀死孙子进程。在Windows上你可以使用作业对象 (Job Object) 来管理进程树但这涉及更复杂的P/Invoke编程。5.3 性能与稳定性考量避免频繁启动进程 启动进程是相对昂贵的操作。如果需要在循环中多次调用同一个轻量级工具考虑是否可以将该工具的功能集成到主程序中或者使用进程池、保持一个进程实例并通过标准输入进行多次交互如果工具支持。输出缓冲区死锁 这是经典陷阱。如果一个子进程向标准输出写入大量数据同时向标准错误流也写入大量数据而父进程只读取其中一个流例如只用StandardOutput.ReadToEnd()缓冲区可能会被填满导致子进程阻塞等待父进程读取而父进程又在等待子进程退出形成死锁。解决方案就是始终使用异步读取 (BeginOutputReadLine) 或同时异步读取两个流如前文示例所示。提升权限以管理员身份运行 如果你的程序需要启动一个需要管理员权限的exe而你的主程序本身不是以管理员运行的直接启动会失败。你可以设置startInfo.Verb “runas”;。当UseShellExecute true时这会触发UAC提权对话框。startInfo.FileName net.exe; startInfo.Arguments start someservice; startInfo.Verb runas; // 请求提升权限 startInfo.UseShellExecute true; // Verb需要UseShellExecute为true Process.Start(startInfo);注意设置Verb “runas”后你将无法重定向输入输出流因为UseShellExecute必须为true。6. 综合案例封装一个健壮的外部进程调用器结合以上所有知识点我们可以设计一个相对健壮、易用的辅助类用于处理大多数外部进程调用场景。using System; using System.Diagnostics; using System.Text; using System.Threading; using System.Threading.Tasks; public class ExternalProcessRunner { public class ExecutionResult { public int ExitCode { get; set; } public string StandardOutput { get; set; } string.Empty; public string StandardError { get; set; } string.Empty; public bool TimedOut { get; set; } public TimeSpan ExecutionTime { get; set; } } public static async TaskExecutionResult RunAsync( string fileName, string arguments, string workingDirectory null, int timeoutMilliseconds Timeout.Infinite, CancellationToken cancellationToken default, Actionstring onOutputReceived null, Actionstring onErrorReceived null) { var result new ExecutionResult(); var stopwatch Stopwatch.StartNew(); var outputBuilder new StringBuilder(); var errorBuilder new StringBuilder(); var startInfo new ProcessStartInfo { FileName fileName, Arguments arguments, UseShellExecute false, RedirectStandardOutput true, RedirectStandardError true, CreateNoWindow true, StandardOutputEncoding Encoding.UTF8, StandardErrorEncoding Encoding.UTF8, }; if (!string.IsNullOrEmpty(workingDirectory)) startInfo.WorkingDirectory workingDirectory; using (var process new Process { StartInfo startInfo }) using (var cts CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { if (timeoutMilliseconds ! Timeout.Infinite) { cts.CancelAfter(timeoutMilliseconds); } var tcs new TaskCompletionSourcebool(); process.Exited (sender, args) tcs.TrySetResult(true); process.EnableRaisingEvents true; process.OutputDataReceived (sender, e) { if (e.Data ! null) { outputBuilder.AppendLine(e.Data); onOutputReceived?.Invoke(e.Data); } }; process.ErrorDataReceived (sender, e) { if (e.Data ! null) { errorBuilder.AppendLine(e.Data); onErrorReceived?.Invoke(e.Data); } }; try { process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); // 等待进程退出或取消/超时 await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, cts.Token)); if (cts.Token.IsCancellationRequested) { result.TimedOut (timeoutMilliseconds ! Timeout.Infinite); // 尝试友好终止然后强制终止 if (!process.HasExited) { process.CloseMainWindow(); // 对GUI程序可能有效 await Task.Delay(500); if (!process.HasExited) process.Kill(); } // 等待进程实际退出 await Task.Run(() process.WaitForExit(5000)); if (cancellationToken.IsCancellationRequested) cancellationToken.ThrowIfCancellationRequested(); else throw new TimeoutException($进程执行超时 ({timeoutMilliseconds}ms)。); } else { // 进程正常退出确保拿到最终退出码 await tcs.Task; } // 再给一点时间让异步读取事件处理完最后的数据 await Task.Delay(100); } finally { stopwatch.Stop(); result.ExecutionTime stopwatch.Elapsed; if (process.HasExited) { result.ExitCode process.ExitCode; } result.StandardOutput outputBuilder.ToString().TrimEnd(); result.StandardError errorBuilder.ToString().TrimEnd(); } } return result; } // 同步版本简化适用于简单场景 public static ExecutionResult Run( string fileName, string arguments, string workingDirectory null, int timeoutMilliseconds Timeout.Infinite) { // 注意同步版本无法很好地处理实时输出回调 var task RunAsync(fileName, arguments, workingDirectory, timeoutMilliseconds); task.Wait(); // 在UI线程上调用此方法会导致死锁谨慎使用 return task.Result; } } // 使用示例 public async Task UseRunnerAsync() { var result await ExternalProcessRunner.RunAsync( fileName: ffmpeg.exe, arguments: $-i \{inputVideo}\ -c:v libx264 -crf 23 \{outputVideo}\, workingDirectory: C:\FFmpeg\bin, timeoutMilliseconds: 300000, // 5分钟超时 onOutputReceived: (line) Console.WriteLine($[FFmpeg] {line}), onErrorReceived: (line) Console.Error.WriteLine($[FFmpeg Error] {line}) ); if (result.ExitCode 0) { Console.WriteLine($转换成功耗时{result.ExecutionTime.TotalSeconds:F2}秒); } else { Console.WriteLine($转换失败退出码{result.ExitCode}); Console.WriteLine($错误输出{result.StandardError}); } }这个ExternalProcessRunner类封装了超时控制、取消支持、实时输出回调、完整的输出/错误捕获以及基本的资源管理。它为你处理了大部分繁琐和易错的细节让你可以更专注于业务逻辑。在实际项目中你可能还需要根据具体需求对其进行扩展比如添加环境变量配置、更精细的进程树管理、或者更复杂的错误重试逻辑。