Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get Live output from Process

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 !

like image 951
Extaze Avatar asked Jan 10 '12 18:01

Extaze


4 Answers

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.

like image 63
Michiel van Vaardegem Avatar answered Nov 06 '22 21:11

Michiel van Vaardegem


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
    }
like image 41
user3042599 Avatar answered Nov 06 '22 20:11

user3042599


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

like image 7
user2848240 Avatar answered Nov 06 '22 21:11

user2848240


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.

like image 5
Bork Blatt Avatar answered Nov 06 '22 22:11

Bork Blatt