如何在C#程序中可靠执行Mercurial的hg status命令
关于Mercurial预提交钩子的进程重定向问题
我用C#编写了一个Mercurial预提交钩子程序,环境为Windows 10 Pro 64位、TortoiseHg 6.4.5,程序目标框架是.NET 6.0。需求是在提交前获取已变更文件列表并处理,但直接重定向stdout的方法在文件数量较多时会挂死,最终只能通过临时文件重定向的方式实现,现提出两个疑问。
最初的直观实现(文件多时挂死)
IEnumerable<string> GetHgStatus() { // 变更文件数量多时会无限挂死,杀死hg.exe进程后stdout/stderr无输出 // 执行命令: hg status -n -m -a var psi = new ProcessStartInfo { // 参数说明: // -n = 不输出前缀状态标识,如 M | A | R // -m = 仅显示已修改文件 // -a = 仅显示已添加文件 FileName = config.HgPath, Arguments = $"status -n -m -a", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, WorkingDirectory = config.RepoPath, }; var exe = Process.Start(psi); try { exe.WaitForExit(); // 永远不会返回 while (!exe.StandardOutput.EndOfStream) yield return exe.StandardOutput.ReadLine(); } finally { exe.Dispose(); } }
当变更文件数量较多时,程序会无限等待,任务管理器中hg.exe进程占用约57-60MB内存但无任何操作,杀死进程后stdout和stderr均无输出。
尝试通过cmd.exe运行(同样挂死)
IEnumerable<string> GetHgStatus() { // 变更文件数量多时仍会无限挂死,杀死hg.exe进程后stdout/stderr无输出 // 执行命令: hg status -n -m -a var psi = new ProcessStartInfo { // 参数说明: // -n = 不输出前缀状态标识,如 M | A | R // -m = 仅显示已修改文件 // -a = 仅显示已添加文件 FileName = "cmd.exe", Arguments = $"/c \"{config.HgPath}\" status -n -m -a", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, WorkingDirectory = config.RepoPath, }; var exe = Process.Start(psi); try { exe.WaitForExit(); // 永远不会返回 while (!exe.StandardOutput.EndOfStream) yield return exe.StandardOutput.ReadLine(); } finally { exe.Dispose(); } }
表现与第一种方式完全一致,若将CreateNoWindow设为false,可看到hg创建的命令行窗口无响应。
临时文件重定向的可行实现
IEnumerable<string> GetHgStatus() { // 需要循环捕获文件访问异常,直到文件可读取 // 执行命令: hg status -n -m -a var output = Path.GetTempFileName(); var psi = new ProcessStartInfo { // 参数说明: // -n = 不输出前缀状态标识,如 M | A | R // -m = 仅显示已修改文件 // -a = 仅显示已添加文件 FileName = "cmd.exe", Arguments = $"/c \"{config.HgPath}\" status -n -m -a > {output}", UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = config.RepoPath, }; var exe = Process.Start(psi); try { exe.WaitForExit(); // 必须调用Close()然后轮询直到文件可访问 exe.Close(); do { try { return File.ReadAllLines(output); } catch (IOException e) when (e.Message.StartsWith("The process cannot access the file")) { continue; } } while (true); } finally { exe.Dispose(); File.Delete(output); } }
疑问
- 为何无法直接运行hg.exe并重定向其stdout,必须通过shell间接重定向至文件?
- 是否有更优方式等待进程关闭文件句柄,以避免轮询循环?
解答
问题1:管道死锁导致挂死
这是典型的进程管道死锁问题:当你同时重定向了stdout和stderr,但代码逻辑是先等待进程退出再读取输出时,一旦hg.exe输出的数据填满了stdout或stderr的系统缓冲区,进程就会被阻塞,等待缓冲区被读取;但你的程序又在等待进程退出,双方互相等待,形成死循环。
命令行中能正常运行是因为cmd会同时处理stdout和stderr的输出,不会让缓冲区满导致进程阻塞,而你的代码逻辑完全颠倒了读取和等待退出的顺序,才引发了挂死。
问题2:替代轮询的最优方案
有两种更简洁的方式解决问题,同时避免临时文件和轮询:
方案1:异步读取输出,避免缓冲区阻塞
IEnumerable<string> GetHgStatus() { var psi = new ProcessStartInfo { FileName = config.HgPath, Arguments = "status -n -m -a", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, WorkingDirectory = config.RepoPath, }; using var exe = Process.Start(psi); var outputLines = new List<string>(); // 异步读取stdout,避免缓冲区满 var stdoutTask = Task.Run(() => { while (!exe.StandardOutput.EndOfStream) { outputLines.Add(exe.StandardOutput.ReadLine()); } }); // 必须同时读取stderr,否则stderr缓冲区满也会导致进程阻塞 var stderrTask = Task.Run(() => { while (!exe.StandardError.EndOfStream) { exe.StandardError.ReadLine(); // 可根据需求处理或忽略错误输出 } }); // 等待进程退出和所有读取任务完成 exe.WaitForExit(); Task.WaitAll(stdoutTask, stderrTask); return outputLines; }
方案2:使用Process事件实时读取输出
IEnumerable<string> GetHgStatus() { var outputLines = new List<string>(); var psi = new ProcessStartInfo { FileName = config.HgPath, Arguments = "status -n -m -a", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, WorkingDirectory = config.RepoPath, }; using var exe = Process.Start(psi); // 注册stdout读取事件 exe.OutputDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) outputLines.Add(e.Data); }; // 注册stderr读取事件,避免缓冲区阻塞 exe.ErrorDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) {} // 可根据需求处理错误输出 }; // 启动异步读取 exe.BeginOutputReadLine(); exe.BeginErrorReadLine(); exe.WaitForExit(); return outputLines; }
如果坚持使用临时文件方案,也可以用FileSystemWatcher监听临时文件的写入完成事件,替代轮询,但这种方式远不如直接修复管道死锁的方案简洁高效。
内容的提问来源于stack exchange,提问作者naasking
相关产品推荐
相关产品推荐

