Execute a process on remote machine, wait for it to exit and retrieve its exit code using WMI

This week I spent quite sometime reading through articles and trying to understand how to run a process on a remote machine and also get the exit code of the process once it terminates. I ran into this issue after having used PSExec to do the same. While PSExec does a great job of executing a process, getting its StdOut and the exit code, while using it with .Net, I came across a bunch of issues. Ultimately I found a neat way to do it using nothing but WMI calls.

The solution has multiple parts as follows,

1. Start the remote process

2. Find if the remote process is running and if it does, start an event monitor to wait for it to exit

3. Once the process exits, retrieve its exit code

Start the process using WMI

 ConnectionOptions connOptions = new ConnectionOptions();<br>connOptions.Impersonation = ImpersonationLevel.Impersonate;<br>connOptions.EnablePrivileges = true;<br>ManagementScope manScope = new ManagementScope(String.Format(@"\\{0}\ROOT\CIMV2", remoteComputerName), connOptions);<br><br>try<br>{<br>    manScope.Connect();<br>}<br>catch (Exception e)<br>{<br>    throw new Exception("Management Connect to remote machine " + remoteComputerName + " as user " + strUserName + " failed with the following error " + e.Message);<br>}<br>ObjectGetOptions objectGetOptions = new ObjectGetOptions();<br>ManagementPath managementPath = new ManagementPath("Win32_Process");<br>using (ManagementClass processClass = new ManagementClass(manScope, managementPath, objectGetOptions))<br>{<br>    using (ManagementBaseObject inParams = processClass.GetMethodParameters("Create"))<br>    {<br>        inParams["CommandLine"] = arguments;<br>        using (ManagementBaseObject outParams = processClass.InvokeMethod("Create", inParams, null))<br>        {<br><br>            if ((uint)outParams["returnValue"] != 0)<br>            {<br>                throw new Exception("Error while starting process " + arguments + " creation returned an exit code of " + outParams["returnValue"] + ". It was launched as " + strUserName + " on " + remoteComputerName);<br>            }<br>            this.ProcessId = (uint)outParams["processId"];<br><br>        }<br>    }<br>}<br>

 

The above code snippet launches the process on a remote machine using the “Create” method in “Win32_Process” class. The create method returns a value that indicates if the process was launched successfully or not. If the process was launched successfully, it also returns the process id.

Start an event handler to wait for the process to exit

 ManualResetEvent mre = new ManualResetEvent(false);<br>WqlEventQuery q = new WqlEventQuery("Win32_ProcessStopTrace");<br>using (ManagementEventWatcher w = new ManagementEventWatcher(manScope, q))<br>{<br>    w.EventArrived += new EventArrivedEventHandler(this.ProcessStoptEventArrived);<br>    w.Start();<br>    if (!mre.WaitOne(WaitTimePerCommand,false))<br>    {<br>        w.Stop();<br>        this.EventArrived = false;<br>    }<br>    else<br>        w.Stop();<br>}<br>

The above snippet connects to the “Win32_ProcessStopTrace” event and sets a callback to be called when any process exits on the machine. Once the callback is set, we can wait in a loop until we receive the notification for our process or use the ManualResetEvent provided in the System.Threading namespace to indicate that our process has exited. I also have a timeout set in the Event so that if the process takes longer than expected, we can kill it.

The callback method is a simple function in which you can check if the exited process is the process that we are waiting for using the process id as below,

 public void ProcessStoptEventArrived(object sender, EventArrivedEventArgs e)
{
    if ((uint)e.NewEvent.Properties["ProcessId"].Value == ProcessId)
    {
        Console.WriteLine("Process: {0}, Stopped with Code: {1}", (int)(uint)e.NewEvent.Properties["ProcessId"].Value, (int)(uint)e.NewEvent.Properties["ExitStatus"].Value);
        ExitCode = (int)(uint)e.NewEvent.Properties["ExitStatus"].Value;
        mre.Set();
    }
}

 

The event also gives us the exit code of the exiting process!

I have provided a complete class that can be used to launch any process on a remote box below.

 public class ProcessWMI
{
    public uint ProcessId;
    public int ExitCode;
    public bool EventArrived;
    public ManualResetEvent mre = new ManualResetEvent(false);
    public void ProcessStoptEventArrived(object sender, EventArrivedEventArgs e)
    {
        if ((uint)e.NewEvent.Properties["ProcessId"].Value == ProcessId)
        {
            Console.WriteLine("Process: {0}, Stopped with Code: {1}", (int)(uint)e.NewEvent.Properties["ProcessId"].Value, (int)(uint)e.NewEvent.Properties["ExitStatus"].Value);
            ExitCode = (int)(uint)e.NewEvent.Properties["ExitStatus"].Value;
            EventArrived = true;
            mre.Set();
        }
    }
    public ProcessWMI()
    {
        this.ProcessId = 0;
        ExitCode = -1;
        EventArrived = false;
    }
    public void ExecuteRemoteProcessWMI(string remoteComputerName, string arguments, int WaitTimePerCommand)
    {
        string strUserName = string.Empty;
        try
        {
            ConnectionOptions connOptions = new ConnectionOptions();
            connOptions.Impersonation = ImpersonationLevel.Impersonate;
            connOptions.EnablePrivileges = true;
            ManagementScope manScope = new ManagementScope(String.Format(@"\\{0}\ROOT\CIMV2", remoteComputerName), connOptions);

            try
            {
                manScope.Connect();
            }
            catch (Exception e)
            {
                throw new Exception("Management Connect to remote machine " + remoteComputerName + " as user " + strUserName + " failed with the following error " + e.Message);
            }
            ObjectGetOptions objectGetOptions = new ObjectGetOptions();
            ManagementPath managementPath = new ManagementPath("Win32_Process");
            using (ManagementClass processClass = new ManagementClass(manScope, managementPath, objectGetOptions))
            {
                using (ManagementBaseObject inParams = processClass.GetMethodParameters("Create"))
                {
                    inParams["CommandLine"] = arguments;
                    using (ManagementBaseObject outParams = processClass.InvokeMethod("Create", inParams, null))
                    {

                        if ((uint)outParams["returnValue"] != 0)
                        {
                            throw new Exception("Error while starting process " + arguments + " creation returned an exit code of " + outParams["returnValue"] + ". It was launched as " + strUserName + " on " + remoteComputerName);
                        }
                        this.ProcessId = (uint)outParams["processId"];
                    }
                }
            }

            SelectQuery CheckProcess = new SelectQuery("Select * from Win32_Process Where ProcessId = " + ProcessId);
            using (ManagementObjectSearcher ProcessSearcher = new ManagementObjectSearcher(manScope, CheckProcess))
            {
                using (ManagementObjectCollection MoC = ProcessSearcher.Get())
                {
                    if (MoC.Count == 0)
                    {
                        throw new Exception("ERROR AS WARNING: Process " + arguments + " terminated before it could be tracked on " + remoteComputerName);
                    }
                }
            }

            WqlEventQuery q = new WqlEventQuery("Win32_ProcessStopTrace");
            using (ManagementEventWatcher w = new ManagementEventWatcher(manScope, q))
            {
                w.EventArrived += new EventArrivedEventHandler(this.ProcessStoptEventArrived);
                w.Start();
                if (!mre.WaitOne(WaitTimePerCommand,false))
                {
                    w.Stop();
                    this.EventArrived = false;
                }
                else
                    w.Stop();
            }
            if (!this.EventArrived)
            {
                SelectQuery sq = new SelectQuery("Select * from Win32_Process Where ProcessId = " + ProcessId);
                using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(manScope, sq))
                {
                    foreach (ManagementObject queryObj in searcher.Get())
                    {
                        queryObj.InvokeMethod("Terminate", null);
                        queryObj.Dispose();
                        throw new Exception("Process " + arguments + " timed out and was killed on " + remoteComputerName);
                    }
                }
            }
            else
            {
                if (this.ExitCode != 0)
                    throw new Exception("Process " + arguments + "exited with exit code " + this.ExitCode + " on " + remoteComputerName + " run as " + strUserName);
                else
                    Console.WriteLine("process exited with Exit code 0");
            }
            
        }
        catch (Exception e)
        {
            throw new Exception(string.Format("Execute process failed Machinename {0}, ProcessName {1}, RunAs {2}, Error is {3}, Stack trace {4}", remoteComputerName, arguments, strUserName, e.Message, e.StackTrace), e);
        }
    }
}

 

This can be used in a program with two lines of code.

 ProcessWMI p = new ProcessWMI();
p.ExecuteRemoteProcessWMI(remoteMachine, sBatFile, timeout);

One thing that I am yet to do is to try and capture the StdOut and StdErr of the remote process..