Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to get current process invocation name (argv[0]/$0)? (login shell on *nix) #30212

Open
rjmholt opened this issue Jul 10, 2019 · 7 comments
Open

Comments

@rjmholt
Copy link

rjmholt commented Jul 10, 2019

I'm working to enable PowerShell as a login shell on *nix in PowerShell/PowerShell#10050.

When -Login or -l is passed, this works fine (we exec /bin/sh and then exec pwsh again).

But the convention in many *nix environments for login shells is to prepend a - onto the process name when it is exec'd (e.g. bash sees its own process name as -bash and then deduces it must run login shell logic).

With PowerShell as a .NET Core 3.0 application, I don't know how to access this information.

I've tried the args in public static int Main(string[] args), Environment.GetCommandLineArgs() and Process.GetCurrentProcess():

public static int Main(string[] args)
{
            Console.WriteLine("ARGS");
            foreach (string arg in args)
            {
                Console.WriteLine(arg);
            }
            Console.WriteLine("ENVIRONMENT ARGS");
            foreach (string arg in Environment.GetCommandLineArgs())
            {
                Console.WriteLine(arg);
            }
            Console.WriteLine($"PROCESS NAME: {System.Diagnostics.Process.GetCurrentProcess().ProcessName}");
...
}

Which from PowerShell prints when started as the default login shell on macOS:

ARGS
ENVIRONMENT ARGS
/Users/rjmholt/Documents/Dev/Microsoft/PowerShell/src/powershell-unix/bin/Debug/netcoreapp3.0/osx-x64/publish/pwsh.dll
PROCESS NAME: pwsh

Writing a small C program to show the same (compiled with cc ex.c):

#include <stdio.h>

int main(int argc, char **argv)
{
    for (int i = 0; i < argc; i++)
    {
	printf("%d: %s\n", i, argv[i]);
    }
}

When started as a login shell, prints:

0: -a.out

The mechanism for this seems to be the login util, which prepends the - before exec-ing the given executable.

My question is: is there a way to detect the name with which a process has been started? Ideally we could do this performantly, since it's a startup time thing and we just want to detect it as fast as possible so we can exec another process.

@rjmholt rjmholt changed the title How to get process invocation name? (login shell on *nix) How to get current process invocation name? (login shell on *nix) Jul 10, 2019
@rjmholt rjmholt changed the title How to get current process invocation name? (login shell on *nix) How to get current process invocation name ($0)? (login shell on *nix) Jul 10, 2019
@rjmholt
Copy link
Author

rjmholt commented Jul 10, 2019

Additional output from starting PowerShell as a login shell with the Console.WriteLines above:

Last login: Wed Jul 10 10:02:49 on ttys002
ARGS
ENVIRONMENT ARGS
/Users/rjmholt/Documents/Dev/Microsoft/PowerShell/src/powershell-unix/bin/Debug/netcoreapp3.0/osx-x64/publish/pwsh.dll
PROCESS NAME: pwsh
PowerShell 7.0.0-preview.1-110-g3a46ca0c02f207b7e98ccefdd32655af524d04d9
Copyright (c) Microsoft Corporation. All rights reserved.

https://aka.ms/powershell
Type 'help' to get help.

Loading personal and system profiles took 1082ms.
/Users/rjmholt
 > ps aux | grep pwsh
rjmholt           1835   2.2  0.5  6629876  79620 s002  R+   10:08am   0:03.40 -pwsh
rjmholt           1175   0.0  0.6  6618972 106472 s000  S+    9:03am   0:15.01 pwsh-preview
/Users/rjmholt
 > $pid
1835
/Users/rjmholt
 >

Note that the process is listed by ps as -pwsh but Process.GetCurrentProcess().ProcessName is pwsh.

@rjmholt rjmholt changed the title How to get current process invocation name ($0)? (login shell on *nix) How to get current process invocation name (argv[0]/$0)? (login shell on *nix) Jul 11, 2019
@rjmholt
Copy link
Author

rjmholt commented Jul 11, 2019

In case anyone else (admittedly not many others writing login shells on .NET) wants a starting point for how to do this:

        private static void DumpArgv()
        {
            int pid = getpid();

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                string[] argv = System.IO.File.ReadAllText($"/proc/{pid}/cmdline").Split('\0');
                for (int i = 0; i < argv.Length; i++)
                {
                    Console.WriteLine($"argv[{i}]: {argv[i]}");
                }
                return;
            }

            // Get proc table size
            int[] mib = new [] { CTL_KERN, KERN_ARGMAX };
            int size = sizeof(int);
            int maxargs = 0;

            unsafe
            {
                if (sysctl(mib, mib.Length, &maxargs, &size, IntPtr.Zero, 0) == -1)
                {
                    throw new Exception("argmax");
                }

                // Now read the proc table
                IntPtr procargs = Marshal.AllocHGlobal(maxargs);
                try
                {
                    size = maxargs;

                    mib = new [] { CTL_KERN, KERN_PROCARGS2, pid };

                    if (sysctl(mib, mib.Length, procargs.ToPointer(), &size, IntPtr.Zero, 0) == -1)
                    {
                        throw new Exception("procargs");
                    }

                    // Skip over argc
                    int argc = Marshal.ReadInt32(procargs);
                    Console.WriteLine($"argc: {argc}");
                    byte *argvPtr = (byte *)IntPtr.Add(procargs, sizeof(int)).ToPointer();

                    Console.WriteLine($"exec_path: {Marshal.PtrToStringAnsi((IntPtr)argvPtr)}");
                    while (*argvPtr != 0) { argvPtr++; }
                    while (*argvPtr == 0) { argvPtr++; }

                    for (int i = 0; i < argc; i++)
                    {
                        Console.WriteLine($"argv[{i}]: {Marshal.PtrToStringAnsi((IntPtr)argvPtr)}");
                        while (*argvPtr != 0) { argvPtr++; }
                        while (*argvPtr == 0) { argvPtr++; }
                    }
                }
                finally
                {
                    Marshal.FreeHGlobal(procargs);
                }
            }
        }

        [DllImport("libc")]
        private static extern int getpid();

        [DllImport("libc")]
        private static unsafe extern int sysctl(int[] mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp);

Based on https://gist.github.com/nonowarn/770696#file-getargv-c.

@krwq
Copy link
Member

krwq commented Jul 17, 2019

Is this possibly related to dotnet/corefx#37294 - I have already expressed my concerns about it:
dotnet/corefx#37294 (comment)

Perhaps we should add ProcessPath now?
cc: @tmds

@tmds
Copy link
Member

tmds commented Jul 17, 2019

execve takes a separate pathname and argv array:

       int execve(const char *pathname, char *const argv[],
                  char *const envp[]);

argv[0] typically matches with the final part of the pathname. e.g. pathname=/bin/sh, argv[0]=sh.
ProcessName gets derived from pathname (/proc/xxx/stat) and then we use argv[0] and argv[1] (/proc/xxx/cmdline) to support longer names.

In this case you want to check the argv[0], that one is -pwsh while pathname is pwsh.

We're not using cmdline as the base because if we would, scripts return their interpreter executable rather than the script name, which makes it difficult to find a specific Process using .NET APIs.

We can probably augment the cmdline reading to specifically support the login shell use-case. That is, ProcessName would then include the -.

A more generic .NET API would expose the pathname and argv array as separate properties.

@krwq
Copy link
Member

krwq commented Jul 17, 2019

@tmds I believe until we got 2 APIs it's actually more useful to get argv[0] since you can write additional logic to get it yourself while getting actual argv[0] with current solution requires native call as in above workaround.

I agree there should eventually be helper function to get user friendly name (that would be useful even for dotnet run where you most likely don't want to see dotnet path as your process) but that feels like something we could address with additional API post 3.0

@tmds
Copy link
Member

tmds commented Jul 17, 2019

it's actually more useful to get argv[0] since you can write additional logic

The current implementation of ProcessName helps users identify the correct process. There are no issues reported for this.
The login shell is the niche use-case so I'd rather have the additional logic there, than change what ProcessName is returning now.

@msftgits msftgits transferred this issue from dotnet/corefx Feb 1, 2020
@msftgits msftgits added this to the Future milestone Feb 1, 2020
@maryamariyan maryamariyan added the untriaged New issue has not been triaged by the area owner label Feb 23, 2020
@adamsitnik adamsitnik removed the untriaged New issue has not been triaged by the area owner label Jul 6, 2020
@mklement0
Copy link

mklement0 commented Oct 16, 2020

Note that another problem with Environment.GetCommandLineArgs()[0] - apart from #11305 - is that it doesn't reflect the actual invocation path if invocation happened via a symlink, which is quite common on Unix, and it is even somewhat common for utilities to modify their behavior based on the file name of the invocation name (e.g., the tset utility applies a number of implied options when invoked via a symlink named reset)

I'm assuming that fixing the issue at hand would also fix the symlink issue - or should I open a separate one?

The division of labor would then be:

  • Environment.GetCommandLineArgs()[0] would report the true argv[0] / $0 invocation name, and allow shells and utilities to detect invocation conventions (- prefix) / alternate names (via symlinks) that request modified behavior.

  • Environment.ProcessPath (see Add Environment.ProcessPath #42768) reports the full, physical file path of the executable (as Process.GetCurrentProcess().MainModule.FileName already does).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants