如何从交互式流程的

时间:2016-07-19 13:35:07

标签: c# multithreading asynchronous stdout stdin

我正在尝试构建一个使用pythons动态解释器及其eval函数的wpf应用程序。 编辑:我已经提供了更详细的说明here简单来说,我希望能够执行以下操作:

string expression = Console.ReadLine("Please enter your expression");
if (EvaluateWithPythonProcess(expression) > 4)
{
  // Do something
}
else
{
  // Do something else
}

由于我的程序在整个生命周期中使用此功能,因此每次我想开始评估时都无法退出python进程。因此,StdIn,StdOut和StdErr Streams始终保持开放状态。

我能够使用Process类和两个相应的OnOutputDataReceived和OnErrorDataReceived方法启动交互式python.exe,这些方法将数据从stdOut和stdErr传输到StringBuilders:

// create the python process StartupInfo object
                ProcessStartInfo _processStartInfo = new ProcessStartInfo(PythonHelper.PathToPython + "python.exe");

                // python uses "-i" to run in interactive mode
                _processStartInfo.Arguments = "-i";

                // Only start the python process, but don't show a (console) window
                _processStartInfo.WindowStyle = ProcessWindowStyle.Minimized;
                _processStartInfo.CreateNoWindow = true;

                // Enable the redirection of python process std's
                _processStartInfo.UseShellExecute = false;

                _processStartInfo.RedirectStandardOutput = true;
                _processStartInfo.RedirectStandardInput = true;
                _processStartInfo.RedirectStandardError = true;

                // Create the python process object and apply the startupInfos from above
                _pythonProcess = new Process();
                _pythonProcess.StartInfo = _processStartInfo;

                // Start the process, _hasStarted indicates if the process was actually started (true) or if it was reused (false, was already running)

                    _pythonProcess.OutputDataReceived += new DataReceivedEventHandler(OnOutputDataReceived);
                    _pythonProcess.ErrorDataReceived += new DataReceivedEventHandler(OnErrorDataReceived);
                    bool _hasStarted = _pythonProcess.Start();

                    _pythonProcess.BeginOutputReadLine();
                    _pythonProcess.BeginErrorReadLine();
                    _input = _pythonProcess.StandardInput;

但是,我无法通过此异步结果收集来同步我的应用程序。由于两个On * DataReceived()方法是异步调用的,我不知道python是否已完成对表达式的求值。一个可能的解决方案是在向pythons stdIn发送命令之前创建一个等待句柄,之后我可以等待。 OnOutputDataReceived和OnErrorDataReceived方法都可以发出此句柄的信号。但是,这在某种程度上被python的预期行为所掩盖:

                // example A: Import the sys modul in python 
                // this does cause neither an output, nor an error:
                _input.WriteLine("import sys");

                // example B: Writing to pythons stderr or stdout results in Error AND Output, how can I tell if an error occured?
                _input.WriteLine("sys.stderr.write('Initialized stdErr')");

                _input.WriteLine("sys.stdout.write('Initialized stdOut')");

                // example C: This is the intended use, but how can I tell if evaluation has finished succesfully?
                _input.WriteLine("print(4+7)");

                // example D: A simple typo might lead to unforeseeable errors but how can I tell if evaluation has finished succesfully?
                _input.WriteLine("pr int(4+7)");

1 个答案:

答案 0 :(得分:0)

我找到了一个解决方案,我认为这是针对我的具体方案的解决方法而不是一般解决方案。

根据@Peter的评论,我试图弄清楚“人类会如何解决这个问题”

  1. 我必须确保子进程仅通过stdOut与父进程通信。
  2. 我必须创建一个基于消息的协议,以确保子python进程始终报告他是否已收到并理解父c#进程发送的消息,如果是,则报告表达式的评估值。
  3. 我必须找到一种方法来同步后续的写作和阅读
  4. 通过定义一个永远是我父进程的目标的python方法来实现第一点和第二点。我使用pythons异常处理例程来检测错误并防止它写入stdErr,如下所示:

    def PythonEval(expression):
        "Takes an expression and uses the eval function, wrapped in a try-except-statement, to inform the parent process about the value of this expression"
        try:
          print(eval(expression))
          print('CHILD: DONE')
        except:
          print('CHILD: ERROR')
        return
    

    通过将python代码包装在字符串中并将其传递给子进程的stdIn,可以在我的c#应用程序中应用此定义:

    childInput = childProcess.StandardInput;
    
    childInput.WriteLine("def PythonEval(expression):\n" +  
        "\t\"Takes an expression and uses the eval function, wrapped in a try-except-clause, to inform the LMT about the outcome of this expression\"\n" + 
        "\ttry:\n" +
            "\t\tprint(eval(expression))\n" + 
            "\t\tprint('" + _pythonSuccessMessage + "')\n" + 
        "\texcept:\n" + 
            "\t\tprint('" + _pythonErrorMessage + "')\n" + 
        "\treturn\n" + 
        "\n"); // This last newline character is important, it ends the definition of a method if python is in interactive mode 
    

    如果我想在子进程的帮助下计算表达式,那么父进程现在必须将表达式包装在这个python方法的相应调用中:

    childInput.WriteLine("PythonEval('" + expression + "')");
    

    这将在所有情况下导致向子进程的stdOut发送消息,该消息具有“CHILD:DONE | ERROR”形式的最后一行,我可以将其与之比较并在后一种情况下设置一个布尔标志_hasError。整个消息传递给stringBuilder OutputMessage。

    当子进程将此消息发送到其stdOut时,将触发C#进程对象的OutputDataReceivedEvent,并通过OnOutputDataReceived方法异步读取数据。为了与进程的asnyc读取操作同步,我使用AutoResetEvent。它允许将父c#进程与python进程同步,并通过使用AutoResetEvent.WaitOne(int timeout)重载来防止死锁。

    AutoResetEvent在一个特定的方法中手动重置,该方法在waitOne完成之后(在发生超时之前)自动将命令发送到python,并在async OnOutputDataReceived()方法中手动设置,如下所示:

    private AutoResetEvent _outputResetEvent;
    private bool _hasError;
    private StringBuilder _outputMessage;
    
    private void EvaluateWithPython(string expression)
    {
        // Set _outputResetEvent to unsignalled state
        _outputResetEvent.Reset();
    
        // Reset _hasError, 
        _hasError = true;
    
        // Write command to python, using its dedicated method
        childInput.WriteLine("PythonEval('" + expression + "')"); // The ' chars again are important, as the eval method in python takes a string, which is indicated by 's in python
    
        // wait for python to write into stdOut, which is captured by OnOutputDataReceived (below) and sets _outputResetEvent to signalled stat
        bool _timeoutOccured = _outputResetEvent.WaitOne(5000);
    
        // Give OnOutputDataReceived time to finish
        Task.Delay(200).Wait();        
    }
    
    private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
    {
        if (e == null)
        {
            throw new ArgumentNullException();
        }
    
        if (e.Data != null)
        {
            // Pass message into StringBuilder line by line, as OnOutputDataReceived is called line by line
            _outputMessage.AppendLine(e.Data);
    
            // Check for end of message, this is in all cases of the form "CHILD: DONE|ERROR"
            // In this case, set errorFlag if needed and signal the AutoResetEvent
            if (e.Data.Equals("CHILD: ERROR"))
            {
                _hasError = true;
                _outputResetEvent.Set();
            }
            else if (e.Data.Equals("CHILD: DONE"))
            {
                _hasError = false;
                _outputResetEvent.Set();
            }
        }
        else
        {
            // TODO: We only reach this point if child python process ends and stdout is closed (?)
            _outputResetEvent.Set();
        }
    
    
    }
    

    通过这种方法,我可以调用EvaluateWithPython并且可以同步:

    • 检查python是否在发生超时之前完成(如果没有超时则以某种方式作出反应)
    • 如果没有发生超时,我知道_hasError告诉评估是否成功
    • 如果是这种情况,则outputMessage包含next-but-last行的结果。

    为了应对任何监督问题,我还将编写一个OnErrorDataReceived方法,该方法将从python进程中捕获未处理的异常和语法错误,例如,可能抛出异常,因为这应该在我看来永远不会发生。