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();
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"];

        }
    }
}

 

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);
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();
}

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..

Comments (7)

  1. Jon Szabo says:

    Thanks for sharing this complete solution.  I was looking for an easy way to get the exit code on a remote wmi process, and now I have it. 🙂

  2. Roland Willis says:

    Superb. Thanks. Precisely what I was looking for.

  3. Sonny says:

    Pretty much the same issue I was running into with PsExec. I really need the stdout and stderr of the process though. Redirecting to files is an option I suppose, though not a very elegant one.

  4. Monica Chandnani says:

    This code works, but quite often, I don't get every event that stopped. Does naybody else see this?

  5. John Erickson says:

    @Monica There's a bug in this code. Win32_ProcessStopTrace needs to be started *BEFORE* the process is started.  Otherwise a short-lived process will start and stop before Win32_ProcessStopTrace tracing has begun.  This adds the additional complication that a process with the same PID could exit between when Win32_ProcessStopTrace tracing starts and the new process is invoked.  Should the new process have the same process ID (they are recycled), then one would get the exit code of the wrong process. There's no good way to do this via WMI.

  6. T says:

    The page layout cuts off the code on the right side in IE 11

  7. Zach Smith says:

    Thanks so much for posting this!  this has been a big help

Skip to main content