7

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.

Joshua
  • 40,822
  • 8
  • 72
  • 132
  • Does [this answer](https://stackoverflow.com/a/7881186/3034273) your question? – Xerillio Oct 28 '20 at 20:53
  • @Xerillio: Clearly no, because I show `.MainModule.FileName` returning the wrong name in the question. – Joshua Oct 28 '20 at 20:54
  • I don't follow. The answer suggests `Assembly.GetExecutingAssembly().CodeBase` - that's not one of your examples. Perhaps it gives the same result as one of your examples (I'm not sure), but it doesn't feel like a "clearly" bad suggestion for you to try. – Xerillio Oct 28 '20 at 20:59
  • @Xerillio: Anything involving` Assembly.GetExecutingAssembly()` doesn't work in .NET core reliably because that's the `.dll` not the executable itself. – Joshua Oct 28 '20 at 21:01
  • `Process.GetCurrentProcess().MainModule.FileName` worked for me on .NET 5.0-rc.2.20475.5. I get the .exe's file name, same as on .NET Core 3.1. Even did a Release build and ran it from the command line instead of the debugger. Same result. – madreflection Oct 28 '20 at 21:27
  • @madreflection: Try making a symbolic link to the executable and then running it via the original name. You don't get the symbolic link name back. I'm not trying to open the file, I'm trying to switch on the user-invoked name. – Joshua Oct 28 '20 at 21:32
  • @Joshua, If I have understood it right, You need to get the name of the `.exe` file that is being executed, right? – Jamshaid K. Oct 28 '20 at 21:37
  • Can you confirm what is the problem with the first code snippet that you tried, does it throw any error? – Jamshaid K. Oct 28 '20 at 21:38
  • `typeof(Program).Assembly.GetName().Name` Have you tried this, does it work ? – Jamshaid K. Oct 28 '20 at 21:38
  • @JamshaidKamran: I need to get the name the exe was invoked as. The exe will have multiple names, and I need to know which one. Apologies for mixing languages but this is exactly `GetFileNameWithoutExtension(argv[0])`. – Joshua Oct 28 '20 at 21:38
  • Am I wrong to assume that in dot net core, "dotnet" is the excuting assembly and your code is nothing more than a dll at the heart of it? That is the behavior I see in Linux, Windows, and MacOS. This is not unlike Java where you will never see you own executable.. only a Java executable.. only the JVM running your jar packages. – Señor CMasMas Nov 03 '20 at 04:07
  • @SeñorCMasMas: When you run dotnet publish you get a real executable. See here https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file for details. – Joshua Nov 03 '20 at 04:11
  • Thanks for the information @Joshua, thats why I said "at the heart of it".. I bet these bugs exist because the "real executable" isn't. It's still being run by the dotnet runtime.. even if that too is included. In windows, there are special hooks in PE files for dotnet, but not in elf. I am speculating.. not SAYING. ;) – Señor CMasMas Nov 03 '20 at 04:20
  • @Joshua, what do you mean "running it via the original name"? If I run the symlink or hard link, `MainModule.FileName` shows the linked name. – shingo Nov 03 '20 at 04:28
  • @shingo: Got linux? Behavior seems to differ from one system to the next. – Joshua Nov 03 '20 at 04:29
  • @Joshua, windows with mklink. – shingo Nov 03 '20 at 04:34

3 Answers3

2

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
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    I have tested all of these and the only one that works is GetProcessCommandLine(). I had in fact made a library containing the solution and nobody else cared. https://github.com/joshudson/Emet/tree/master/MultiCall – Joshua Feb 12 '22 at 15:52
  • @Joshua you mean `GetProcessCommandLine` is the only one that works as expected with for .NET 5 for you? – noseratio Feb 12 '22 at 21:25
  • I personally need it for IPC (where a process can re-launch itself), and from that standpoint `GetProcessCommandLine` seems to be the most reliable way, too. – noseratio Feb 12 '22 at 21:58
  • Try a Linux machine and watch everything fail. – Joshua Feb 12 '22 at 22:00
1

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.

noseratio
  • 59,932
  • 34
  • 208
  • 486
Joshua
  • 40,822
  • 8
  • 72
  • 132
  • Thanks for taking time to answer and for your library too. Does it work for `dotnet my.dll` scenario? – noseratio Feb 12 '22 at 22:14
  • @noseratio: Don't know. – Joshua Feb 12 '22 at 22:17
  • It probably should, on Windows via `GetProcessCommandLine` it looks like `"C:\Program Files\dotnet\dotnet.exe" ProcessPath.dll` – noseratio Feb 12 '22 at 22:19
  • 1
    @noseratio: My use case didn't work like that. But I guess your use case would benefit. README: https://github.com/joshudson/Emet/tree/master/MultiCall – Joshua Feb 12 '22 at 22:40
-1

Using .NET Core 3.1, this worked for me to restart a currently running program.

                string filename = Process.GetCurrentProcess().MainModule.FileName;

                System.Diagnostics.Process.Start(filename);

                // Closes the current process
                Environment.Exit(0);