You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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);
    }
}

疑问

  1. 为何无法直接运行hg.exe并重定向其stdout,必须通过shell间接重定向至文件?
  2. 是否有更优方式等待进程关闭文件句柄,以避免轮询循环?

解答

问题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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 03:25:03