Trace-ProcessCreation: wrapping existing code with a cmdlet

I was spammingasking around earlier this week about how to monitor process creation in windows.  I was expecting to have to go the IFEO or hook-an-API route, but Wes Haggard pointed me to a class he wrote that uses WMI for monitor process creation/modification/deletion!

Since this involves hooking up for events and doing operations on background threads (as the delegates are called), this is the kind of thing you're much better off doing in a cmdlet than trying (and most likely failing) to pull it off in "only" powershell script.

Since Wes had already wrapped his effort into a class, all I had to do was write a cmdlet that use that class. 

Lessons I learned during the creation of this cmdlet:

  • At first I tried to WriteObject on the background thread that gets called for the events (procWatcher_ProcessCreated in this case).  No dice (silent failure, no exception, just didn't work).  Because of that, since "get it on the main thread" reminds me of WinForms with InvokeRequired and Invoke, I asked if there was AsyncOperation{,Manager} support, but Jachym Kouba nicely pointed out to just use a queue and normal locking to pass the data back to the main thread, and have ProcessRecord check the queue.
  • ProcessDeleted unfortunately doesn't seem to have the exit code, at least via the class that mgmtclassgen creates.  I could create a Process object for the process at creation time, hook Exited, and display the exit code that way, but since I was far more interested in the command line than exit code, I just hooked process creation for now.
  • When you have an "infinitely running" cmdlet, your loop should be checking the Stopping property, which the engine/host will be nice enough to set to true when the environment says you should stop (in my case, the user hits control-c)

I've also moved this new cmdlet and the other one (get-tfhistory) into the same zip file of PowerShell scripts so others can more easily play with them if they want (that solution also includes Win32_Process and Wes' ProcessWatcher class).

Here's some example output (excuse WLW for the formatting :)

C:\> trace-processcreation gvim.exe | select CommandLine,CreationDate

CommandLine CreationDate
----------- ------------
"C:\Program Files\Vim\vim70\gvim.exe" 2/20/2007 4:40:20 PM
"c:\program files\vim\vim70\gvim.exe" 2/20/2007 4:41:05 PM
"c:\program files\vim\vim70\gvim.exe" foo bar baz 2/20/2007 4:41:11 PM
C:\>

Here's the cmdlet code:

     [Cmdlet(VerbsDiagnostic.Trace, "ProcessCreation")]
    public class TraceProcessCreationCommand : PSCmdlet
    {
        private Queue<Win32_Process> m_eventRecords = new Queue<Win32_Process>();
        private object m_lock = new object();
        private ProcessWatcher m_procWatcher;

        private string m_processName;

        [Parameter(Mandatory = false, Position = 0, ValueFromPipeline = true)]
        public string ProcessName
        {
            get { return m_processName; }
            set { m_processName = value; }
        }

        protected override void BeginProcessing()
        {
            m_procWatcher = new ProcessWatcher(m_processName);

            m_procWatcher.ProcessCreated += new ProcessEventHandler(procWatcher_ProcessCreated);
            //m_procWatcher.ProcessDeleted += new ProcessEventHandler(procWatcher_ProcessDeleted);
            //m_procWatcher.ProcessModified += new ProcessEventHandler(procWatcher_ProcessModified);

            m_procWatcher.Start();
        }

        protected override void ProcessRecord()
        {
            while (!Stopping)
            {
                // any new objects to write?
                if (m_eventRecords.Count > 0)
                {
                    Win32_Process record;
                    lock (m_lock)
                    {
                        record = m_eventRecords.Dequeue();
                    }
                    WriteObject(record);
                }
                // Sleep for a bit so we're not too CPU-hungry
                System.Threading.Thread.Sleep(100);
            }
        }

        protected override void EndProcessing()
        {
            m_procWatcher.Stop();
        }

        private void procWatcher_ProcessCreated(Win32_Process proc)
        {
            lock (m_lock)
            {
                m_eventRecords.Enqueue(proc);
            }
        }
    }