Can I get command line arguments of other processes from .NET/C#?

C#.Net

C# Problem Overview


I have a project where I have multiple instances of an app running, each of which was started with different command line arguments. I'd like to have a way to click a button from one of those instances which then shuts down all of the instances and starts them back up again with the same command line arguments.

I can get the processes themselves easily enough through Process.GetProcessesByName(), but whenever I do, the StartInfo.Arguments property is always an empty string. It looks like maybe that property is only valid before starting a process.

This question had some suggestions, but they're all in native code, and I'd like to do this directly from .NET. Any suggestions?

C# Solutions


Solution 1 - C#

This is using all managed objects, but it does dip down into the WMI realm:

private static void Main()
{
    foreach (var process in Process.GetProcesses())
    {
        try
        {
            Console.WriteLine(process.GetCommandLine());
        }
        catch (Win32Exception ex) when ((uint)ex.ErrorCode == 0x80004005)
        {
            // Intentionally empty - no security access to the process.
        }
        catch (InvalidOperationException)
        {
            // Intentionally empty - the process exited before getting details.
        }

    }
}

private static string GetCommandLine(this Process process)
{
    using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id))
    using (ManagementObjectCollection objects = searcher.Get())
    {
        return objects.Cast<ManagementBaseObject>().SingleOrDefault()?["CommandLine"]?.ToString();
    }

}

Solution 2 - C#

If you don't want to use WMI and rather have a native way of doing this, I wrote a DLL that utilizes NTDLL.DLL's NtQueryInformationProcess() export and derives the command line from the information returned.

The DLL was written in C++ and has no dependencies so it will work on any Windows system.

To use it, just add these imports:

[DllImport("ProcCmdLine32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetProcCmdLineW")]
public extern static int GetProcCmdLine32W(uint nProcId, StringBuilder sb, uint dwSizeBuf);

[DllImport("ProcCmdLine64.dll", CharSet = CharSet.Unicode, EntryPoint = "GetProcCmdLineW")]
public extern static int GetProcCmdLine64W(uint nProcId, StringBuilder sb, uint dwSizeBuf);

Then call it as so:

public static string GetCommandLineOfProcessW(Process proc)
{
	var sb = new StringBuilder(capacity: 0xFFFF);
	var rc = -1;
	switch (IntPtr.Size)
	{
		case 4:
			rc = Win32Native.GetProcCmdLine32W((uint)proc.Id, sb, (uint)sb.Capacity);
			break;
		case 8:
			rc = Win32Native.GetProcCmdLine64W((uint)proc.Id, sb, (uint)sb.Capacity);
			break;
	}
	return (0 == rc) ? sb.ToString() : throw new Win32Exception(rc, ErrorToString(rc));
}

All the source code for the DLL with an example .NET console application is available in this repo.

If you just want the pre-compiled DLLs with some sample code, you can download a zip package from here.

Edited To Add:

I have converted the C++ code to C#. Now you don't need the ProcCmdLine.DLL, you can simply just add this class to your code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;

public static class ProcessCommandLine
{
	private static class Win32Native
	{
		public const uint PROCESS_BASIC_INFORMATION = 0;

		[Flags]
		public enum OpenProcessDesiredAccessFlags : uint
		{
			PROCESS_VM_READ = 0x0010,
			PROCESS_QUERY_INFORMATION = 0x0400,
		}

		[StructLayout(LayoutKind.Sequential)]
		public struct ProcessBasicInformation
		{
			public IntPtr Reserved1;
			public IntPtr PebBaseAddress;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
			public IntPtr[] Reserved2;
			public IntPtr UniqueProcessId;
			public IntPtr Reserved3;
		}

		[StructLayout(LayoutKind.Sequential)]
		public struct UnicodeString
		{
			public ushort Length;
			public ushort MaximumLength;
			public IntPtr Buffer;
		}

		// This is not the real struct!
		// I faked it to get ProcessParameters address.
		// Actual struct definition:
		// https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb
		[StructLayout(LayoutKind.Sequential)]
		public struct PEB
		{
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
			public IntPtr[] Reserved;
			public IntPtr ProcessParameters;
		}

		[StructLayout(LayoutKind.Sequential)]
		public struct RtlUserProcessParameters
		{
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
			public byte[] Reserved1;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
			public IntPtr[] Reserved2;
			public UnicodeString ImagePathName;
			public UnicodeString CommandLine;
		}

		[DllImport("ntdll.dll")]
		public static extern uint NtQueryInformationProcess(
			IntPtr ProcessHandle,
			uint ProcessInformationClass,
			IntPtr ProcessInformation,
			uint ProcessInformationLength,
			out uint ReturnLength);

		[DllImport("kernel32.dll")]
		public static extern IntPtr OpenProcess(
			OpenProcessDesiredAccessFlags dwDesiredAccess,
			[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
			uint dwProcessId);

		[DllImport("kernel32.dll")]
		[return: MarshalAs(UnmanagedType.Bool)]
		public static extern bool ReadProcessMemory(
			IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer,
			uint nSize, out uint lpNumberOfBytesRead);

		[DllImport("kernel32.dll")]
		[return: MarshalAs(UnmanagedType.Bool)]
		public static extern bool CloseHandle(IntPtr hObject);

		[DllImport("shell32.dll", SetLastError = true,
			CharSet = CharSet.Unicode, EntryPoint = "CommandLineToArgvW")]
		public static extern IntPtr CommandLineToArgv(string lpCmdLine, out int pNumArgs);
	}

	private static bool ReadStructFromProcessMemory<TStruct>(
		IntPtr hProcess, IntPtr lpBaseAddress, out TStruct val)
	{
		val = default;
		var structSize = Marshal.SizeOf<TStruct>();
		var mem = Marshal.AllocHGlobal(structSize);
		try
		{
			if (Win32Native.ReadProcessMemory(
				hProcess, lpBaseAddress, mem, (uint)structSize, out var len) &&
				(len == structSize))
			{
				val = Marshal.PtrToStructure<TStruct>(mem);
				return true;
			}
		}
		finally
		{
			Marshal.FreeHGlobal(mem);
		}
		return false;
	}

	public static string ErrorToString(int error) =>
		new string[]
		{
			"Success",
			"Failed to open process for reading",
			"Failed to query process information",
			"PEB address was null",
			"Failed to read PEB information",
			"Failed to read process parameters",
			"Failed to read command line from process"
		}[Math.Abs(error)];

	public static int Retrieve(Process process, out string commandLine)
	{
		int rc = 0;
		commandLine = null;
		var hProcess = Win32Native.OpenProcess(
			Win32Native.OpenProcessDesiredAccessFlags.PROCESS_QUERY_INFORMATION |
			Win32Native.OpenProcessDesiredAccessFlags.PROCESS_VM_READ, false, (uint)process.Id);
		if (hProcess != IntPtr.Zero)
		{
			try
			{
				var sizePBI = Marshal.SizeOf<Win32Native.ProcessBasicInformation>();
				var memPBI = Marshal.AllocHGlobal(sizePBI);
				try
				{
					var ret = Win32Native.NtQueryInformationProcess(
						hProcess, Win32Native.PROCESS_BASIC_INFORMATION, memPBI,
						(uint)sizePBI, out var len);
					if (0 == ret)
					{
						var pbiInfo = Marshal.PtrToStructure<Win32Native.ProcessBasicInformation>(memPBI);
						if (pbiInfo.PebBaseAddress != IntPtr.Zero)
						{
							if (ReadStructFromProcessMemory<Win32Native.PEB>(hProcess,
								pbiInfo.PebBaseAddress, out var pebInfo))
							{
								if (ReadStructFromProcessMemory<Win32Native.RtlUserProcessParameters>(
									hProcess, pebInfo.ProcessParameters, out var ruppInfo))
								{
									var clLen = ruppInfo.CommandLine.MaximumLength;
									var memCL = Marshal.AllocHGlobal(clLen);
									try
									{
										if (Win32Native.ReadProcessMemory(hProcess,
											ruppInfo.CommandLine.Buffer, memCL, clLen, out len))
										{
											commandLine = Marshal.PtrToStringUni(memCL);
											rc = 0;
										}
										else
										{
											// couldn't read command line buffer
											rc = -6;
										}
									}
									finally
									{
										Marshal.FreeHGlobal(memCL);
									}
								}
								else
								{
									// couldn't read ProcessParameters
									rc = -5;
								}
							}
							else
							{
								// couldn't read PEB information
								rc = -4;
							}
						}
						else
						{
							// PebBaseAddress is null
							rc = -3;
						}
					}
					else
					{
						// NtQueryInformationProcess failed
						rc = -2;
					}
				}
				finally
				{
					Marshal.FreeHGlobal(memPBI);
				}
			}
			finally
			{
				Win32Native.CloseHandle(hProcess);
			}
		}
		else
		{
			// couldn't open process for VM read
			rc = -1;
		}
		return rc;
	}

	public static IReadOnlyList<string> CommandLineToArgs(string commandLine)
	{
		if (string.IsNullOrEmpty(commandLine)) { return Array.Empty<string>(); }

		var argv = Win32Native.CommandLineToArgv(commandLine, out var argc);
		if (argv == IntPtr.Zero)
		{
			throw new Win32Exception(Marshal.GetLastWin32Error());
		}
		try
		{
			var args = new string[argc];
			for (var i = 0; i < args.Length; ++i)
			{
				var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
				args[i] = Marshal.PtrToStringUni(p);
			}
			return args.ToList().AsReadOnly();
		}
		finally
		{
			Marshal.FreeHGlobal(argv);
		}
	}
}

Solution 3 - C#

A C# v6+ adaption of Jesse C. Slicer's excellent answer that:

  • is complete and should run as-is, once you add a reference to assembly System.Management.dll (needed for the WMI System.Management.ManagementSearcher class).

  • streamlines the original code and fixes a few problems

  • handles an additional exception that can occur if a process being examined has already exited.

using System.Management;
using System.ComponentModel;

// Note: The class must be static in order to be able to define an extension method.
static class Progam
{	
	private static void Main()
	{
		foreach (var process in Process.GetProcesses())
		{
			try
			{
				Console.WriteLine($"PID: {process.Id}; cmd: {process.GetCommandLine()}");
			}
			// Catch and ignore "access denied" exceptions.
			catch (Win32Exception ex) when (ex.HResult == -2147467259) {}
			// Catch and ignore "Cannot process request because the process (<pid>) has
            // exited." exceptions.
			// These can happen if a process was initially included in 
            // Process.GetProcesses(), but has terminated before it can be
            // examined below.
			catch (InvalidOperationException ex) when (ex.HResult == -2146233079) {}
		}
	}

	// Define an extension method for type System.Process that returns the command 
    // line via WMI.
	private static string GetCommandLine(this Process process)
	{
		string cmdLine = null;
		using (var searcher = new ManagementObjectSearcher(
          $"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {process.Id}"))
		{
			// By definition, the query returns at most 1 match, because the process 
            // is looked up by ID (which is unique by definition).
			using (var matchEnum = searcher.Get().GetEnumerator())
            {
			    if (matchEnum.MoveNext()) // Move to the 1st item.
			    {
			        cmdLine = matchEnum.Current["CommandLine"]?.ToString();
			    }
            }
		}
		if (cmdLine == null)
		{
			// Not having found a command line implies 1 of 2 exceptions, which the
            // WMI query masked:
			// An "Access denied" exception due to lack of privileges.
			// A "Cannot process request because the process (<pid>) has exited."
            // exception due to the process having terminated.
			// We provoke the same exception again simply by accessing process.MainModule.
			var dummy = process.MainModule; // Provoke exception.
		}
		return cmdLine;
	}
}

Solution 4 - C#

If you want to do the same on Linux where there’s no WMI nor ProcCmdLine32.dll, here's the code:

string cmdline = File.ReadAllText( $"/proc/{ process.Id }/cmdline" );

Solution 5 - C#

The StartInfo.Arguments is only used when you start the app, it is not a record of the command line arguments. If you start the applications with command line arguments, then store the arguments when they come into your application. In the simplest case, you could store them in a text file, then when you hit the button, shut down all the processes except the one with the button press event. Fire off a new application, and feed it that file in a new command line arg. While the old app shuts down, the new app fires off all the new processes (one for each line in the file) and shuts down. Psuedocode below:

static void Main(string[] args)
{
   if (args.Contains(StartProcessesSwitch))
      StartProcesses(GetFileWithArgs(args))
   else
      WriteArgsToFile();
      //Run Program normally
}

void button_click(object sender, ButtonClickEventArgs e)
{
   ShutDownAllMyProcesses()
}

void ShutDownAllMyProcesses()
{
   List<Process> processes = GetMyProcesses();
   foreach (Process p in processes)
   {
      if (p != Process.GetCurrentProcess())
         p.Kill(); //or whatever you need to do to close
   }
   ProcessStartInfo psi = new ProcessStartInfo();
   psi.Arguments = CreateArgsWithFile();
   psi.FileName = "<your application here>";
   Process p = new Process();
   p.StartInfo = psi;
   p.Start();
   CloseAppplication();
}

Hope this helps. Good luck!

Solution 6 - C#

First: Thank you Jesse, for your excellent solution. My variation is below. Note: One of the things I like about C# is that it is a strongly typed language. Therefore I eschew the use of var type. I feel that a little clarity is worth a few casts.

class Program
{
    static void Main(string[] args)
    {

 
            Process[] processes = Process.GetProcessesByName("job Test");
            for (int p = 0; p < processes.Length; p++)
            {
                String[] arguments = CommandLineUtilities.getCommandLinesParsed(processes[p]);
            }
            System.Threading.Thread.Sleep(10000);
    }
}



public abstract class CommandLineUtilities
{
    public static String getCommandLines(Process processs)
    {
        ManagementObjectSearcher commandLineSearcher = new ManagementObjectSearcher(
            "SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + processs.Id);
        String commandLine = "";
        foreach (ManagementObject commandLineObject in commandLineSearcher.Get())
        {
             commandLine+= (String)commandLineObject["CommandLine"];
        }

        return commandLine;
    }

    public static String[] getCommandLinesParsed(Process process)
    {
        return (parseCommandLine(getCommandLines(process)));
    }

    /// <summary>
    /// This routine parses a command line to an array of strings
    /// Element zero is the program name
    /// Command line arguments fill the remainder of the array
    /// In all cases the values are stripped of the enclosing quotation marks
    /// </summary>
    /// <param name="commandLine"></param>
    /// <returns>String array</returns>
    public  static String[] parseCommandLine(String commandLine)
    {
        List<String> arguments = new List<String>();

        Boolean stringIsQuoted = false;
        String argString = "";
        for (int c = 0; c < commandLine.Length; c++)  //process string one character at a tie
        {
            if (commandLine.Substring(c, 1) == "\"")
            {
                if (stringIsQuoted)  //end quote so populate next element of list with constructed argument
                {
                    arguments.Add(argString);
                    argString = "";
                }
                else
                {
                    stringIsQuoted = true; //beginning quote so flag and scip
                }
            }
            else if (commandLine.Substring(c, 1) == "".PadRight(1))
            {
                if (stringIsQuoted)
                {
                    argString += commandLine.Substring(c, 1); //blank is embedded in quotes, so preserve it
                }
                else if (argString.Length > 0)
                {
                    arguments.Add(argString);  //non-quoted blank so add to list if the first consecutive blank
                }
            }
            else
            {
                argString += commandLine.Substring(c, 1);  //non-blan character:  add it to the element being constructed
            }
        }

        return arguments.ToArray();

    }

}

Solution 7 - C#

Here is my take without polluting your assembly with types and with performance tweaks. It works on both x86&x64. Microsoft will unlikely to change internal apis/structures because a lot of consumer code depends of them.

If process is not elevated, it will return null for system processes like taskmanager etc. You might want to fallback to slow WMI solution for that case. Ive written WMI query without System.Management dependency and COM support but thats another story.

Actually if first pinvoke succeeds, its unlikely others will fail, but ive kept error checking code for sanity. You can use second further simplified version.

public unsafe static string? GetCommandLine(int processId)
{
    var processHadle = OpenProcess(0x410, 0, processId);
    if (processHadle == 0)
        goto error;
    var mem = stackalloc nint[sizeof(nint) * 16];
    int length;
    if (NtQueryInformationProcess(processHadle, 0, mem, sizeof(nint) * 6, &length) != 0)
        goto error;
    var pbiBaseAddress = mem[1];
    if (pbiBaseAddress == 0)
        goto error;
    if (ReadProcessMemory(processHadle, pbiBaseAddress, mem, sizeof(nint) * 5, &length) == 0
        || (length != sizeof(nint) * 5))
        goto error;
    var processParameters = mem[4];
    if (ReadProcessMemory(processHadle, processParameters, mem, sizeof(nint) * 16, &length) == 0
        || (length != sizeof(nint) * 16))
        goto error;
    var cmdLineUnicode = mem + 14;
    var cmdLineLength = ((short*)cmdLineUnicode)[1];
    var pStr = Marshal.AllocHGlobal(cmdLineLength);
    if (ReadProcessMemory(processHadle, *(IntPtr*)(cmdLineUnicode + 1), (void*)pStr, cmdLineLength, &length) == 0)
        goto error;
    var str = new string((char*)pStr);
    Marshal.FreeHGlobal(pStr);
    return str;
    error:
    if (processHadle != 0)
        CloseHandle(processHadle);
    if (pStr != IntPtr.Zero)
        Marshal.FreeHGlobal(pStr);
    return null;

    [DllImport("ntdll.dll")]
    static extern int NtQueryInformationProcess(nint ProcessHandle, int ProcessInformationClass, void* ProcessInformation, int ProcessInformationLength, int* ReturnLength);
    [DllImport("kernel32.dll")]
    static extern nint OpenProcess(int dwDesiredAccess, int bInheritHandle, int dwProcessId);
    [DllImport("kernel32.dll")]
    static extern int ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, void* lpBuffer, int nSize, int* lpNumberOfBytesRead);
    [DllImport("kernel32.dll")]
    static extern int CloseHandle(nint hObject);
}

here is the second:

public unsafe static string GetCommandLineByPEB(int processId)
{
    var processHadle = OpenProcess(0x410, 0, processId);
    if (processHadle == 0)
        return null;
    var mem = stackalloc nint[sizeof(nint) * 16];
    int len;
    NtQueryInformationProcess(processHadle, 0, mem, sizeof(nint) * 6, &len);
    ReadProcessMemory(processHadle, mem[1], mem, sizeof(nint) * 5, &len);
    ReadProcessMemory(processHadle, mem[4], mem, sizeof(nint) * 16, &len);
    var cmdLineUnicode = mem + 14;
    length = ((short*)cmdLineUnicode)[1];
    var pStr = (char*)Marshal.AllocHGlobal(length);
    ReadProcessMemory(processHadle, cmdLineUnicode[1], pStr, length, &len);
    CloseHandle(processHadle);
    var str = new string(pStr);
    Marshal.FreeHGlobal((nint)pStr);
    Marshal.FreeHGlobal((nint)mem);
    return str;

    [DllImport("ntdll.dll")] static extern int NtQueryInformationProcess(nint ProcessHandle, int ProcessInformationClass, void* ProcessInformation, int ProcessInformationLength, int* ReturnLength);
    [DllImport("kernel32.dll")] static extern nint OpenProcess(int dwDesiredAccess, int bInheritHandle, int dwProcessId);
    [DllImport("kernel32.dll")] static extern int ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, void* lpBuffer, int nSize, int* lpNumberOfBytesRead);
    [DllImport("kernel32.dll")] static extern int CloseHandle(nint hObject);
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJonathan SchusterView Question on Stackoverflow
Solution 1 - C#Jesse C. SlicerView Answer on Stackoverflow
Solution 2 - C#AndyView Answer on Stackoverflow
Solution 3 - C#mklement0View Answer on Stackoverflow
Solution 4 - C#SoontsView Answer on Stackoverflow
Solution 5 - C#AudieView Answer on Stackoverflow
Solution 6 - C#Mark AinsworthView Answer on Stackoverflow
Solution 7 - C#ömer hayyamView Answer on Stackoverflow