0

I created a generic class to execute a console application while redirecting its output to a RichTextBox in my form.

The code works just fine, but the Process.Exited event never fires even though the console application exists normally after it completes it function.

Also, WaitForExit doesn't seem to do anything and any code I write after it is never executed for some reason.

Here's the class (updated):

using System;
using System.Diagnostics;

public class ConsoleProcess
{
    private string fpath;
    public ConsoleProcess(string filePath)
    {
        fpath = filePath;
    }

    public void Run(string arguments)
    {
        var procInfo = new ProcessStartInfo()
        {
            FileName = fpath,
            Arguments = arguments,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden
        };

        var proc = new Process() { EnableRaisingEvents = true, StartInfo = procInfo };

        proc.OutputDataReceived += Proc_DataReceived;
        proc.ErrorDataReceived += Proc_DataReceived;
        proc.Exited += Proc_Exited;

        proc.Start();
        proc.BeginOutputReadLine();
        proc.BeginErrorReadLine();
    }

    public event EventHandler<EventArgs>? ProcessExited;
    private void Proc_Exited(object? sender, EventArgs e)
    {
        ProcessExited?.Invoke(sender, e);
    }

    public event EventHandler<DataReceivedEventArgs>? DataReceived;
    private void Proc_DataReceived(object sender, DataReceivedEventArgs e)
    {
        DataReceived?.Invoke(sender, e);
    }
}

Here's the code I use in the Form:

        ConsoleProcess process = new("console_app.exe");
        process.DataReceived += Process_DataReceived;
        process.ProcessExited += Process_Exited;

        private async void Execute()
        {
            await Task.Run(() =>
            {
                process.Run("--arguments");
            });
        }

        private void Process_DataReceived(object? sender, System.Diagnostics.DataReceivedEventArgs e)
        {
            //do stuff
        }

        private void Process_Exited(object? sender, EventArgs e)
        {
            MessageBox.Show("Done");
        }

PS: I'm aware that there are several posts about this issue, and I've checked them, but none of them helped, so here I am.

root
  • 143
  • 17
  • 2
    `ConsoleProcess : Process` <-- _Yikes!_ **You should not be subclassing `Process`!** – Dai Apr 12 '22 at 07:45
  • Have you used your debugger? If not, **why not**? – Dai Apr 12 '22 at 07:47
  • 1
    Because your `class ConsoleProcess` inherits from `Process` it has its own `Exited` event, which is a separate instance from your `Process` instance created in your `Run` method. – Dai Apr 12 '22 at 07:49
  • ...it sounds like you're new to the whole "composition vs. inheritance" _thing_. The long-and-short of it (thesedays) is generally that _inheritance is not the answer_, *especially* w.r.t. inheriting from (subclassing) standard library types. (To drive this point home even further: Microsoft recently made almost all classes in .NET `sealed` which prevents subclassing - though this is intended for performance reasons rather than stopping people from abusing OOP inheritnace). – Dai Apr 12 '22 at 07:51
  • 1
    The proc.Exited event is not subscribed. Get ahead by using `this` instead of `proc`. – Hans Passant Apr 12 '22 at 09:01

1 Answers1

1

To solve the issue that you're facing, try adding proc.WaitForExit();.

Code:

    ...
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();

//added - wait for exit
proc.WaitForExit(); 
    ...

If you want to use Task, then try the following:

Note: I renamed class ConsoleProcess to HelperProcess as the original name seems somewhat deceptive.

Create a new project - Windows Forms App

  • Name: ProcessTest
  • Target Framework: .NET 5

Create a class (name: HelperProcess.cs)

HelperProcess:

using System;
using System.Threading.Tasks;
using System.Diagnostics;

namespace ProcessTest
{
    public class HelperProcess
    {
        public event EventHandler<EventArgs> ProcessExited;
        public event EventHandler<DataReceivedEventArgs> DataReceived;

        private string fPath;

        public HelperProcess(string filePath)
        {
            fPath = filePath;
        }

        public async Task Run(string arguments)
        {
            Debug.WriteLine($"fPath: {fPath}");

            var procInfo = new ProcessStartInfo()
            {
                FileName = fPath,
                Arguments = arguments,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true,
                WindowStyle = ProcessWindowStyle.Hidden
            };

            using (Process proc = new Process() { EnableRaisingEvents = true, StartInfo = procInfo })
            {
                //subscribe to events
                proc.OutputDataReceived += Proc_DataReceived;
                proc.ErrorDataReceived += Proc_DataReceived;
                proc.Exited += Proc_Exited;

                //start
                proc.Start();

                //start reading
                proc.BeginOutputReadLine();
                proc.BeginErrorReadLine();

                //Debug.WriteLine("before proc.WaitForExitAsync");

                //wait for exit
                await proc.WaitForExitAsync();

                //Debug.WriteLine("after proc.WaitForExitAsync");

                //unsubscribe from events
                proc.OutputDataReceived -= Proc_DataReceived;
                proc.ErrorDataReceived -= Proc_DataReceived;
                proc.Exited -= Proc_Exited;
            }
        }

        private void Proc_Exited(object sender, EventArgs e)
        {
            //Debug.WriteLine("Proc_Exited");
            ProcessExited?.Invoke(sender, e);
        }

        private void Proc_DataReceived(object sender, DataReceivedEventArgs e)
        {
            //ToDo: add desired code

            //Debug.WriteLine($"Proc_DataReceived: {e.Data}");

            //if (DataReceived != null && !String.IsNullOrEmpty(e.Data))
            //{
            //    DataReceived.Invoke(sender, e);
            //}

            DataReceived?.Invoke(sender, e);
        }
    }
}

Add button to Form1 (name: btnRun)

  • Double-click button to add Click event handler

Open Properties Window

  • In VS menu, click View
  • Select Properties Window

Add FormClosed event handler to Form1

  • In Properties Window, select Form1 from the drop-down
  • Click enter image description here
  • Double-click FormClosed to add the event handler to Form1

Form1:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;

namespace ProcessTest
{
    public partial class Form1 : Form
    {
        private HelperProcess _process = null;
        public Form1()
        {
            InitializeComponent();

            string fPath = string.Empty;

            //environment variable windir has the same value as SystemRoot
            //use 'Sysnative' to access 64-bit files (in System32) if program is running as 32-bit process
            //use 'SysWow64' to access 32-bit files on 64-bit OS

            if (Environment.Is64BitOperatingSystem && !Environment.Is64BitProcess)
                fPath = System.IO.Path.Combine(Environment.GetEnvironmentVariable("windir"), "Sysnative", "wbem", "wmic.exe");
            else
                fPath = System.IO.Path.Combine(Environment.GetEnvironmentVariable("windir"), "System32", "wbem", "wmic.exe");

            //create new instance
            _process = new HelperProcess(fPath);

            //subscribe to events
            _process.DataReceived += Process_DataReceived;
            _process.ProcessExited += Process_ProcessExited;
        }

        private async void btnRun_Click(object sender, EventArgs e)
        {
            Debug.WriteLine("before Execute");
            await Execute();
            Debug.WriteLine("after Execute");
        }

        private async Task Execute()
        {
            await _process.Run("process get");

            Debug.WriteLine("after _process.Run");
        }

        private void Process_ProcessExited(object sender, EventArgs e)
        {
            Debug.WriteLine("Main: Process_ProcessExited");
        }

        private void Process_DataReceived(object sender, DataReceivedEventArgs e)
        {
            Debug.WriteLine($"Main: Process_DataReceived: {e.Data}");
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (_process != null)
            {
                //unsubscribe from events
                _process.DataReceived -= Process_DataReceived;
                _process.ProcessExited -= Process_ProcessExited;
            }
        }
    }
}

Resources:

Tu deschizi eu inchid
  • 4,117
  • 3
  • 13
  • 24
  • Even though the console app had finished and exited from memory, the `Process.Exited` still didn't fire. But, when I was closing my WinForms app or started another `Run` only then did the event fire. Any idea what could be causing this odd behvaiour? – root Apr 13 '22 at 08:02
  • @root: In the code I posted above I've shown how one can.use `WriteLine` to debug one's code. If one uses`Task`, then one needs to pay attention to where one uses `void` to ensure that the order of execution is as expected. – Tu deschizi eu inchid Apr 13 '22 at 13:27