Search code examples
.netwinapipinvoke

Fast way to get process owners in .NET


I have the following .NET code that reaches out to the OpenProcessToken Win32 API to retrieve the owner names of all processes on the system:

using System.Security.Principal;
using System.Runtime.InteropServices;

public class Test
{
    [DllImport("advapi32.dll", SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);

    [DllImport("kernel32.dll", SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CloseHandle(IntPtr hObject);
    
    private const UInt32 TOKEN_QUERY = 0x0008;

    public static List<List<String>> GetProcessWithUsers() 
    {
        var processes = Process.GetProcesses();
        var result = new List<List<string>>();
        foreach (var proc in processes)
        {
            result.Add(new List<string> { proc.ProcessName, GetProcessUser(proc) });
        }
        return result;
    }

    public static string GetProcessUser(Process process)
    {
        IntPtr tokenHandle = IntPtr.Zero;
        try
        {
            OpenProcessToken(process.Handle, TOKEN_QUERY, out tokenHandle);
            WindowsIdentity wi = new WindowsIdentity(tokenHandle);
            return wi.Name;
        }
        catch
        {
            return null;
        }
        finally
        {
            if (tokenHandle != IntPtr.Zero)
            {
                CloseHandle(tokenHandle);
            }
        }
    }
}

Calling Test.GetProcessWithUsers() (e.g. in LinqPad) takes almost 2 seconds for the 280 processes on my system.

I don't consider this an acceptable amount of time for this task.

Process.GetProcesses() is snappy, new WindowsIdentity()'s contribution is negligible, so what's the hold-up with OpenProcessToken()? Are there alternative Win32 API functions that would be faster?


Solution

  • Most of the time used seems to be caused by .NET's Process class (Access Denied exception legitimately thrown, etc.), so here is a full P/Invoke version that doesn't use it but uses the native CreateToolhelp32Snapshot function:

    [DllImport("advapi32", SetLastError = true)]
    private static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, out IntPtr TokenHandle);
    
    [DllImport("kernel32", SetLastError = true)]
    private static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
    
    [DllImport("kernel32", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr hObject);
    
    [DllImport("kernel32", SetLastError = true)]
    private static extern IntPtr CreateToolhelp32Snapshot(int dwFlags, int th32ProcessID);
    
    [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
    
    [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
    
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    private struct PROCESSENTRY32
    {
        public int dwSize;
        public int cntUsage;
        public int th32ProcessID;
        public IntPtr th32DefaultHeapID;
        public int th32ModuleID;
        public int cntThreads;
        public int th32ParentProcessID;
        public int pcPriClassBase;
        public int dwFlags;
    
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string szExeFile;
    }
    
    public static List<List<string>> GetProcessWithUsers()
    {
        var result = new List<List<string>>();
        const int TH32CS_SNAPPROCESS = 2;
        var snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        var entry = new PROCESSENTRY32();
        entry.dwSize = Marshal.SizeOf<PROCESSENTRY32>();
        if (Process32First(snap, ref entry))
        {
            do
            {
                const int PROCESS_QUERY_LIMITED_INFORMATION = 0x00001000;
                var handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, entry.th32ProcessID);
                result.Add(new List<string> { entry.szExeFile, GetProcessUser(handle) });
                if (handle != IntPtr.Zero)
                {
                    CloseHandle(handle);
                }
            }
            while (Process32Next(snap, ref entry));
        }
        CloseHandle(snap);
        return result;
    }
    
    public static string GetProcessUser(IntPtr handle)
    {
        if (handle == IntPtr.Zero)
            return null;
    
        const int TOKEN_QUERY = 0x0008;
        if (!OpenProcessToken(handle, TOKEN_QUERY, out var tokenHandle))
            return null;
    
        var wi = new WindowsIdentity(tokenHandle);
        CloseHandle(tokenHandle);
        return wi.Name;
    }
    

    On my PC, I've been down from 1500 ms to 30 ms (x50).