How do I get the name of the current executable in C#? (.NET 5 edition)

Viewed 880

How do I get the name the executable was invoked as (equivalent to C's argv[0])? I actually need to handle somebody renaming the executable and stuff like that.

There's a famous question with lots of answers that don't work. Answers tried:

System.AppDomain.CurrentDomain.FriendlyName

returns the name it was compiled as

System.Diagnostics.Process.GetCurrentProcess().ProcessName

strips extension (ever rename a .exe to a .com?), also sees through symbolic links

Environment.GetCommandLineArgs()[0]

It returns a name ending in .dll, clearly an error.

Assembly.GetEntryAssembly().Location

Returns null

System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName

Returns a .dll name again.

The documentation for .net 5.0 says Environment.GetCommandLineArguments()[0] works; however it doesn't actually work. It somehow sees through symbolic links and returns the real executable name.

What I'm trying to do is link all of our stuff into a single multi-call binary so I can use the .net 5 framework reducer on the resulting binary so I don't have to ship about 30MB of .net 5 framework we're not using. I really don't want to do a platform ladder and P/Invoke a bunch of stuff unless I have to.

I'm after argv[0] directly, not the running process executable name. In the case of symbolic links, these differ.

2 Answers

Came across this with .NET 6, where Process.GetCurrentProcess().MainModule?.FileName seems to be working fine now, and there's also Environment.ProcessPath.

If targeting Windows only, it might be safer (more predictable) to use interop. Below are some options, including the native GetModuleFileName and GetCommandLine:

using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;

Console.WriteLine("Process.GetCurrentProcess().MainModule?.FileName");
Console.WriteLine(Process.GetCurrentProcess().MainModule?.FileName);
Console.WriteLine();

Console.WriteLine("Assembly.GetExecutingAssembly().Location");
Console.WriteLine(Assembly.GetExecutingAssembly().Location);
Console.WriteLine();

Console.WriteLine("Environment.ProcessPath");
Console.WriteLine(Environment.ProcessPath);
Console.WriteLine();

Console.WriteLine("Environment.CommandLine");
Console.WriteLine(Environment.CommandLine);
Console.WriteLine();

Console.WriteLine("Environment.GetCommandLineArgs()[0]");
Console.WriteLine(Environment.GetCommandLineArgs()[0]);
Console.WriteLine();

Console.WriteLine("Win32.GetProcessPath()");
Console.WriteLine(Win32.GetProcessPath());
Console.WriteLine();

Console.WriteLine("Win32.GetProcessCommandLine()");
Console.WriteLine(Win32.GetProcessCommandLine());
Console.WriteLine();

public static class Win32
{
    private const int MAX_PATH = 260;
    private const int INSUFFICIENT_BUFFER = 0x007A;
    private const int MAX_UNICODESTRING_LEN = short.MaxValue;

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    private static extern IntPtr GetCommandLine();

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    [PreserveSig]
    [return: MarshalAs(UnmanagedType.U4)]
    private static extern int GetModuleFileName(
        IntPtr hModule, StringBuilder lpFilename, [MarshalAs(UnmanagedType.U4)] int nSize);

    public static string GetProcessCommandLine() 
    {
        return Marshal.PtrToStringUni(GetCommandLine()) ?? 
            throw new Win32Exception(nameof(GetCommandLine));
    }

    public static string GetProcessPath()
    {
        var buffer = new StringBuilder(MAX_PATH);
        while (true)
        {
            int size = GetModuleFileName(IntPtr.Zero, buffer, buffer.Capacity);
            if (size == 0)
            {
                throw new Win32Exception();
            }

            if (size == buffer.Capacity)
            {
                // double the buffer size and try again.
                buffer.EnsureCapacity(buffer.Capacity * 2);
                continue;
            }

            return Path.GetFullPath(buffer.ToString());
        }
    }
}

The output when running via dotnet run:

Process.GetCurrentProcess().MainModule?.FileName
C:\temp\ProcessPath\bin\Debug\net6.0\ProcessPath.exe

Assembly.GetExecutingAssembly().Location
C:\temp\ProcessPath\bin\Debug\net6.0\ProcessPath.dll

Environment.ProcessPath
C:\temp\ProcessPath\bin\Debug\net6.0\ProcessPath.exe

Environment.CommandLine
C:\temp\ProcessPath\bin\Debug\net6.0\ProcessPath.dll

Environment.GetCommandLineArgs()[0]
C:\temp\ProcessPath\bin\Debug\net6.0\ProcessPath.dll

Win32.GetProcessPath()
C:\temp\ProcessPath\bin\Debug\net6.0\ProcessPath.exe

Win32.GetProcessCommandLine()
"C:\temp\ProcessPath\bin\Debug\net6.0\ProcessPath.exe"

Oh, and for a Windows Forms application, there always has been Application.ExecutablePath.

Updated, running it on Ubuntu 22.04 with .NET 6.0.2 (with Win32 interop removed), either via dotnet run or directly as ./ProcessPath:

Process.GetCurrentProcess().MainModule?.FileName
/home/noseratio/ProcessPath/bin/Debug/net6.0/ProcessPath

Assembly.GetExecutingAssembly().Location
/home/noseratio/ProcessPath/bin/Debug/net6.0/ProcessPath.dll

Environment.ProcessPath
/home/noseratio/ProcessPath/bin/Debug/net6.0/ProcessPath

Environment.CommandLine
/home/noseratio/ProcessPath/bin/Debug/net6.0/ProcessPath.dll

Environment.GetCommandLineArgs()[0]
/home/noseratio/ProcessPath/bin/Debug/net6.0/ProcessPath.dll

After watching everything fail, it became necessary to P/Invoke stuff to make this work. While Process.GetCurrentProcess().MainModule?.FileName reliably returns the executable binary (at least when not running under the debugger), this does not provide the command invocation the binary was launched with.

On Windows, GetCommandLine() is P/Invokable and needs only some parsing to get the information. On *n?x, reading /proc/self/cmdline does the same job.

I built a library encapsulating this. https://github.com/joshudson/Emet/tree/master/MultiCall You can find binaries on nuget.org ready to go.

I should have self-answered a long time ago. Better late than never. Nobody seemed to care until now.

Related