同步启动一个进程并“流式传输”输出

12
我正在尝试从F#启动进程,等待它完成,但也要逐步读取其输出。这是正确/最佳的方法吗?(在我的情况下,我想要执行Git命令,但这与问题无关)
let gitexecute (logger:string->unit) cmd = 
    let procStartInfo = new ProcessStartInfo(@"C:\Program Files\Git\bin\git.exe", cmd) 

    // Redirect to the Process.StandardOutput StreamReader.
    procStartInfo.RedirectStandardOutput <- true
    procStartInfo.UseShellExecute <- false;

    // Do not create the black window.
    procStartInfo.CreateNoWindow <- true;

    // Create a process, assign its ProcessStartInfo and start it
    let proc = new Process();
    proc.StartInfo <- procStartInfo;
    proc.Start() |> ignore

    // Get the output into a string
    while not proc.StandardOutput.EndOfStream do
        proc.StandardOutput.ReadLine() |> logger

我不理解的是proc.Start()怎么能返回一个布尔值,同时又足够异步,让我可以逐步获取输出。

不幸的是,我目前没有足够大的存储库或足够慢的机器来确定发生的顺序...

更新

我尝试了Brian的建议,它确实有效。

我的问题有点模糊。我的误解在于我认为Process.Start()返回整个进程的成功,而不仅仅是'启动'的成功,因此我看不出它怎么能够运行。


1
我在这种任务中使用异步工作流程取得了一些成功:https://dev59.com/_03Sa4cB1Zd3GeqPtTUQ - elmattic
我其实不确定这里的问题是什么。你上面的代码是否有效?问题是什么? - justin.m.chase
1
在我看来,你可以编写 tester.exe,向标准输出打印几行并刷新,等待两秒钟,然后再写入另外两行……并使用它来测试此代码是否按照你的期望进行操作,对吗? - Brian
@Brian,你说得对,我确实这样做了,而且它有效。谢谢。 - Benjol
@Stringer,有趣,谢谢。 - Benjol
1个回答

14

您编写的代码形式几乎是正确的:process.Start会在另一个进程中启动您指定的进程,因此您的输出流读取将与进程执行并行进行。但是,一个问题是您应该在最后加入process.WaitForExit的调用 - 输出流关闭并不意味着进程已终止。

但是,如果您尝试同时读取进程的stdout和stderr,则会遇到同步读取问题:没有办法同时同步读取2个流 - 如果您读取stdout并且进程正在写入stderr并等待您消耗其输出或反之亦然,则会死锁。

为了解决这个问题,您可以订阅OutputDataRecieved和ErrorDataRecieved,如下所示:

type ProcessResult = { exitCode : int; stdout : string; stderr : string }

let executeProcess (exe,cmdline) =
    let psi = new System.Diagnostics.ProcessStartInfo(exe,cmdline) 
    psi.UseShellExecute <- false
    psi.RedirectStandardOutput <- true
    psi.RedirectStandardError <- true
    psi.CreateNoWindow <- true        
    let p = System.Diagnostics.Process.Start(psi) 
    let output = new System.Text.StringBuilder()
    let error = new System.Text.StringBuilder()
    p.OutputDataReceived.Add(fun args -> output.Append(args.Data) |> ignore)
    p.ErrorDataReceived.Add(fun args -> error.Append(args.Data) |> ignore)
    p.BeginErrorReadLine()
    p.BeginOutputReadLine()
    p.WaitForExit()
    { exitCode = p.ExitCode; stdout = output.ToString(); stderr = error.ToString() }

你也可以写类似以下内容的代码:

async {
    while true do
        let! args = Async.AwaitEvent p.OutputDataReceived
        ...
} |> Async.StartImmediate

用于 F# 风格的响应式事件处理。


你的代码中只有一个错误,就是Process.Start()方法不会返回进程,而是返回一个布尔值。顺便回答一下我没有问到的问题加1分 :) - Benjol
2
我正在调用一个静态的Process.Start(ProcessStartInfo)方法,该方法返回一个Process对象(http://msdn.microsoft.com/en-us/library/0w4h05yb.aspx)。 - Dmitry Lomov
3
这个解决方案存在问题,即 OutputDataReceivedErrorDataReceived 事件只在发送 EOL(行结束)后触发,因此进程可能会要求用户在同一行中回答某些问题(例如,“确定吗?[Y/N]”),然后 F# 进程封装器将无法打印出来。 - knocte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接