24

I've a problem in my project. I would like to launch a process, 7z.exe (console version). I've tried three different things:

  • Process.StandardOutput.ReadToEnd();
  • OutputDataReceived & BeginOutputReadLine
  • StreamWriter

Nothing works. It always "wait" for the end of the process to show what i want. I don't have any code to put, just if you want my code with one of the things listed upthere. Thanks.

Edit: My code:

        process.StartInfo.UseShellExecute = false;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.CreateNoWindow = true;
        process.Start();

        this.sr = process.StandardOutput;
        while (!sr.EndOfStream)
        {
            String s = sr.ReadLine();
            if (s != "")
            {
                System.Console.WriteLine(DateTime.Now + " - " + s);
            }
        }

Or

process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.OutputDataReceived += new DataReceivedEventHandler(recieve);
process.StartInfo.CreateNoWindow = true;
process.Start();
process.BeginOutputReadLine();
process.WaitForExit();
public void recieve(object e, DataReceivedEventArgs outLine)
{
    System.Console.WriteLine(DateTime.Now + " - " + outLine.Data);
}

Or

process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.Start();
string output = p.StandardOutput.ReadToEnd();
process.WaitForExit();

Where "process" is my pre-made Process

Ok i know why it doesn't works properly: 7z.exe is the bug: it display a percent loading in console, and it sends information only when the current file is finished. In extraction for example, it works fine :). I will search for another way to use 7z functions without 7z.exe (maybe with 7za.exe or with some DLL). Thanks to all. To answer to the question, OuputDataRecieved event works fine !

Extaze
  • 1,117
  • 2
  • 10
  • 18

7 Answers7

30

Take a look at this page, it looks this is the solution for you: http://msdn.microsoft.com/en-us/library/system.diagnostics.process.beginoutputreadline.aspx and http://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx

[Edit] This is a working example:

        Process p = new Process();
        p.StartInfo.RedirectStandardError = true;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.CreateNoWindow = true;
        p.StartInfo.FileName = @"C:\Program Files (x86)\gnuwin32\bin\ls.exe";
        p.StartInfo.Arguments = "-R C:\\";

        p.OutputDataReceived += new DataReceivedEventHandler((s, e) => 
        { 
            Console.WriteLine(e.Data); 
        });
        p.ErrorDataReceived += new DataReceivedEventHandler((s, e) =>
        {
            Console.WriteLine(e.Data);
        });

        p.Start();
        p.BeginOutputReadLine();
        p.BeginErrorReadLine();

Btw, ls -R C:\ lists all files from the root of C: recursively. These are a lot of files, and I'm sure it isn't done when the first results show up in the screen. There is a possibility 7zip holds the output before showing it. I'm not sure what params you give to the proces.

jjxtra
  • 20,415
  • 16
  • 100
  • 140
Michiel van Vaardegem
  • 2,260
  • 20
  • 35
  • All I can find, is you should use the event OutputDataReceived, at the moment I don't have time to test it, maybe later. (http://harbertc.wordpress.com/2006/05/16/reading-text-from-a-process-executed-programmatically-in-c/) – Michiel van Vaardegem Jan 11 '12 at 18:50
  • I tried everything, but the problem was 7z.exe. I've decided to take a 7z library for c# (SevenZipSharp) and the problem was solved. Thanks ;) – Extaze Jan 22 '12 at 10:19
  • 1
    I have the exact same problem with 7-zip and output stream redirection. If you just look at 7-zip's output, it's obvious that it's not performing standard console output; instead, it seems to be calling console functions that re-write what's on the display in order to show a progress percentage. The reason you don't get output until the end, is that it doesn't write to the standard console output until the very end, as a kind of final log. – Triynko Apr 19 '13 at 18:34
  • BeginOutputReadLine() works perfectly for 7z.exe while testing an archive – SepehrM May 15 '14 at 18:27
  • 2
    `p.BeginErrorReadLine();` is needed else error handler doesn't get called – Red Riding Hood Jul 27 '18 at 10:51
9

To correctly handle output and/or error redirection you must also redirect input. It seem to be feature/bug in runtime of the external application youre starting and from what I have seen so far, it is not mentioned anywhere else.

Example usage:

        Process p = new Process(...);

        p.StartInfo.UseShellExecute = false;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.RedirectStandardError = true;
        p.StartInfo.RedirectStandardInput = true; // Is a MUST!
        p.EnableRaisingEvents = true;

        p.OutputDataReceived += OutputDataReceived;
        p.ErrorDataReceived += ErrorDataReceived;

        Process.Start();

        p.BeginOutputReadLine();
        p.BeginErrorReadLine();

        p.WaitForExit();

        p.OutputDataReceived -= OutputDataReceived;
        p.ErrorDataReceived -= ErrorDataReceived;

...

    void OutputDataReceived(object sender, DataReceivedEventArgs e)
    {
        // Process line provided in e.Data
    }

    void ErrorDataReceived(object sender, DataReceivedEventArgs e)
    {
        // Process line provided in e.Data
    }
user3042599
  • 517
  • 4
  • 5
  • 3
    The redirecting of the standard input fixed the problem for me. – Daniel Abbatt Feb 01 '16 at 10:15
  • That is incredible; I had some python scripts converted to exe files, very simple printing never got caught until after the programs exited, UNTIL I specified RedirectStandardInput. I don't understand how no one else (including top answer upvoters) seemed to have this problem but we had this problem. – pete Feb 07 '22 at 07:50
  • Addendum, it's still very wonky. It often doesn't trigger the callback at all until a long time afterward or it errors out. Whereas if we open in a console we can always see the output immediately. All I want to do is have it be the same as if it were opened in a console. Does anyone know the solution – pete Feb 11 '22 at 17:36
7

I don't know if anyone is still looking for a solution to this, but it has come up several times for me because I'm writing a tool in Unity in support of some games and due to the limited interoperability of certain systems with mono (like PIA for reading text from Word, for example), I often have to write OS-specific (sometimes Windows, sometimes MacOS) executables and launch them from Process.Start().

The problem is, when you launch an executable like this it's going to fire up in another thread that blocks your main app, causing a hang. If you want to provide useful feedback to your users during this time beyond the spinning icons conjured up by your respective OS, then you're kind of screwed. Using a stream won't work because the thread is still blocked until execution finishes.

The solution I've hit on, which might seem extreme for some people but I find works quite well for me, is to use sockets and multithreading to set up reliable synchronous comms between the two apps. Of course, this only works if you are authoring both apps. If not, I think you are out of luck. ... I would like to see if it works with just multithreading using a traditional stream approach, so if someone would like to try that and post the results here that would be great.

Anyway, here's the solution currently working for me:

In the main, or calling app, I do something like this:

/// <summary>
/// Handles the OK button click.
/// </summary>
private void HandleOKButtonClick() {
string executableFolder = "";

#if UNITY_EDITOR
executableFolder = Path.Combine(Application.dataPath, "../../../../build/Include/Executables");
#else
executableFolder = Path.Combine(Application.dataPath, "Include/Executables");
#endif

EstablishSocketServer();

var proc = new Process {
    StartInfo = new ProcessStartInfo {
        FileName = Path.Combine(executableFolder, "WordConverter.exe"),
        Arguments = locationField.value + " " + _ipAddress.ToString() + " " + SOCKET_PORT.ToString(), 
        UseShellExecute = false,
        RedirectStandardOutput = true,
        CreateNoWindow = true
    }
};

proc.Start();

Here's where I establish the socket server:

/// <summary>
/// Establishes a socket server for communication with each chapter build script so we can get progress updates.
/// </summary>
private void EstablishSocketServer() {
    //_dialog.SetMessage("Establishing socket connection for updates. \n");
    TearDownSocketServer();

    Thread currentThread;

    _ipAddress = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0];
    _listener = new TcpListener(_ipAddress, SOCKET_PORT);
    _listener.Start();

    UnityEngine.Debug.Log("Server mounted, listening to port " + SOCKET_PORT);

    _builderCommThreads = new List<Thread>();

    for (int i = 0; i < 1; i++) {
        currentThread = new Thread(new ThreadStart(HandleIncomingSocketMessage));
        _builderCommThreads.Add(currentThread);
        currentThread.Start();
    }
}

/// <summary>
/// Tears down socket server.
/// </summary>
private void TearDownSocketServer() {
    _builderCommThreads = null;

    _ipAddress = null;
    _listener = null;
}

Here's my socket handler for the thread... note that you will have to create multiple threads in some cases; that's why I have that _builderCommThreads List in there (I ported it from code elsewhere where I was doing something similar but calling multiple instances in a row):

/// <summary>
/// Handles the incoming socket message.
/// </summary>
private void HandleIncomingSocketMessage() {
    if (_listener == null) return;

    while (true) {
        Socket soc = _listener.AcceptSocket();
        //soc.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 10000);
        NetworkStream s = null;
        StreamReader sr = null;
        StreamWriter sw = null;
        bool reading = true;

        if (soc == null) break;

        UnityEngine.Debug.Log("Connected: " + soc.RemoteEndPoint);

        try {
            s = new NetworkStream(soc);
            sr = new StreamReader(s, Encoding.Unicode);
            sw = new StreamWriter(s, Encoding.Unicode);
            sw.AutoFlush = true; // enable automatic flushing

            while (reading == true) {
                string line = sr.ReadLine();

                if (line != null) {
                    //UnityEngine.Debug.Log("SOCKET MESSAGE: " + line);
                    UnityEngine.Debug.Log(line);

                    lock (_threadLock) {
                        // Do stuff with your messages here
                    }
                }
            }

            //
        } catch (Exception e) {
            if (s != null) s.Close();
            if (soc != null) soc.Close();
            UnityEngine.Debug.Log(e.Message);
            //return;
        } finally {

        //
        if (s != null) s.Close();
        if (soc != null) soc.Close();

        UnityEngine.Debug.Log("Disconnected: " + soc.RemoteEndPoint);
        }
    }

    return;
}

Of course, you'll need to declare some stuff up at the top:

private TcpListener _listener = null;
private IPAddress _ipAddress = null;
private List<Thread> _builderCommThreads = null;
private System.Object _threadLock = new System.Object();

...then in the invoked executable, set up the other end (I used statics in this case, you can use what ever you want):

private static TcpClient _client = null;
private static Stream _s = null;
private static StreamReader _sr = null;
private static StreamWriter _sw = null;
private static string _ipAddress = "";
private static int _port = 0;
private static System.Object _threadLock = new System.Object();

/// <summary>
/// Main method.
/// </summary>
/// <param name="args"></param>
static void Main(string[] args) {
    try {
        if (args.Length == 3) {
            _ipAddress = args[1];
            _port = Convert.ToInt32(args[2]);

            EstablishSocketClient();
        }

        // Do stuff here

        if (args.Length == 3) Cleanup();
    } catch (Exception exception) {
        // Handle stuff here
        if (args.Length == 3) Cleanup();
    }
}

/// <summary>
/// Establishes the socket client.
/// </summary>
private static void EstablishSocketClient() {
    _client = new TcpClient(_ipAddress, _port);

    try {
        _s = _client.GetStream();
        _sr = new StreamReader(_s, Encoding.Unicode);
        _sw = new StreamWriter(_s, Encoding.Unicode);
        _sw.AutoFlush = true;
    } catch (Exception e) {
        Cleanup();
    }
}

/// <summary>
/// Clean up this instance.
/// </summary>
private static void Cleanup() {
    _s.Close();
    _client.Close();

    _client = null;
    _s = null;
    _sr = null;
    _sw = null;
}

/// <summary>
/// Logs a message for output.
/// </summary>
/// <param name="message"></param>
private static void Log(string message) {
    if (_sw != null) {
        _sw.WriteLine(message);
    } else {
        Console.Out.WriteLine(message);
    }
}

...I'm using this to launch a command line tool on Windows that uses the PIA stuff to pull text out of a Word doc. I tried PIA the .dlls in Unity, but ran into interop issues with mono. I'm also using it on MacOS to invoke shell scripts that launch additional Unity instances in batchmode and run editor scripts in those instances that talk back to the tool over this socket connection. It's great, because I can now send feedback to the user, debug, monitor and respond to specific steps in the process, et cetera, et cetera.

HTH

user2848240
  • 91
  • 1
  • 4
  • 1
    While not technically the answer to the question, this was closely related and extremely helpful. – PRMan May 22 '18 at 16:31
5

The problem is caused by calling the Process.WaitForExit method. What this does, according to the documentation, is:

Sets the period of time to wait for the associated process to exit, and blocks the current thread of execution until the time has elapsed or the process has exited. To avoid blocking the current thread, use the Exited event.

So, to prevent the thread blocking until the process has exited, wire up the Process.Exited event handler of the Process object, as below. The Exited event can occur only if the value of the EnableRaisingEvents property is true.

    process.EnableRaisingEvents = true;
    process.Exited += Proc_Exited;


    private void Proc_Exited(object sender, EventArgs e)
    {
        // Code to handle process exit
    }

Done this way, you will be able to get the output of your process, while it is still running, via the Process.OutputDataReceived event as you currently do. (PS - the code example on that event page also makes the mistake of using Process.WaitForExit.)

Another note is that you need to ensure your Process object is not cleaned up before your Exited method can fire. If your Process is initialized in a using statement, this might be a problem.

Bork Blatt
  • 3,308
  • 2
  • 19
  • 17
2

I have used the CmdProcessor class described here on several projects with much success. It looks a bit daunting at first but is very easy to use.

joebalt
  • 969
  • 1
  • 12
  • 24
1

Windows treats pipes and consoles differently. Pipes are buffered, consoles are not. RedirectStandardOutput connects a pipe. There are only two solutions.

  1. Change the console app to flush its buffer after every write
  2. Write a shim to fake a console per https://www.codeproject.com/Articles/16163/Real-Time-Console-Output-Redirection

Note that RTConsole does not handle STDERR which suffers the same problem.

Thanks to https://stackoverflow.com/users/4139809/jeremy-lakeman for sharing this information with me in relation to another question.

Peter Wone
  • 17,965
  • 12
  • 82
  • 134
  • This is THE one and only correct solution; solves a months-long mystery and I can't believe it had no upvotes yet. Everything was fixed after I changed my python program to write "flush=True" in every call to print(). – pete Mar 08 '22 at 00:01
0

Try this.

        Process notePad = new Process();

        notePad.StartInfo.FileName = "7z.exe";
        notePad.StartInfo.RedirectStandardOutput = true;
        notePad.StartInfo.UseShellExecute = false;

        notePad.Start();
        StreamReader s = notePad.StandardOutput;



        String output= s.ReadToEnd();


        notePad.WaitForExit();

Let the above be in a thread.

Now for updating the output to UI,you can use a timer with two lines

  Console.Clear();
  Console.WriteLine(output);

This may help you

sonu thomas
  • 2,161
  • 4
  • 24
  • 38