首页 > 解决方案 > 在 C#/.NET 进程中重定向标准错误时缺少输出

问题描述

我正在使用名为“sam-ba”v3.5 的第 3 方命令行工具(可在此处免费获得)。它是一个 C++ / QML 命令行工具,可与硬件模块连接以读取/写入数据。在大多数情况下,命令的输出被发送到标准错误。

我有一个 C#/.NET 应用程序,它创建一个 Process 对象来执行 sam-ba 工具并运行命令。执行命令按预期工作。并不总是有效的是标准错误输出的重定向。在某些命令中,C# 应用程序未接收到部分或全部输出。例如,下面是直接在 Windows 10 命令行中使用 sam-ba 工具执行命令:

C:\Temp\Stuff\sam-ba_3.5>sam-ba -p serial:COM5 -d sama5d3 -m version
Error: Cannot open invalid port 'COM5'
Cannot open invalid port 'COM5'

以下是来自 C# 应用程序的一些简单代码,用于创建 Process 对象以使用相同的命令执行 sam-ba 工具:

Process p = new Process
{
    StartInfo = new ProcessStartInfo("sam-ba.exe", "-p serial:COM5 -d sama5d3 -m version")
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        CreateNoWindow = true
    }
};

p.Start();
string output = p.StandardOutput.ReadToEnd();
string error = p.StandardError.ReadToEnd();
p.WaitForExit();

Console.WriteLine("Standard Out: " + output);
Console.WriteLine("Standard Error: " + error);

C# 应用程序的输出:

Standard Out:
Standard Error: Cannot open invalid port 'COM5'

在这个简单的示例中,只有 1 条输出行被重定向到标准错误,而另一条则没有。我尝试了许多不同的命令,结果好坏参半。有时我得到了一切,有时是部分输出,有时没有输出。

现在......这是真正的问题。以下是一个 python 脚本 (v3.8),它完全执行 C# 应用程序正在执行的操作:

import subprocess
import sys

result = subprocess.run("sam-ba.exe -p serial:COM5 -d sama5d3 -m version", capture_output=True, text=True)
print("stdout:", result.stdout)
print("stderr:", result.stderr)

此脚本始终将正确的输出返回到标准错误。 但是...当我从 C# 应用程序运行此脚本以创建 C# -> python -> sam-ba 链时,我遇到了流中缺少输出的相同问题。

这让我得出两个结论:

  1. 该 sam-ba 工具中的某些内容与输出文本的方式不同。格式,内容,......一些东西。该代码中的某处存在不一致
  2. C# Process 对象在执行外部应用程序时创建的环境有所不同,而直接运行外部应用程序时不会发生这种情况。否则,为什么 python 脚本在直接运行时会得到所有输出,而在通过 C# Process 对象运行时却没有?

把我带到这里的是#2。我正在寻找有关如何诊断此问题的任何见解。我做错了什么,我可以在 Process 对象中尝试的设置,关于数据如何进入流而不是重定向出来的想法,或者是否有人以前见过这样的事情以及他们如何解决它。

更新

掌握了 sam-ba 工具的源代码。C# 应用程序未捕获的输出来自 QML 文件。他们正在使用我无法找到任何详细信息的“print()”方法。C# 应用程序可以捕获的输出通过信号传递回 C++ 端,然后发送到标准错误。这反馈到我的结论 #1 中,他们的代码存在不一致之处。

不过,这可能意味着 C# 和 QT/QML 之间存在冲突,这可以解释为什么 Python 脚本获得 QML 输出,而 C# 应用程序却没有。

标签: c#.netqtqmlio-redirection

解决方案


以下在运行进程时使用ShellExecute而不是CreateProcess 。当使用 ShellExecute 时,不能为 Process 重定向 StandardOutput 和/或 StandardError。为了解决这个问题,StandardOutput 和 StandardError 都被重定向到一个临时文件,然后从临时文件中读取数据——这似乎导致从 cmd 窗口运行时看到的相同输出。

注意:在以下代码中,必须使用%windir%\system32\cmd.exe(例如:C:\Windows\system32\cmd.exe)和/c选项。请参阅下面的使用部分。

添加使用语句using System.Diagnostics;

然后尝试以下操作:

public string RunProcess(string fqExePath, string arguments, bool runAsAdministrator = false)
{
    string result = string.Empty;
    string tempFilename = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "tempSam-ba.txt");
    string tempArguments = arguments;

    if (String.IsNullOrEmpty(fqExePath))
    {
        Debug.WriteLine("fqExePath not specified");
        return "Error: fqExePath not specified";
    }

    //redirect both StandardOutput and StandardError to a temp file
    if (!arguments.Contains("2>&1"))
    {
        tempArguments += String.Format(" {0} {1} {2}", @"1>", tempFilename, @"2>&1");
    }

    //create new instance
    ProcessStartInfo startInfo = new ProcessStartInfo(fqExePath, tempArguments);

    if (runAsAdministrator)
    {
        startInfo.Verb = "runas"; //elevates permissions
    }//if

    //set environment variables
    //pStartInfo.EnvironmentVariables["SomeVar"] = "someValue";

    startInfo.RedirectStandardError = false;
    startInfo.RedirectStandardOutput = false;

    startInfo.RedirectStandardInput = false;

    startInfo.UseShellExecute = true; //use ShellExecute instead of CreateProcess
    startInfo.CreateNoWindow = false;

    startInfo.WindowStyle = ProcessWindowStyle.Hidden;
    startInfo.ErrorDialog = false;
    startInfo.WorkingDirectory = System.IO.Path.GetDirectoryName(fqExePath);

    using (Process p = Process.Start(startInfo))
    {
        //start
        p.Start();

        //waits until the process is finished before continuing
        p.WaitForExit();
    }

    //read output from temp file
    //file may still be in use, so try to read it.
    //if it is still in use, sleep and try again
    if (System.IO.File.Exists(tempFilename))
    {
        string errMsg = string.Empty;
        int count = 0;
        do
        {
            //re-initialize
            errMsg = string.Empty;

            try
            {
                result = System.IO.File.ReadAllText(tempFilename);
                Debug.WriteLine(result);
            }
            catch(System.IO.IOException ex)
            {
                errMsg = ex.Message;
            }
            catch (Exception ex)
            {
                errMsg = ex.Message;
            }

            System.Threading.Thread.Sleep(125);
            count += 1; //increment
        } while (!String.IsNullOrEmpty(errMsg) && count < 10);

        //delete temp file
        System.IO.File.Delete(tempFilename);
    }

    return result;
}

用法

RunProcess(@"C:\Windows\system32\cmd.exe", @"/c C:\Temp\sam-ba_3.5\sam-ba.exe -p serial:COM5 -d sama5d3 -m version");

注意/c C:\Temp\sam-ba_3.5\sam-ba.exe -p serial:COM5 -d sama5d3 -m version是进程“参数”属性的值。

更新

选项 2

这是一个使用命名管道的解决方案。Process 用于将输出重定向到命名管道而不是文件。创建一个命名管道“服务器”,它侦听来自客户端的连接。然后System.Diagnostics.Process用于运行所需的命令并将输出重定向到命名管道服务器。“服务器”读取输出,然后引发事件“DataReceived”,该事件会将数据返回给任何订阅者。

命名管道服务器代码来自此处,但我已对其进行了修改。我添加了许多可以订阅的事件。通过将“ShutdownWhenOperationComplete”设置为“true”,我还添加了服务器在完成读取数据后自行关闭的功能。

创建一个名为:HelperNamedPipeServer.cs 的类

HelperNamedPipeServer.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO.Pipes;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Security.Principal;

namespace ProcessTest
{
    public class HelperNamedPipeServer : IDisposable
    {
        //delegates
        public delegate void EventHandlerClientConnected(object sender, bool e);
        public delegate void EventHandlerDataReceived(object sender, string data);
        public delegate void EventHandlerOperationCompleted(object sender, bool e);
        public delegate void EventHandlerMessageComplete(object sender, bool e);
        public delegate void EventHandlerReadComplete(object sender, bool e);
        public delegate void EventHandlerServerShutdown(object sender, bool e);
        public delegate void EventHandlerServerStarted(object sender, bool e);

        //event that subscribers can subscribe to
        public event EventHandlerClientConnected ClientConnected;
        public event EventHandlerDataReceived DataReceived;
        public event EventHandlerMessageComplete MessageReadComplete;
        public event EventHandlerOperationCompleted OperationCompleted;
        public event EventHandlerReadComplete ReadComplete;
        public event EventHandlerServerShutdown ServerShutdown;
        public event EventHandlerServerStarted ServerStarted;

        
        public bool IsClientConnected
        {
            get
            {
                if (_pipeServer == null)
                {
                    return false;
                }
                else
                {
                    return _pipeServer.IsConnected;
                }
            }
        }

        public string PipeName { get; set; } = string.Empty;

        public bool ShutdownWhenOperationComplete { get; set; } = false;

        //private int _bufferSize = 4096;
        private int _bufferSize = 65535;

        //private volatile NamedPipeServerStream _pipeServer = null;
        private NamedPipeServerStream _pipeServer = null;

        public HelperNamedPipeServer()
        {
            PipeName = "sam-ba-pipe";
        }

        public HelperNamedPipeServer(string pipeName)
        {
            PipeName = pipeName;
        }

        private NamedPipeServerStream CreateNamedPipeServerStream(string pipeName)
        {
            //named pipe with security
            //SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null); //member of Administrators group
            //SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.WorldSid, null); //everyone
            //SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); //member of Users group

            //PipeAccessRule rule = new PipeAccessRule(sid, PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow);
            //PipeSecurity pSec = new PipeSecurity();
            //pSec.AddAccessRule(rule);

            //named pipe - with specified security
            //return new NamedPipeServerStream(PipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, _bufferSize, _bufferSize, pSec);

            //named pipe - access for everyone
            //return new System.IO.Pipes.NamedPipeServerStream(pipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Message, PipeOptions.Asynchronous);
            return new System.IO.Pipes.NamedPipeServerStream(pipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
            
        }

        public void Dispose()
        {
            Shutdown();
        }

        private void OnClientConnected()
        {
            LogMsg("OnClientConnected");

            //raise event
            if (ClientConnected != null)
                ClientConnected(this, true);
        }

        private void OnDataReceived(string data)
        {
            LogMsg("OnClientConnected");

            //raise event
            if (DataReceived != null && !String.IsNullOrEmpty(data))
            {
                if (DataReceived != null)
                    DataReceived(this, data);
            }
        }

        private void OnMessageReadComplete()
        {
            LogMsg("OnMessageReadComplete");

            //raise event
            if (MessageReadComplete != null)
                MessageReadComplete(this, true);
        }


        private void OnOperationCompleted()
        {
            LogMsg("OnOperationCompleted");

            //raise event
            if (OperationCompleted != null)
                OperationCompleted(this, true);
        }

        private void OnReadComplete()
        {
            LogMsg("OnReadComplete");

            //raise event
            if (ReadComplete != null)
                ReadComplete(this, true);
        }

        private void OnServerShutdown()
        {
            LogMsg("OnServerShutdown");

            //raise event
            if (ServerShutdown != null)
                ServerShutdown(this, true);
        }

        private void OnServerStarted()
        {
            LogMsg("OnServerStarted");

            //raise event
            if (ServerStarted != null)
                ServerStarted(this, true);
        }


        private async void DoConnectionLoop(IAsyncResult result)
        {   //wait for connection, then process the data

            if (!result.IsCompleted) return;
            if (_pipeServer == null) return;

            //IOException = pipe is broken
            //ObjectDisposedException = cannot access closed pipe
            //OperationCanceledException - read was canceled

            //accept client connection
            try
            {
                //client connected - stop waiting for connection
                _pipeServer.EndWaitForConnection(result);

                OnClientConnected(); //raise event
            }
            catch (IOException) { RebuildNamedPipe(); return; }
            catch (ObjectDisposedException) { RebuildNamedPipe(); return; }
            catch (OperationCanceledException) { RebuildNamedPipe(); return; }

            while (IsClientConnected)
            {
                if (_pipeServer == null) break;

                try
                {
                    // read from client
                    string clientMessage = await ReadClientMessageAsync(_pipeServer);

                    OnDataReceived(clientMessage); //raise event
                }
                catch (IOException) { RebuildNamedPipe(); return; }
                catch (ObjectDisposedException) { RebuildNamedPipe(); return; }
                catch (OperationCanceledException) { RebuildNamedPipe(); return; }
            }

            //raise event
            OnOperationCompleted();

            if (!ShutdownWhenOperationComplete)
            {
                
                //client disconnected. start listening for clients again
                if (_pipeServer != null)
                    RebuildNamedPipe();
            }
            else
            {
                Shutdown();
            }
        }

        private void LogMsg(string msg)
        {
            //ToDo: log message
            string output = String.Format("{0} - {1}", DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), msg);

            //ToDo: uncomment this line, if desired
            //Debug.WriteLine(output);
        }

        private void RebuildNamedPipe()
        {
            Shutdown();
            _pipeServer = CreateNamedPipeServerStream(PipeName);
            _pipeServer.BeginWaitForConnection(DoConnectionLoop, null);
        }


        private async Task<string> ReadClientMessageAsync(NamedPipeServerStream stream)
        {
            byte[] buffer = null;
            string clientMsg = string.Empty;
            StringBuilder sb = new StringBuilder();
            int msgIndex = 0;
            int read = 0;

            LogMsg("Reading message...");

            if (stream.ReadMode == PipeTransmissionMode.Byte)
            {
                LogMsg("PipeTransmissionMode.Byte");

                //byte mode ignores message boundaries
                do
                {
                    //create instance
                    buffer = new byte[_bufferSize];

                    read = await stream.ReadAsync(buffer, 0, buffer.Length);

                    if (read > 0)
                    {
                        clientMsg = Encoding.UTF8.GetString(buffer, 0, read);
                        //string clientMsg = Encoding.Default.GetString(buffer, 0, read);

                        //remove newline
                        //clientMsg = System.Text.RegularExpressions.Regex.Replace(clientString, @"\r\n|\t|\n|\r|", "");

                        //LogMsg("clientMsg [" + msgIndex + "]: " + clientMsg);
                        sb.Append(clientMsg);

                        msgIndex += 1; //increment
                    }
                } while (read > 0);

                //raise event
                OnReadComplete();
                OnMessageReadComplete();
            }
            else if (stream.ReadMode == PipeTransmissionMode.Message)
            {
                LogMsg("PipeTransmissionMode.Message");

                do
                {
                    do
                    {
                        //create instance
                        buffer = new byte[_bufferSize];

                        read = await stream.ReadAsync(buffer, 0, buffer.Length);

                        if (read > 0)
                        {
                            clientMsg = Encoding.UTF8.GetString(buffer, 0, read);
                            //string clientMsg = Encoding.Default.GetString(buffer, 0, read);

                            //remove newline
                            //clientMsg = System.Text.RegularExpressions.Regex.Replace(clientString, @"\r\n|\t|\n|\r|", "");

                            //LogMsg("clientMsg [" + msgIndex + "]: " + clientMsg);
                            sb.Append(clientMsg);

                            msgIndex += 1; //increment
                        }
                    } while (!stream.IsMessageComplete);

                    //raise event
                    OnMessageReadComplete();
                } while (read > 0);

                //raise event
                OnReadComplete();

                LogMsg("message completed");
            }

            return sb.ToString();
        }

        private void Shutdown()
        {
            LogMsg("Shutting down named pipe server");

            if (_pipeServer != null)
            {
                try { _pipeServer.Close(); } catch { }
                try { _pipeServer.Dispose(); } catch { }
                _pipeServer = null;
            }
        }

        public void StartServer(object obj = null)
        {
            LogMsg("Info: Starting named pipe server...");

            _pipeServer = CreateNamedPipeServerStream(PipeName);
            _pipeServer.BeginWaitForConnection(DoConnectionLoop, null);
        }

        public void StopServer()
        {
            Shutdown();
            OnServerShutdown(); //raise event
            LogMsg("Info: Server shutdown.");
        }
    }
}

接下来,我创建了一个“Helper”类,其中包含用于启动命名管道服务器、使用 Process 运行命令并返回数据的代码。获取数据的方式有以下三种。它由方法返回,可以订阅“DataReceived”事件,或者一旦方法完成,数据将在属性“Data”中。

助手.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.IO.Pipes;
using System.IO;
using System.Threading;

namespace ProcessTest
{
    public class Helper : IDisposable
    {

        public delegate void EventHandlerDataReceived(object sender, string data);

        //event that subscribers can subscribe to
        public event EventHandlerDataReceived DataReceived;

        private StringBuilder _sbData = new StringBuilder();

        private HelperNamedPipeServer _helperNamedPipeServer = null;

        private bool _namedPipeServerOperationComplete = false;

        public string Data { get; private set; } = string.Empty;

        public Helper()
        {

        }

        private void OnDataReceived(string data)
        {
            if (!String.IsNullOrEmpty(data) && DataReceived != null)
            {
                DataReceived(this, data);

                //Debug.Write("Data: " + data);
            }
        }

        public void Dispose()
        {
            ShutdownNamedPipeServer();
        }

        public async Task<string> RunSambaNamedPipesAsync(string fqExePath, string arguments, string pipeName = "sam-ba-pipe", string serverName = ".", bool runAsAdministrator = false)
        {
            string result = string.Empty;
            string tempArguments = arguments;

            //re-initialize
            _namedPipeServerOperationComplete = false;
            _sbData = new StringBuilder();
            Data = string.Empty;

            if (String.IsNullOrEmpty(fqExePath))
            {
                Debug.WriteLine("fqExePath not specified");
                return "fqExePath not specified";
            }

            //create new instance
            _helperNamedPipeServer = new HelperNamedPipeServer(pipeName);
            _helperNamedPipeServer.ShutdownWhenOperationComplete = true;

            //subscribe to events
            _helperNamedPipeServer.DataReceived += HelperNamedPipeServer_DataReceived;
            _helperNamedPipeServer.OperationCompleted += HelperNamedPipeServer_OperationCompleted;

            //start named pipe server on it's own thread
            Thread t = new Thread(_helperNamedPipeServer.StartServer);
            t.Start();

            //get pipe name to use with Process
            //this is where output from the process
            //will be redirected to
            string fqNamedPipe = string.Empty;

            if (String.IsNullOrEmpty(serverName))
            {
                fqNamedPipe = String.Format(@"\\{0}\pipe\{1}", serverName, pipeName);
            }
            else
            {
                fqNamedPipe = String.Format(@"\\{0}\pipe\{1}", ".", pipeName);
            }

            //redirect both StandardOutput and StandardError to named pipe
            if (!arguments.Contains("2>&1"))
            {
                tempArguments += String.Format(" {0} {1} {2}", @"1>", fqNamedPipe, @"2>&1");
            }

            //run Process
            RunProcess(fqExePath, tempArguments, runAsAdministrator);

            while (!_namedPipeServerOperationComplete)
            {
                await Task.Delay(125);
            }

            //set value
            Data = _sbData.ToString();

            return Data;

        }


        public void RunProcess(string fqExePath, string arguments,  bool runAsAdministrator = false)
        {

            if (String.IsNullOrEmpty(fqExePath))
            {
                Debug.WriteLine("fqExePath not specified");
                throw new Exception( "Error: fqExePath not specified");
            }

            //create new instance
            ProcessStartInfo startInfo = new ProcessStartInfo(fqExePath, arguments);

            if (runAsAdministrator)
            {
                startInfo.Verb = "runas"; //elevates permissions
            }//if

            //set environment variables
            //pStartInfo.EnvironmentVariables["SomeVar"] = "someValue";

            startInfo.RedirectStandardError = false;
            startInfo.RedirectStandardOutput = false;

            startInfo.RedirectStandardInput = false;

            startInfo.UseShellExecute = true; //use ShellExecute instead of CreateProcess

            startInfo.WindowStyle = ProcessWindowStyle.Hidden;
            startInfo.ErrorDialog = false;
            startInfo.WorkingDirectory = System.IO.Path.GetDirectoryName(fqExePath);

            using (Process p = Process.Start(startInfo))
            {
                //start
                p.Start();

                //waits until the process is finished before continuing
                p.WaitForExit();
            }
        }

        private void HelperNamedPipeServer_OperationCompleted(object sender, bool e)
        {
            //Debug.WriteLine("Info: Named pipe server - Operation completed.");

            //set value
            Data = _sbData.ToString();

            //set value
            _namedPipeServerOperationComplete = true;

        }

        private void HelperNamedPipeServer_DataReceived(object sender, string data)
        {
            Debug.WriteLine("Info: Data received from named pipe server.");

            if (!String.IsNullOrEmpty(data))
            {
                //append
                _sbData.Append(data.TrimEnd('\0'));

                //send data to subscribers
                OnDataReceived(data);
            }
        }

        private void ShutdownNamedPipeServer()
        {
            Debug.WriteLine("Info: ShutdownNamedPipeServer");
            try
            {
                if (_helperNamedPipeServer != null)
                {
                    //unsubscribe from events
                    _helperNamedPipeServer.DataReceived -= HelperNamedPipeServer_DataReceived;
                    _helperNamedPipeServer.OperationCompleted -= HelperNamedPipeServer_OperationCompleted;

                    _helperNamedPipeServer.Dispose();
                    _helperNamedPipeServer = null;
                }
            }
            catch (Exception ex)
            {
            }
        }
    }
}

用法

private async void btnRunUsingNamedPipes_Click(object sender, EventArgs e)
{
    //Button name: btnRunUsingNamedPipes

    using (Helper helper = new Helper())
    {
        //subscribe to event
        helper.DataReceived += Helper_DataReceived;

        var result = await helper.RunSambaNamedPipesAsync(@"C:\Windows\system32\cmd.exe", @"/c C:\Temp\sam-ba_3.5\sam-ba.exe -p serial:COM5 -d sama5d3 -m version");
        Debug.WriteLine("Result: " + result);

        //unsubscribe from event
        helper.DataReceived -= Helper_DataReceived;
    }
}

private void Helper_DataReceived(object sender, string data)
{
    //System.Diagnostics.Debug.WriteLine(data);

    //RichTextBox name: richTextBoxOutput

    if (richTextBoxOutput.InvokeRequired)
    {
        richTextBoxOutput.Invoke((MethodInvoker)delegate
        {
            richTextBoxOutput.Text = data;
            richTextBoxOutput.Refresh();
        });
    }
}

资源


推荐阅读