在C# / .NET进程中重定向标准错误时缺少输出

rqdpfwrv  于 2022-12-01  发布在  .NET
关注(0)|答案(1)|浏览(141)

我正在使用第三方的命令行工具“sam-ba”v3.5(here免费提供)。它是一个C++ / QML命令行工具,与硬件模块接口以读取/写入数据。在大多数情况下,命令的输出被发送到Standard Error。
我有一个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'

在这个简单的例子中,只有一个输出行被重定向到标准错误,而另一个没有。我尝试了许多不同的命令,结果是混合的。有时我得到所有的东西,有时部分输出,有时没有输出。
下面是一个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工具输出文本的方式有些不一样,格式,内容,......有些不一致
1.当执行外部应用程序时,由C# Process对象创建的
环境
有一些不同,这在直接运行外部应用程序时不会发生。否则,为什么python脚本在直接运行时会获得所有输出,而在通过C# Process对象运行时却不会?
第二个问题让我来到这里。我在寻找如何诊断这个问题的任何见解。我做错了什么,我可以在Process对象中尝试的设置,关于数据如何进入流而不被重定向出来的想法,或者是否有人以前见过这样的事情以及他们是如何解决的。

更新

拿到了sam-ba工具的源代码。C#应用程序没有捕获的输出来自QML文件。它们使用的是这个“print()'方法,我无法真正找到任何细节。C#应用程序可以捕获的输出通过信号传递回C++端,然后发送到标准错误。这反馈到我的结论#1中,它们的代码中存在不一致。
尽管如此,这可能意味着C#和QT/QML之间存在冲突,这可以解释为什么Python脚本获得QML输出,而C#应用程序没有。

rm5edbpk

rm5edbpk1#

下列程式在执行行程序时使用ShellExecute而非CreateProcess。使用ShellExecute时,无法重新导向行程序的StandardOutput和/或StandardError。若要解决这个问题,StandardOutput和StandardError都会重新导向至缓存档,然后从缓存档读取数据--这似乎会产生与从cmd windows 执行时相同的输出。

注意:在下列程式码中,必须使用%windir%\system32\cmd.exe(例如:C:\Windows\system32\cmd.exe)。请参阅下面的用法部分。
添加using语句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是进程“Argument”属性的值。
更新
选项2

下面是一个使用命名管道的解决方案。进程用于将输出重定向到命名管道而不是文件。一个进程创建一个命名管道“服务器”,侦听来自客户端的连接。然后使用System.Diagnostics.Process运行所需的命令,并将输出重定向到命名管道服务器。“服务器”读取输出,然后引发事件“DataReceived,”该事件将把数据返回给任何订阅者。
命名管道服务器代码来自here,但我对其进行了修改。我添加了许多事件--可以订阅这些事件。我还添加了服务器在读取完数据后自行关闭的功能,方法是将“ShutdownWhenOperationComplete”设置为“true”。
创建一个名为的类:HelperNamedPipeServer.cs

帮助程序命名管道服务器.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();
        });
    }
}

资源

相关问题