From ca6e8c0a9e62e22ee2a8787764f6294400ca931d Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 14 Jun 2019 16:33:41 -0700 Subject: [PATCH 01/44] First attempt --- .../host/msh/CommandLineParameterParser.cs | 8 +++++ .../host/msh/ManagedEntrance.cs | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index de5d7215e84..316a5f4dc2d 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -514,6 +514,8 @@ private static string GetConfigurationNameFromGroupPolicy() /// internal static void EarlyParse(string[] args) { + loginArgIndex = -1; + if (args == null) { Dbg.Assert(args != null, "Argument 'args' to EarlyParseHelper should never be null"); @@ -531,6 +533,12 @@ internal static void EarlyParse(string[] args) string switchKey = switchKeyResults.SwitchKey; + if (MatchSwitch(switchKey, match: "login", smallestUnambiguousMatch: "l")) + { + loginArgIndex = i; + return; + } + if (MatchSwitch(switchKey, match: "settingsfile", smallestUnambiguousMatch: "settings")) { // parse setting file arg and don't write error as there is no host yet. diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs index eb9b588d414..b4b1f4275ae 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs @@ -98,6 +98,42 @@ public static int Start(string consoleFilePath, [MarshalAs(UnmanagedType.LPArray return exitCode; } + + private static int StartLoginShell(string[] args, int loginArgIndex) + { + string pwshPath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; + + var sb = new System.Text.StringBuilder("exec ", capacity: 256).Append(pwshPath); + for (int i = 0; i < args.Length; i++) + { + if (i == loginArgIndex) + { + continue; + } + + sb.Append(' '); + + string arg = args[i]; + + if (arg.StartsWith('-')) + { + sb.Append(arg); + continue; + } + + sb.Append('\'').Append(arg.Replace("'", "'\\''")).Append('\''); + } + string pwshInvocation = sb.ToString(); + + return Exec("/bin/sh", new string[] { "-l", "-i", "-c", pwshInvocation, null }); + } + + [DllImport("libc", + EntryPoint = "execv", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi, + SetLastError = true)] + private static extern int Exec(string path, string[] args); } } From 33519b45e9c92edd3f1110acfb6f762f9c18ab9f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 26 Jun 2019 13:04:11 -0700 Subject: [PATCH 02/44] Move -Login parsing out to Program.cs --- .../host/msh/ManagedEntrance.cs | 36 ------- src/powershell/Program.cs | 100 ++++++++++++++++++ 2 files changed, 100 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs index b4b1f4275ae..eb9b588d414 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs @@ -98,42 +98,6 @@ public static int Start(string consoleFilePath, [MarshalAs(UnmanagedType.LPArray return exitCode; } - - private static int StartLoginShell(string[] args, int loginArgIndex) - { - string pwshPath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; - - var sb = new System.Text.StringBuilder("exec ", capacity: 256).Append(pwshPath); - for (int i = 0; i < args.Length; i++) - { - if (i == loginArgIndex) - { - continue; - } - - sb.Append(' '); - - string arg = args[i]; - - if (arg.StartsWith('-')) - { - sb.Append(arg); - continue; - } - - sb.Append('\'').Append(arg.Replace("'", "'\\''")).Append('\''); - } - string pwshInvocation = sb.ToString(); - - return Exec("/bin/sh", new string[] { "-l", "-i", "-c", pwshInvocation, null }); - } - - [DllImport("libc", - EntryPoint = "execv", - CallingConvention = CallingConvention.Cdecl, - CharSet = CharSet.Ansi, - SetLastError = true)] - private static extern int Exec(string path, string[] args); } } diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 94560f25e04..630ea9ff382 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -3,6 +3,7 @@ using System; using System.Reflection; +using System.Runtime.InteropServices; namespace Microsoft.PowerShell { @@ -19,7 +20,106 @@ public sealed class ManagedPSEntry /// public static int Main(string[] args) { + if (HasLoginSpecified(args, out int loginIndex)) + { + return ExecPwshLogin(args, loginIndex); + } return UnmanagedPSEntry.Start(string.Empty, args, args.Length); } + + private static bool HasLoginSpecified(string[] args, out int loginIndex) + { + loginIndex = -1; + + ReadOnlySpan loginParam = stackalloc char[] { 'l', 'o', 'g', 'i', 'n' }; + ReadOnlySpan loginParamUpper = stackalloc char[] { 'L', 'O', 'G', 'I', 'N' }; + ReadOnlySpan fileParam = stackalloc char[] { 'f', 'i', 'l', 'e' }; + ReadOnlySpan fileParamUpper = stackalloc char[] { 'F', 'I', 'L', 'E' }; + + for (int i = 0; i < args.Length; i++) + { + string arg = args[i]; + + // Too short to be like -Login + if (arg.Length < 2) { continue; } + + // Check for "-Login" or some prefix thereof + if (arg[0] == '-') + { + if (IsParam(arg, in loginParam, in loginParamUpper)) + { + loginIndex = i; + return true; + } + + if (IsParam(arg, in fileParam, in fileParamUpper)) + { + return false; + } + } + } + + return false; + } + + private static bool IsParam( + string arg, + in ReadOnlySpan paramToCheck, + in ReadOnlySpan paramToCheckUpper) + { + if (arg.Length > paramToCheck.Length + 1) + { + return false; + } + + for (int i = 1; i < paramToCheck.Length; i++) + { + if (arg[i] != paramToCheck[i-1] + && arg[i] != paramToCheckUpper[i-1]) + { + return false; + } + } + + return true; + } + + private static int ExecPwshLogin(string[] args, int loginArgIndex) + { + string pwshPath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; + + var sb = new System.Text.StringBuilder("exec ", capacity: 256).Append(pwshPath); + for (int i = 0; i < args.Length; i++) + { + if (i == loginArgIndex) + { + continue; + } + + sb.Append(' '); + + string arg = args[i]; + + if (arg.StartsWith('-')) + { + sb.Append(arg); + continue; + } + + sb.Append('\'').Append(arg.Replace("'", "'\\''")).Append('\''); + } + string pwshInvocation = sb.ToString(); + + string[] execArgs = new string[] { "-l", "-i", "-c", pwshInvocation, null }; + + return Exec("/bin/sh", execArgs); + } + + [DllImport("libc", + EntryPoint = "execv", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi, + SetLastError = true)] + private static extern int Exec(string path, string[] args); } } From 43cf25aab2d5e5579a0441d9c70b22fa02a3d9c8 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 26 Jun 2019 13:10:21 -0700 Subject: [PATCH 03/44] Fix merge bug --- .../host/msh/CommandLineParameterParser.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 316a5f4dc2d..de5d7215e84 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -514,8 +514,6 @@ private static string GetConfigurationNameFromGroupPolicy() /// internal static void EarlyParse(string[] args) { - loginArgIndex = -1; - if (args == null) { Dbg.Assert(args != null, "Argument 'args' to EarlyParseHelper should never be null"); @@ -533,12 +531,6 @@ internal static void EarlyParse(string[] args) string switchKey = switchKeyResults.SwitchKey; - if (MatchSwitch(switchKey, match: "login", smallestUnambiguousMatch: "l")) - { - loginArgIndex = i; - return; - } - if (MatchSwitch(switchKey, match: "settingsfile", smallestUnambiguousMatch: "settings")) { // parse setting file arg and don't write error as there is no host yet. From 9d7137c9c5c6993803d3b349bcc86f2abd03a83d Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 26 Jun 2019 14:47:00 -0700 Subject: [PATCH 04/44] Fix login params --- src/powershell/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 630ea9ff382..e8832bb0b94 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -110,7 +110,7 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) } string pwshInvocation = sb.ToString(); - string[] execArgs = new string[] { "-l", "-i", "-c", pwshInvocation, null }; + string[] execArgs = new string[] { "-l", "-c", pwshInvocation, null }; return Exec("/bin/sh", execArgs); } From 53e698edf89dacc605cf9f52c6a25f56517cd5e0 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 2 Jul 2019 15:03:40 -0700 Subject: [PATCH 05/44] Fix and speed up login logic --- src/powershell/Program.cs | 101 +++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index e8832bb0b94..a792a1f093a 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -35,6 +35,8 @@ private static bool HasLoginSpecified(string[] args, out int loginIndex) ReadOnlySpan loginParamUpper = stackalloc char[] { 'L', 'O', 'G', 'I', 'N' }; ReadOnlySpan fileParam = stackalloc char[] { 'f', 'i', 'l', 'e' }; ReadOnlySpan fileParamUpper = stackalloc char[] { 'F', 'I', 'L', 'E' }; + ReadOnlySpan commandParam = stackalloc char[] { 'c', 'o', 'm', 'm', 'a', 'n', 'd' }; + ReadOnlySpan commandParamUpper = stackalloc char[] { 'C', 'O', 'M', 'M', 'A', 'N', 'D' }; for (int i = 0; i < args.Length; i++) { @@ -52,7 +54,8 @@ private static bool HasLoginSpecified(string[] args, out int loginIndex) return true; } - if (IsParam(arg, in fileParam, in fileParamUpper)) + if (IsParam(arg, in fileParam, in fileParamUpper) + || IsParam(arg, in commandParam, in commandParamUpper)) { return false; } @@ -72,7 +75,7 @@ private static bool IsParam( return false; } - for (int i = 1; i < paramToCheck.Length; i++) + for (int i = 1; i < arg.Length - 1; i++) { if (arg[i] != paramToCheck[i-1] && arg[i] != paramToCheckUpper[i-1]) @@ -88,31 +91,93 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) { string pwshPath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; - var sb = new System.Text.StringBuilder("exec ", capacity: 256).Append(pwshPath); - for (int i = 0; i < args.Length; i++) + int quotedPwshPathLength = GetQuotedPathLength(pwshPath); + string pwshInvocation = string.Create( + 10 + quotedPwshPathLength, // exec '{pwshPath}' "$@" + (pwshPath, quotedPwshPathLength), + CreatePwshInvocation); + + var execArgs = new string[args.Length + 4]; + execArgs[0] = "-l"; + execArgs[1] = "-c"; + execArgs[2] = pwshInvocation; + execArgs[3] = ""; + + int i = 0; + int j = 4; + for (; i < args.Length; i++) { - if (i == loginArgIndex) - { - continue; - } + if (i == loginArgIndex) { continue; } - sb.Append(' '); + execArgs[j] = args[i]; + j++; + } - string arg = args[i]; + execArgs[execArgs.Length - 1] = null; + + int exitCode = Exec("/bin/sh", execArgs); - if (arg.StartsWith('-')) + if (exitCode != 0) + { + int errno = Marshal.GetLastWin32Error(); + Console.WriteLine($"Error: {errno}"); + return errno; + } + + return 0; + } + + private static int GetQuotedPathLength(string str) + { + int length = 2; + foreach (char c in str) + { + length++; + if (c == '\'') { length++; } + } + + return length; + } + + private static void CreatePwshInvocation(Span strBuf, (string path, int quotedLength) pwshPath) + { + strBuf[0] = 'e'; + strBuf[1] = 'x'; + strBuf[2] = 'e'; + strBuf[3] = 'c'; + strBuf[4] = ' '; + + Span pathSpan = strBuf.Slice(5, pwshPath.quotedLength); + QuoteAndWriteToSpan(pwshPath.path, pathSpan); + + int argIndex = 5 + pwshPath.quotedLength; + strBuf[argIndex] = ' '; + strBuf[argIndex + 1] = '"'; + strBuf[argIndex + 2] = '$'; + strBuf[argIndex + 3] = '@'; + strBuf[argIndex + 4] = '"'; + } + + private static void QuoteAndWriteToSpan(string arg, Span span) + { + span[0] = '\''; + + int i = 0; + int j = 1; + for (; i < arg.Length; i++, j++) + { + char c = arg[i]; + + if (c == '\'') { - sb.Append(arg); - continue; + span[j] = '\\'; + j++; } - sb.Append('\'').Append(arg.Replace("'", "'\\''")).Append('\''); + span[j] = c; } - string pwshInvocation = sb.ToString(); - - string[] execArgs = new string[] { "-l", "-c", pwshInvocation, null }; - return Exec("/bin/sh", execArgs); + span[j] = '\''; } [DllImport("libc", From 025b22c05905d4eef69bc9fcde61147031d6b0fb Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 09:32:13 -0700 Subject: [PATCH 06/44] Document startup code --- src/powershell/Program.cs | 103 +++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 25 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index a792a1f093a..1dbb42ba3d1 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -27,10 +27,17 @@ public static int Main(string[] args) return UnmanagedPSEntry.Start(string.Empty, args, args.Length); } + /// + /// Checks args to see if -Login has been specified. + /// + /// Arguments passed to the program. + /// The arg index (in argv) where -Login was found. + /// private static bool HasLoginSpecified(string[] args, out int loginIndex) { loginIndex = -1; + // Parameter comparison strings, stackalloc'd for performance ReadOnlySpan loginParam = stackalloc char[] { 'l', 'o', 'g', 'i', 'n' }; ReadOnlySpan loginParamUpper = stackalloc char[] { 'L', 'O', 'G', 'I', 'N' }; ReadOnlySpan fileParam = stackalloc char[] { 'f', 'i', 'l', 'e' }; @@ -42,39 +49,51 @@ private static bool HasLoginSpecified(string[] args, out int loginIndex) { string arg = args[i]; - // Too short to be like -Login - if (arg.Length < 2) { continue; } + // Must look like '-' + if (arg == null || arg.Length < 2 || arg[0] != '-') + { + continue; + } // Check for "-Login" or some prefix thereof - if (arg[0] == '-') + if (IsParam(arg, in loginParam, in loginParamUpper)) + { + loginIndex = i; + return true; + } + + // After -File and -Command, all parameters are passed + // to the invoked file or command, so we can stop looking. + if (IsParam(arg, in fileParam, in fileParamUpper) + || IsParam(arg, in commandParam, in commandParamUpper)) { - if (IsParam(arg, in loginParam, in loginParamUpper)) - { - loginIndex = i; - return true; - } - - if (IsParam(arg, in fileParam, in fileParamUpper) - || IsParam(arg, in commandParam, in commandParamUpper)) - { - return false; - } + return false; } } return false; } + /// + /// Determines if a given parameter is the one we're looking for. + /// Assumes any prefix determines that parameter (true for -l, -c and -f). + /// + /// The argument to check. + /// The lowercase name of the parameter to check. + /// The uppercase name of the parameter to check. + /// private static bool IsParam( string arg, in ReadOnlySpan paramToCheck, in ReadOnlySpan paramToCheckUpper) { + // Quick fail if the argument is longer than the parameter if (arg.Length > paramToCheck.Length + 1) { return false; } + // Check arg chars in order and allow prefixes for (int i = 1; i < arg.Length - 1; i++) { if (arg[i] != paramToCheck[i-1] @@ -87,22 +106,37 @@ private static bool IsParam( return true; } + /// + /// Create the exec call to /bin/sh -l -c 'pwsh "$@"' and run it. + /// + /// The argument vector passed to pwsh. + /// The index of -Login in the argument vector. + /// + /// The exit code of exec if it fails. + /// If exec succeeds, this process is overwritten so we never actually return. + /// private static int ExecPwshLogin(string[] args, int loginArgIndex) { + // We need the path to the current pwsh executable string pwshPath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; + // Create input for /bin/sh that execs pwsh int quotedPwshPathLength = GetQuotedPathLength(pwshPath); string pwshInvocation = string.Create( 10 + quotedPwshPathLength, // exec '{pwshPath}' "$@" (pwshPath, quotedPwshPathLength), CreatePwshInvocation); + // Set up the arguments for /bin/sh var execArgs = new string[args.Length + 4]; + + // The command arguments execArgs[0] = "-l"; execArgs[1] = "-c"; execArgs[2] = pwshInvocation; - execArgs[3] = ""; + execArgs[3] = ""; // This is required because $@ skips the first argument + // Add the arguments passed to pwsh on the end int i = 0; int j = 4; for (; i < args.Length; i++) @@ -113,20 +147,19 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) j++; } + // A null is required by exec execArgs[execArgs.Length - 1] = null; - int exitCode = Exec("/bin/sh", execArgs); - - if (exitCode != 0) - { - int errno = Marshal.GetLastWin32Error(); - Console.WriteLine($"Error: {errno}"); - return errno; - } - - return 0; + // Finally exec the /bin/sh command + return Exec("/bin/sh", execArgs); } + /// + /// Gets what the length of the given string will be if it's + /// quote escaped for /bin/sh. + /// + /// The string to quote escape. + /// The length of the string when it's quote escaped. private static int GetQuotedPathLength(string str) { int length = 2; @@ -141,15 +174,18 @@ private static int GetQuotedPathLength(string str) private static void CreatePwshInvocation(Span strBuf, (string path, int quotedLength) pwshPath) { + // "exec " strBuf[0] = 'e'; strBuf[1] = 'x'; strBuf[2] = 'e'; strBuf[3] = 'c'; strBuf[4] = ' '; + // The quoted path to pwsh, like "'/opt/microsoft/powershell/7/pwsh'" Span pathSpan = strBuf.Slice(5, pwshPath.quotedLength); QuoteAndWriteToSpan(pwshPath.path, pathSpan); + // ' "$@"' the argument vector splat to pass pwsh arguments through int argIndex = 5 + pwshPath.quotedLength; strBuf[argIndex] = ' '; strBuf[argIndex + 1] = '"'; @@ -158,6 +194,11 @@ private static void CreatePwshInvocation(Span strBuf, (string path, int qu strBuf[argIndex + 4] = '"'; } + /// + /// Quotes (and sh quote escapes) a string and writes it to the given span. + /// + /// The string to quote. + /// The span to write to. private static void QuoteAndWriteToSpan(string arg, Span span) { span[0] = '\''; @@ -170,6 +211,7 @@ private static void QuoteAndWriteToSpan(string arg, Span span) if (c == '\'') { + // /bin/sh quote escaping uses backslashes span[j] = '\\'; j++; } @@ -180,6 +222,17 @@ private static void QuoteAndWriteToSpan(string arg, Span span) span[j] = '\''; } + /// + /// The `execv` syscall we use to exec /bin/sh. + /// + /// The path to the executable to exec. + /// + /// The arguments to send through to the executable. + /// Array must have its final element be null. + /// + /// + /// An exit code if exec failed, but if successful the calling process will be overwritten. + /// [DllImport("libc", EntryPoint = "execv", CallingConvention = CallingConvention.Cdecl, From ff4de637862ad10168a11972655f3e62e4c53493 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 09:48:45 -0700 Subject: [PATCH 07/44] Remove -LoadProfile from implementation --- .../host/msh/CommandLineParameterParser.cs | 6 +----- .../resources/ManagedEntranceStrings.resx | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index de5d7215e84..a9553dac016 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -183,7 +183,6 @@ internal class CommandLineParameterParser "file", "help", "inputformat", - "loadprofile", "noexit", "nologo", "noninteractive", @@ -195,6 +194,7 @@ internal class CommandLineParameterParser "windowstyle", "workingdirectory" }; + // login also belongs in the above list but is handled much earlier internal CommandLineParameterParser(PSHostUserInterface hostUI, string bannerText, string helpText) { @@ -721,10 +721,6 @@ private void ParseHelper(string[] args) _noExit = true; noexitSeen = true; } - else if (MatchSwitch(switchKey, "loadprofile", "l")) - { - _skipUserInit = false; - } else if (MatchSwitch(switchKey, "noprofile", "nop")) { _skipUserInit = true; diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx index 074b926838b..e36cb7bd96e 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx @@ -131,8 +131,8 @@ Type 'help' to get help. [-ConfigurationName <string>] [-CustomPipeName <string>] [-EncodedCommand <Base64EncodedCommand>] [-ExecutionPolicy <ExecutionPolicy>] [-InputFormat {Text | XML}] - [-Interactive] [-LoadProfile] [-MTA] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile] - [-OutputFormat {Text | XML}] [-SettingsFile <filePath>] [-STA] [-Version] + [-Interactive] [-Login] [-MTA] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile] + [-OutputFormat {Text | XML}] [-SettingsFile <filePath>] [-STA] [-Version] [-WindowStyle <style>] [-WorkingDirectory <directoryPath>] pwsh[.exe] -h | -Help | -? | /? @@ -293,10 +293,10 @@ All parameters are case-insensitive. Present an interactive prompt to the user. Inverse for NonInteractive parameter. --LoadProfile | -l +-Login | -l - Load the PowerShell profiles. This is the default behavior even if this is - not specified. + Start PowerShell as a login shell, executing the login profiles with /bin/sh. + On Windows this does nothing. -MTA From 95c29a615a9e236de90672cbeb4acc861bf8d1fd Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 09:49:39 -0700 Subject: [PATCH 08/44] Make -Login *nix-specific --- src/powershell/Program.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 1dbb42ba3d1..afb55379952 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -20,10 +20,12 @@ public sealed class ManagedPSEntry /// public static int Main(string[] args) { +#if UNIX if (HasLoginSpecified(args, out int loginIndex)) { return ExecPwshLogin(args, loginIndex); } +#endif return UnmanagedPSEntry.Start(string.Empty, args, args.Length); } From d371d6bad6d71c8e01020800910618216a5871e1 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 09:52:24 -0700 Subject: [PATCH 09/44] Add no-op -Login to Windows --- .../host/msh/CommandLineParameterParser.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index a9553dac016..8b6481564e2 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -183,6 +183,7 @@ internal class CommandLineParameterParser "file", "help", "inputformat", + "login", "noexit", "nologo", "noninteractive", @@ -194,7 +195,6 @@ internal class CommandLineParameterParser "windowstyle", "workingdirectory" }; - // login also belongs in the above list but is handled much earlier internal CommandLineParameterParser(PSHostUserInterface hostUI, string bannerText, string helpText) { @@ -733,6 +733,11 @@ private void ParseHelper(string[] args) { _noInteractive = true; } + else if (MatchSwitch(switchKey, "login", "l")) + { + // This handles -Login on Windows only, where it does nothing. + // On *nix, -Login is handled much earlier to improve startup performance. + } else if (MatchSwitch(switchKey, "socketservermode", "so")) { _socketServerMode = true; From 0571e5a6d849bb450f90bff2e81057f9d592d628 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 11:34:57 -0700 Subject: [PATCH 10/44] Remove loadprofile add login tests --- test/powershell/Host/ConsoleHost.Tests.ps1 | 62 ++++++++++++++-------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index 4bbc1357703..f025d27902d 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -245,42 +245,60 @@ Describe "ConsoleHost unit tests" -tags "Feature" { } } - Context "-LoadProfile Commandline switch" { + Context "-Login pwsh switch" { BeforeAll { - if (Test-Path $profile) { - Remove-Item -Path "$profile.backup" -ErrorAction SilentlyContinue - Rename-Item -Path $profile -NewName "$profile.backup" + $profilePath = "~/.profile" + $backupProfilePath = "profile.bak" + if (Test-Path $profilePath) { + Move-Item -Path $profilePath -Destination $backupProfilePath -Force } - Set-Content -Path $profile -Value "'profile-loaded'" -Force + $envVarName = 'PSTEST_PROFILE_LOAD' + + $guid = New-Guid + + Set-Content -Force -Path $profilePath -Value @" +export $envVarName='$guid' +"@ + + $pwshExe = (Get-Process -Id $PID).Path } AfterAll { - Remove-Item -Path $profile -ErrorAction SilentlyContinue - - if (Test-Path "$profile.backup") { - Rename-Item -Path "$profile.backup" -NewName $profile + if (Test-Path $backupProfilePath) { + Move-Item -Path $backupProfilePath -Destination $profilePath -Force } } - It "Verifies pwsh will accept switch" -TestCases @( - @{ switch = "-l"}, - @{ switch = "-loadprofile"} - ){ - param($switch) + It "Doesn't run the login profile when -Login not used" { + $result = & $pwshExe -Command "`$env:$envVarName" + $result | Should -BeNullOrEmpty + $LASTEXITCODE | Should -Be 0 + } - if (Test-Path $profile) { - & pwsh $switch -command exit | Should -BeExactly "profile-loaded" - } - else { - # In CI, may not be able to write to $profile location, so just verify that the switch is accepted - # and no error message is in the output - & pwsh $switch -command exit *>&1 | Should -BeNullOrEmpty + It "Accepts the -Login switch and behaves correctly" -TestCases @( + @{ LoginSwitch = '-l' } + @{ LoginSwitch = '-L' } + @{ LoginSwitch = '-login' } + @{ LoginSwitch = '-Login' } + @{ LoginSwitch = '-LOGIN' } + @{ LoginSwitch = '-log' } + ) { + param($LoginSwitch) + + $result = & $pwshExe -Command "`$env:$envVarName" + + if ($IsWindows) { + $result | Should -BeNullOrEmpty + $LASTEXITCODE | Should -Be 0 + return } + + $result | Should -Be $guid + $LASTEXITCODE | Should -Be 0 } } - Context "-SettingsFile Commandline switch" { BeforeAll { From 965c0ffce8260f20c675c24a4db491eef836cdda Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 11:42:48 -0700 Subject: [PATCH 11/44] Fix tests --- .../host/msh/CommandLineParameterParser.cs | 3 ++- test/powershell/Host/ConsoleHost.Tests.ps1 | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 8b6481564e2..fae4306c1cb 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -710,7 +710,8 @@ private void ParseHelper(string[] args) _noExit = false; break; } - else if (MatchSwitch(switchKey, "help", "h") || MatchSwitch(switchKey, "?", "?")) + + if (MatchSwitch(switchKey, "help", "h") || MatchSwitch(switchKey, "?", "?")) { _showHelp = true; _showExtendedHelp = true; diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index f025d27902d..7b5d681bef1 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -260,8 +260,6 @@ Describe "ConsoleHost unit tests" -tags "Feature" { Set-Content -Force -Path $profilePath -Value @" export $envVarName='$guid' "@ - - $pwshExe = (Get-Process -Id $PID).Path } AfterAll { @@ -271,7 +269,7 @@ export $envVarName='$guid' } It "Doesn't run the login profile when -Login not used" { - $result = & $pwshExe -Command "`$env:$envVarName" + $result = & $powershell -Command "`$env:$envVarName" $result | Should -BeNullOrEmpty $LASTEXITCODE | Should -Be 0 } @@ -286,7 +284,7 @@ export $envVarName='$guid' ) { param($LoginSwitch) - $result = & $pwshExe -Command "`$env:$envVarName" + $result = & $powershell -Command "`$env:$envVarName" if ($IsWindows) { $result | Should -BeNullOrEmpty From 137f267b21b1fcd642c01472ae098f01a0b89204 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 20:14:24 -0700 Subject: [PATCH 12/44] Use /bin/bash on macOS --- src/powershell/Program.cs | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index afb55379952..4382fbc0ddb 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -40,13 +40,6 @@ private static bool HasLoginSpecified(string[] args, out int loginIndex) loginIndex = -1; // Parameter comparison strings, stackalloc'd for performance - ReadOnlySpan loginParam = stackalloc char[] { 'l', 'o', 'g', 'i', 'n' }; - ReadOnlySpan loginParamUpper = stackalloc char[] { 'L', 'O', 'G', 'I', 'N' }; - ReadOnlySpan fileParam = stackalloc char[] { 'f', 'i', 'l', 'e' }; - ReadOnlySpan fileParamUpper = stackalloc char[] { 'F', 'I', 'L', 'E' }; - ReadOnlySpan commandParam = stackalloc char[] { 'c', 'o', 'm', 'm', 'a', 'n', 'd' }; - ReadOnlySpan commandParamUpper = stackalloc char[] { 'C', 'O', 'M', 'M', 'A', 'N', 'D' }; - for (int i = 0; i < args.Length; i++) { string arg = args[i]; @@ -58,7 +51,7 @@ private static bool HasLoginSpecified(string[] args, out int loginIndex) } // Check for "-Login" or some prefix thereof - if (IsParam(arg, in loginParam, in loginParamUpper)) + if (IsParam(arg, "login", "LOGIN")) { loginIndex = i; return true; @@ -66,8 +59,8 @@ private static bool HasLoginSpecified(string[] args, out int loginIndex) // After -File and -Command, all parameters are passed // to the invoked file or command, so we can stop looking. - if (IsParam(arg, in fileParam, in fileParamUpper) - || IsParam(arg, in commandParam, in commandParamUpper)) + if (IsParam(arg, "file", "FILE") + || IsParam(arg, "command", "COMMAND")) { return false; } @@ -86,8 +79,8 @@ private static bool HasLoginSpecified(string[] args, out int loginIndex) /// private static bool IsParam( string arg, - in ReadOnlySpan paramToCheck, - in ReadOnlySpan paramToCheckUpper) + string paramToCheck, + string paramToCheckUpper) { // Quick fail if the argument is longer than the parameter if (arg.Length > paramToCheck.Length + 1) @@ -96,7 +89,7 @@ private static bool IsParam( } // Check arg chars in order and allow prefixes - for (int i = 1; i < arg.Length - 1; i++) + for (int i = 1; i < arg.Length; i++) { if (arg[i] != paramToCheck[i-1] && arg[i] != paramToCheckUpper[i-1]) @@ -132,11 +125,12 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) // Set up the arguments for /bin/sh var execArgs = new string[args.Length + 4]; + // execArgs[0] is set below to the correct shell executable + // The command arguments - execArgs[0] = "-l"; - execArgs[1] = "-c"; - execArgs[2] = pwshInvocation; - execArgs[3] = ""; // This is required because $@ skips the first argument + execArgs[1] = "-l"; + execArgs[2] = "-c"; + execArgs[3] = pwshInvocation; // Add the arguments passed to pwsh on the end int i = 0; @@ -152,7 +146,14 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) // A null is required by exec execArgs[execArgs.Length - 1] = null; - // Finally exec the /bin/sh command + // On macOS, sh doesn't support login, so we run /bin/bash + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + execArgs[0] = "/bin/bash"; // This is required because $@ skips $0 + return Exec("/bin/bash", execArgs); + } + + execArgs[0] = "/bin/sh"; // This is required because $@ skips $0 return Exec("/bin/sh", execArgs); } From abda72eb82e1631f6b677f18db0d650c26a76aef Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 20:42:43 -0700 Subject: [PATCH 13/44] Fix arguments --- src/powershell/Program.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 4382fbc0ddb..fc94bcf43c2 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -20,6 +20,13 @@ public sealed class ManagedPSEntry /// public static int Main(string[] args) { + /* + Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); + while (!System.Diagnostics.Debugger.IsAttached) + { + System.Threading.Thread.Sleep(500); + } + */ #if UNIX if (HasLoginSpecified(args, out int loginIndex)) { @@ -102,7 +109,7 @@ private static bool IsParam( } /// - /// Create the exec call to /bin/sh -l -c 'pwsh "$@"' and run it. + /// Create the exec call to /bin/{ba}sh -l -c 'pwsh "$@"' and run it. /// /// The argument vector passed to pwsh. /// The index of -Login in the argument vector. @@ -123,18 +130,20 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) CreatePwshInvocation); // Set up the arguments for /bin/sh - var execArgs = new string[args.Length + 4]; + var execArgs = new string[args.Length + 5]; // execArgs[0] is set below to the correct shell executable // The command arguments + execArgs[0] = "-"; // First argument is ignored execArgs[1] = "-l"; execArgs[2] = "-c"; execArgs[3] = pwshInvocation; + execArgs[4] = "-"; // Required since exec ignores $0 // Add the arguments passed to pwsh on the end int i = 0; - int j = 4; + int j = 5; for (; i < args.Length; i++) { if (i == loginArgIndex) { continue; } @@ -149,11 +158,9 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) // On macOS, sh doesn't support login, so we run /bin/bash if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - execArgs[0] = "/bin/bash"; // This is required because $@ skips $0 return Exec("/bin/bash", execArgs); } - execArgs[0] = "/bin/sh"; // This is required because $@ skips $0 return Exec("/bin/sh", execArgs); } From 8638d4e22e487816b2a7bf9fbdd8f02462939648 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 20:49:52 -0700 Subject: [PATCH 14/44] Fix tests --- test/powershell/Host/ConsoleHost.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index 7b5d681bef1..0d1358ff6d2 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -274,7 +274,7 @@ export $envVarName='$guid' $LASTEXITCODE | Should -Be 0 } - It "Accepts the -Login switch and behaves correctly" -TestCases @( + It "Accepts the switch for -Login and behaves correctly" -TestCases @( @{ LoginSwitch = '-l' } @{ LoginSwitch = '-L' } @{ LoginSwitch = '-login' } @@ -284,7 +284,7 @@ export $envVarName='$guid' ) { param($LoginSwitch) - $result = & $powershell -Command "`$env:$envVarName" + $result = & $powershell $LoginSwitch -Command "`$env:$envVarName" if ($IsWindows) { $result | Should -BeNullOrEmpty From 023746e48fdc06f4d5752a9fe333c5bfead44f8e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 3 Jul 2019 20:52:32 -0700 Subject: [PATCH 15/44] Add login methods to UNIX block --- src/powershell/Program.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index fc94bcf43c2..b460951f54a 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -36,6 +36,7 @@ public static int Main(string[] args) return UnmanagedPSEntry.Start(string.Empty, args, args.Length); } +#if UNIX /// /// Checks args to see if -Login has been specified. /// @@ -250,4 +251,5 @@ private static void QuoteAndWriteToSpan(string arg, Span span) SetLastError = true)] private static extern int Exec(string path, string[] args); } +#endif } From e20a16e8290e6da4083f5ae1031747c98540810c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 4 Jul 2019 22:10:47 -0700 Subject: [PATCH 16/44] Fix endif placement --- src/powershell/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index b460951f54a..c24e80c138a 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -250,6 +250,6 @@ private static void QuoteAndWriteToSpan(string arg, Span span) CharSet = CharSet.Ansi, SetLastError = true)] private static extern int Exec(string path, string[] args); - } #endif + } } From 17b63ecafa6e8bff0a4ace84afba2af892aeab2a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 4 Jul 2019 22:12:38 -0700 Subject: [PATCH 17/44] Move login dummy switch to alpha position --- .../host/msh/CommandLineParameterParser.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index fae4306c1cb..6335230d1f6 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -717,6 +717,11 @@ private void ParseHelper(string[] args) _showExtendedHelp = true; _abortStartup = true; } + else if (MatchSwitch(switchKey, "login", "l")) + { + // This handles -Login on Windows only, where it does nothing. + // On *nix, -Login is handled much earlier to improve startup performance. + } else if (MatchSwitch(switchKey, "noexit", "noe")) { _noExit = true; @@ -734,11 +739,6 @@ private void ParseHelper(string[] args) { _noInteractive = true; } - else if (MatchSwitch(switchKey, "login", "l")) - { - // This handles -Login on Windows only, where it does nothing. - // On *nix, -Login is handled much earlier to improve startup performance. - } else if (MatchSwitch(switchKey, "socketservermode", "so")) { _socketServerMode = true; From a8636f21eb0662165fd4c9037afb2f9ec0b4a705 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 4 Jul 2019 22:16:33 -0700 Subject: [PATCH 18/44] Remove debug code --- src/powershell/Program.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index c24e80c138a..c357de1bcb0 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -20,13 +20,6 @@ public sealed class ManagedPSEntry /// public static int Main(string[] args) { - /* - Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); - while (!System.Diagnostics.Debugger.IsAttached) - { - System.Threading.Thread.Sleep(500); - } - */ #if UNIX if (HasLoginSpecified(args, out int loginIndex)) { From 9c49ed1e0b7a447aa0c96b8085b63a607837cba7 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 5 Jul 2019 10:51:23 -0700 Subject: [PATCH 19/44] Use zsh instead of bash on macOS --- src/powershell/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index c357de1bcb0..a5ff2a705b5 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -152,7 +152,7 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) // On macOS, sh doesn't support login, so we run /bin/bash if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - return Exec("/bin/bash", execArgs); + return Exec("/bin/zsh", execArgs); } return Exec("/bin/sh", execArgs); From d09f6c20aaf87dde66230bede83d9c2c9acd8930 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 5 Jul 2019 17:47:17 -0700 Subject: [PATCH 20/44] Switch back to bash --- src/powershell/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index a5ff2a705b5..c357de1bcb0 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -152,7 +152,7 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) // On macOS, sh doesn't support login, so we run /bin/bash if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - return Exec("/bin/zsh", execArgs); + return Exec("/bin/bash", execArgs); } return Exec("/bin/sh", execArgs); From 964eaebb529cac7326c79199cc31f1c42d3e22eb Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 8 Jul 2019 10:52:07 -0700 Subject: [PATCH 21/44] Move back to zsh --- src/powershell/Program.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index c357de1bcb0..7c77b783a1b 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -129,11 +129,14 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) // execArgs[0] is set below to the correct shell executable // The command arguments - execArgs[0] = "-"; // First argument is ignored - execArgs[1] = "-l"; - execArgs[2] = "-c"; - execArgs[3] = pwshInvocation; - execArgs[4] = "-"; // Required since exec ignores $0 + + // First argument is the command name. + // Setting this to /bin/sh enables sh emulation in zsh (which examines $0 to determine how it should behave). + execArgs[0] = "/bin/sh"; + execArgs[1] = "-l"; // Login flag + execArgs[2] = "-c"; // Command parameter + execArgs[3] = pwshInvocation; // Command to execute + execArgs[4] = "-"; // Within the shell, exec ignores $0 // Add the arguments passed to pwsh on the end int i = 0; @@ -149,10 +152,10 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) // A null is required by exec execArgs[execArgs.Length - 1] = null; - // On macOS, sh doesn't support login, so we run /bin/bash + // On macOS, sh doesn't support login, so we run /bin/zsh in sh emulation mode if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - return Exec("/bin/bash", execArgs); + return Exec("/bin/zsh", execArgs); } return Exec("/bin/sh", execArgs); From e97369d7552374c2ee881a89b6b3763eb04a6e47 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 8 Jul 2019 17:16:30 -0700 Subject: [PATCH 22/44] Make arg help consistent with online help --- .../resources/ManagedEntranceStrings.resx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx index e36cb7bd96e..acf466975bc 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx @@ -295,8 +295,9 @@ All parameters are case-insensitive. -Login | -l - Start PowerShell as a login shell, executing the login profiles with /bin/sh. - On Windows this does nothing. + On Linux and macOS, starts PowerShell as a login shell, + using /bin/sh to execute login profiles such as /etc/profile and ~/.profile. + On Windows, this switch does nothing. -MTA From 96594d34308064d2220cb12c6b81ef46febdc56c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 11 Jul 2019 18:45:03 -0700 Subject: [PATCH 23/44] Identify - for login --- src/powershell/Program.cs | 203 ++++++++++++++++++++++++++++++++++---- 1 file changed, 184 insertions(+), 19 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 7c77b783a1b..c37353c48b4 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -4,6 +4,8 @@ using System; using System.Reflection; using System.Runtime.InteropServices; +using System.Text; +using System.IO; namespace Microsoft.PowerShell { @@ -12,6 +14,12 @@ namespace Microsoft.PowerShell /// public sealed class ManagedPSEntry { + // MacOS p/Invoke constants + private const int CTL_KERN = 1; + private const int KERN_ARGMAX = 8; + private const int KERN_PROCARGS2 = 49; + private const int PROC_PIDPATHINFO_MAXSIZE = 4096; + /// /// Starts the managed MSH. /// @@ -21,25 +29,150 @@ public sealed class ManagedPSEntry public static int Main(string[] args) { #if UNIX - if (HasLoginSpecified(args, out int loginIndex)) + System.Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); + while (!System.Diagnostics.Debugger.IsAttached) + { + System.Threading.Thread.Sleep(500); + } + + int returnCode = AttemptExecPwshLogin(args); + if (returnCode < 0) { - return ExecPwshLogin(args, loginIndex); + // TODO: Report error } #endif return UnmanagedPSEntry.Start(string.Empty, args, args.Length); } #if UNIX + private static int AttemptExecPwshLogin(string[] args) + { + bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + byte procNameFirstByte; + bool procStartsWithMinus = false; + string pwshPath; + + if (isLinux) + { + using (FileStream fs = File.OpenRead("/proc/self/cmdline")) + { + procNameFirstByte = (byte)fs.ReadByte(); + } + + switch (procNameFirstByte) + { + case 0x2B: // '+' signifies we have already done login check + return 0; + case 0x2D: // '-' means this is a login shell + procStartsWithMinus = true; + break; + + // For any other char, we check for a login parameter + } + + int loginArgIndex = -1; + if (!procStartsWithMinus && !IsLogin(args, out loginArgIndex)) + { + return 0; + } + + IntPtr linkPathPtr = Marshal.AllocHGlobal(PROC_PIDPATHINFO_MAXSIZE); + ReadLink("/proc/self/exe", linkPathPtr, (UIntPtr)PROC_PIDPATHINFO_MAXSIZE); + pwshPath = Marshal.PtrToStringAnsi(linkPathPtr); + Marshal.FreeHGlobal(linkPathPtr); + + return ExecPwshLogin(args, loginArgIndex, pwshPath, isMacOS: false); + } + + int pid = GetPid(); + + // Set up the mib array and the query for process args size + Span mib = stackalloc int[3]; + int mibLength = 2; + mib[0] = CTL_KERN; + mib[1] = KERN_ARGMAX; + int size = sizeof(int); + int maxargs = 0; + + // Get the process args size + unsafe + { + fixed (int *mibptr = mib) + { + if (SysCtl(mibptr, mibLength, &maxargs, &size, IntPtr.Zero, 0) < 0) + { + throw new Exception("argmax"); + } + } + } + + // Now read the process args into the allocated space + IntPtr procargs = Marshal.AllocHGlobal(maxargs); + IntPtr execPathPtr = IntPtr.Zero; + try + { + size = maxargs; + mib[0] = CTL_KERN; + mib[1] = KERN_PROCARGS2; + mib[2] = pid; + mibLength = 3; + + unsafe + { + fixed (int *mibptr = mib) + { + if (SysCtl(mibptr, mibLength, procargs.ToPointer(), &size, IntPtr.Zero, 0) < 0) + { + throw new Exception("procargs"); + } + } + + // Skip over argc, remember where exec_path is + execPathPtr = IntPtr.Add(procargs, sizeof(int)); + + // Skip over exec_path + byte *argvPtr = (byte *)execPathPtr; + while (*argvPtr != 0) { argvPtr++; } + while (*argvPtr == 0) { argvPtr++; } + + // First char in argv[0] + procNameFirstByte = *argvPtr; + } + + switch (procNameFirstByte) + { + case 0x2B: // '+' signifies we have already done login check + return 0; + case 0x2D: // '-' means this is a login shell + procStartsWithMinus = true; + break; + + // For any other char, we check for a login parameter + } + + int loginArgIndex = -1; + if (!procStartsWithMinus && !IsLogin(args, out loginArgIndex)) + { + return 0; + } + + return ExecPwshLogin(args, loginArgIndex, Marshal.PtrToStringAnsi(execPathPtr), isMacOS: true); + } + finally + { + Marshal.FreeHGlobal(procargs); + } + } + /// /// Checks args to see if -Login has been specified. /// /// Arguments passed to the program. /// The arg index (in argv) where -Login was found. /// - private static bool HasLoginSpecified(string[] args, out int loginIndex) + private static bool IsLogin(string[] args, out int loginIndex) { loginIndex = -1; - // Parameter comparison strings, stackalloc'd for performance for (int i = 0; i < args.Length; i++) { @@ -103,23 +236,22 @@ private static bool IsParam( } /// - /// Create the exec call to /bin/{ba}sh -l -c 'pwsh "$@"' and run it. + /// Create the exec call to /bin/{z}sh -l -c 'exec -a +pwsh pwsh "$@"' and run it. /// /// The argument vector passed to pwsh. /// The index of -Login in the argument vector. + /// True if we are running on macOS. + /// Absolute path to the pwsh executable. /// /// The exit code of exec if it fails. /// If exec succeeds, this process is overwritten so we never actually return. /// - private static int ExecPwshLogin(string[] args, int loginArgIndex) + private static int ExecPwshLogin(string[] args, int loginArgIndex, string pwshPath, bool isMacOS) { - // We need the path to the current pwsh executable - string pwshPath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; - // Create input for /bin/sh that execs pwsh int quotedPwshPathLength = GetQuotedPathLength(pwshPath); string pwshInvocation = string.Create( - 10 + quotedPwshPathLength, // exec '{pwshPath}' "$@" + 19 + quotedPwshPathLength, // exec +pwsh '{pwshPath}' "$@" (pwshPath, quotedPwshPathLength), CreatePwshInvocation); @@ -153,7 +285,7 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex) execArgs[execArgs.Length - 1] = null; // On macOS, sh doesn't support login, so we run /bin/zsh in sh emulation mode - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + if (isMacOS) { return Exec("/bin/zsh", execArgs); } @@ -181,19 +313,28 @@ private static int GetQuotedPathLength(string str) private static void CreatePwshInvocation(Span strBuf, (string path, int quotedLength) pwshPath) { - // "exec " - strBuf[0] = 'e'; - strBuf[1] = 'x'; - strBuf[2] = 'e'; - strBuf[3] = 'c'; - strBuf[4] = ' '; + // "exec -a +pwsh " + strBuf[0] = 'e'; + strBuf[1] = 'x'; + strBuf[2] = 'e'; + strBuf[3] = 'c'; + strBuf[4] = ' '; + strBuf[5] = '-'; + strBuf[6] = 'a'; + strBuf[7] = ' '; + strBuf[8] = '+'; + strBuf[9] = 'p'; + strBuf[10] = 'w'; + strBuf[11] = 's'; + strBuf[12] = 'h'; + strBuf[13] = ' '; // The quoted path to pwsh, like "'/opt/microsoft/powershell/7/pwsh'" - Span pathSpan = strBuf.Slice(5, pwshPath.quotedLength); + Span pathSpan = strBuf.Slice(14, pwshPath.quotedLength); QuoteAndWriteToSpan(pwshPath.path, pathSpan); // ' "$@"' the argument vector splat to pass pwsh arguments through - int argIndex = 5 + pwshPath.quotedLength; + int argIndex = 14 + pwshPath.quotedLength; strBuf[argIndex] = ' '; strBuf[argIndex + 1] = '"'; strBuf[argIndex + 2] = '$'; @@ -246,6 +387,30 @@ private static void QuoteAndWriteToSpan(string arg, Span span) CharSet = CharSet.Ansi, SetLastError = true)] private static extern int Exec(string path, string[] args); + + [DllImport("libc", + EntryPoint = "getpid", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi)] + private static extern int GetPid(); + + [DllImport("libc", + EntryPoint = "sysctl", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi)] + private static unsafe extern int SysCtl(int *mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp); + + [DllImport("libc", + EntryPoint = "proc_pidpath", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi)] + private static extern int ProcPidPath(int pid, IntPtr buf, int buflen); + + [DllImport("libc", + EntryPoint="readlink", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi)] + private static extern IntPtr ReadLink(string pathname, IntPtr buf, UIntPtr size); #endif } } From be073a308501408b056cf7c02016a6df5aff9482 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 11:47:09 -0700 Subject: [PATCH 24/44] Simplify and polish --- src/powershell/Program.cs | 114 +++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 51 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index c37353c48b4..a7d0d7709dc 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -14,6 +14,19 @@ namespace Microsoft.PowerShell /// public sealed class ManagedPSEntry { + private class StartupException : Exception + { + public StartupException(string callName, int exitCode) + { + CallName = callName; + ExitCode = exitCode; + } + + public string CallName { get; } + + public int ExitCode { get; } + } + // MacOS p/Invoke constants private const int CTL_KERN = 1; private const int KERN_ARGMAX = 8; @@ -35,21 +48,16 @@ public static int Main(string[] args) System.Threading.Thread.Sleep(500); } - int returnCode = AttemptExecPwshLogin(args); - if (returnCode < 0) - { - // TODO: Report error - } + AttemptExecPwshLogin(args); #endif return UnmanagedPSEntry.Start(string.Empty, args, args.Length); } #if UNIX - private static int AttemptExecPwshLogin(string[] args) + private static void AttemptExecPwshLogin(string[] args) { bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); byte procNameFirstByte; - bool procStartsWithMinus = false; string pwshPath; if (isLinux) @@ -59,21 +67,9 @@ private static int AttemptExecPwshLogin(string[] args) procNameFirstByte = (byte)fs.ReadByte(); } - switch (procNameFirstByte) + if (!IsLogin(procNameFirstByte, args, out int loginArgIndex)) { - case 0x2B: // '+' signifies we have already done login check - return 0; - case 0x2D: // '-' means this is a login shell - procStartsWithMinus = true; - break; - - // For any other char, we check for a login parameter - } - - int loginArgIndex = -1; - if (!procStartsWithMinus && !IsLogin(args, out loginArgIndex)) - { - return 0; + return; } IntPtr linkPathPtr = Marshal.AllocHGlobal(PROC_PIDPATHINFO_MAXSIZE); @@ -81,37 +77,38 @@ private static int AttemptExecPwshLogin(string[] args) pwshPath = Marshal.PtrToStringAnsi(linkPathPtr); Marshal.FreeHGlobal(linkPathPtr); - return ExecPwshLogin(args, loginArgIndex, pwshPath, isMacOS: false); + // Attempt to exec pwsh + ThrowIfFails("exec", ExecPwshLogin(args, loginArgIndex, pwshPath, isMacOS: false)); + return; } - int pid = GetPid(); + // At this point, we are on macOS - // Set up the mib array and the query for process args size + // Set up the mib array and the query for process maximum args size Span mib = stackalloc int[3]; int mibLength = 2; mib[0] = CTL_KERN; mib[1] = KERN_ARGMAX; - int size = sizeof(int); - int maxargs = 0; + int size = IntPtr.Size / 2; + int argmax = 0; // Get the process args size unsafe { fixed (int *mibptr = mib) { - if (SysCtl(mibptr, mibLength, &maxargs, &size, IntPtr.Zero, 0) < 0) - { - throw new Exception("argmax"); - } + ThrowIfFails(nameof(argmax), SysCtl(mibptr, mibLength, &argmax, &size, IntPtr.Zero, 0)); } } + // Get the PID so we can query this process' args + int pid = GetPid(); + // Now read the process args into the allocated space - IntPtr procargs = Marshal.AllocHGlobal(maxargs); + IntPtr procargs = Marshal.AllocHGlobal(argmax); IntPtr execPathPtr = IntPtr.Zero; try { - size = maxargs; mib[0] = CTL_KERN; mib[1] = KERN_PROCARGS2; mib[2] = pid; @@ -121,10 +118,7 @@ private static int AttemptExecPwshLogin(string[] args) { fixed (int *mibptr = mib) { - if (SysCtl(mibptr, mibLength, procargs.ToPointer(), &size, IntPtr.Zero, 0) < 0) - { - throw new Exception("procargs"); - } + ThrowIfFails(nameof(procargs), SysCtl(mibptr, mibLength, procargs.ToPointer(), &argmax, IntPtr.Zero, 0)); } // Skip over argc, remember where exec_path is @@ -139,24 +133,16 @@ private static int AttemptExecPwshLogin(string[] args) procNameFirstByte = *argvPtr; } - switch (procNameFirstByte) + if (!IsLogin(procNameFirstByte, args, out int loginArgIndex)) { - case 0x2B: // '+' signifies we have already done login check - return 0; - case 0x2D: // '-' means this is a login shell - procStartsWithMinus = true; - break; - - // For any other char, we check for a login parameter + return; } - int loginArgIndex = -1; - if (!procStartsWithMinus && !IsLogin(args, out loginArgIndex)) - { - return 0; - } + // Get the pwshPath from exec_path + pwshPath = Marshal.PtrToStringAnsi(execPathPtr); - return ExecPwshLogin(args, loginArgIndex, Marshal.PtrToStringAnsi(execPathPtr), isMacOS: true); + // Attempt to exec pwsh + ThrowIfFails("exec", ExecPwshLogin(args, loginArgIndex, pwshPath, isMacOS: true)); } finally { @@ -167,12 +153,30 @@ private static int AttemptExecPwshLogin(string[] args) /// /// Checks args to see if -Login has been specified. /// + /// The first byte of the name of the currently running process. /// Arguments passed to the program. /// The arg index (in argv) where -Login was found. /// - private static bool IsLogin(string[] args, out int loginIndex) + private static bool IsLogin( + byte procNameFirstByte, + string[] args, + out int loginIndex) { loginIndex = -1; + + switch (procNameFirstByte) + { + // '+' signifies we have already done login check + case 0x2B: + return false; + + // '-' means this is a login shell + case 0x2D: + return true; + + // For any other char, we check for a login parameter + } + // Parameter comparison strings, stackalloc'd for performance for (int i = 0; i < args.Length; i++) { @@ -370,6 +374,14 @@ private static void QuoteAndWriteToSpan(string arg, Span span) span[j] = '\''; } + private static void ThrowIfFails(string call, int code) + { + if (code < 0) + { + throw new StartupException(call, code); + } + } + /// /// The `execv` syscall we use to exec /bin/sh. /// From 3b15bba56c53090f0baad403926bd0ad8844f944 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 11:47:28 -0700 Subject: [PATCH 25/44] Remove unused p/invoke --- src/powershell/Program.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index a7d0d7709dc..f4cd1564696 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -412,12 +412,6 @@ private static void ThrowIfFails(string call, int code) CharSet = CharSet.Ansi)] private static unsafe extern int SysCtl(int *mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp); - [DllImport("libc", - EntryPoint = "proc_pidpath", - CallingConvention = CallingConvention.Cdecl, - CharSet = CharSet.Ansi)] - private static extern int ProcPidPath(int pid, IntPtr buf, int buflen); - [DllImport("libc", EntryPoint="readlink", CallingConvention = CallingConvention.Cdecl, From 8cf5c8a49bf5ed7831884aed497dedfc51e2443f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 11:48:46 -0700 Subject: [PATCH 26/44] Remove unsupported exec -a use --- src/powershell/Program.cs | 65 +++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index f4cd1564696..20f5a03ede1 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -254,9 +254,15 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex, string pwshPa { // Create input for /bin/sh that execs pwsh int quotedPwshPathLength = GetQuotedPathLength(pwshPath); + + // /bin/sh does not support the exec -a feature + int pwshExecInvocationLength = isMacOS + ? quotedPwshPathLength + 19 // exec -a +pwsh '{pwshPath}' "$@" + : quotedPwshPathLength + 10; // exec '{pwshPath}' "$@" + string pwshInvocation = string.Create( - 19 + quotedPwshPathLength, // exec +pwsh '{pwshPath}' "$@" - (pwshPath, quotedPwshPathLength), + pwshExecInvocationLength, + (pwshPath, quotedPwshPathLength, isMacOS), CreatePwshInvocation); // Set up the arguments for /bin/sh @@ -272,7 +278,7 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex, string pwshPa execArgs[1] = "-l"; // Login flag execArgs[2] = "-c"; // Command parameter execArgs[3] = pwshInvocation; // Command to execute - execArgs[4] = "-"; // Within the shell, exec ignores $0 + execArgs[4] = ""; // Within the shell, exec ignores $0 // Add the arguments passed to pwsh on the end int i = 0; @@ -315,35 +321,42 @@ private static int GetQuotedPathLength(string str) return length; } - private static void CreatePwshInvocation(Span strBuf, (string path, int quotedLength) pwshPath) + private static void CreatePwshInvocation( + Span strBuf, + (string path, int quotedLength, bool supportsDashA) invocationInfo) { // "exec -a +pwsh " - strBuf[0] = 'e'; - strBuf[1] = 'x'; - strBuf[2] = 'e'; - strBuf[3] = 'c'; - strBuf[4] = ' '; - strBuf[5] = '-'; - strBuf[6] = 'a'; - strBuf[7] = ' '; - strBuf[8] = '+'; - strBuf[9] = 'p'; - strBuf[10] = 'w'; - strBuf[11] = 's'; - strBuf[12] = 'h'; - strBuf[13] = ' '; + int i = 0; + strBuf[i++] = 'e'; + strBuf[i++] = 'x'; + strBuf[i++] = 'e'; + strBuf[i++] = 'c'; + strBuf[i++] = ' '; + + if (invocationInfo.supportsDashA) + { + strBuf[i++] = '-'; + strBuf[i++] = 'a'; + strBuf[i++] = ' '; + strBuf[i++] = '+'; + strBuf[i++] = 'p'; + strBuf[i++] = 'w'; + strBuf[i++] = 's'; + strBuf[i++] = 'h'; + strBuf[i++] = ' '; + } // The quoted path to pwsh, like "'/opt/microsoft/powershell/7/pwsh'" - Span pathSpan = strBuf.Slice(14, pwshPath.quotedLength); - QuoteAndWriteToSpan(pwshPath.path, pathSpan); + Span pathSpan = strBuf.Slice(i, invocationInfo.quotedLength); + QuoteAndWriteToSpan(invocationInfo.path, pathSpan); + i += invocationInfo.quotedLength // ' "$@"' the argument vector splat to pass pwsh arguments through - int argIndex = 14 + pwshPath.quotedLength; - strBuf[argIndex] = ' '; - strBuf[argIndex + 1] = '"'; - strBuf[argIndex + 2] = '$'; - strBuf[argIndex + 3] = '@'; - strBuf[argIndex + 4] = '"'; + strBuf[i++] = ' '; + strBuf[i++] = '"'; + strBuf[i++] = '$'; + strBuf[i++] = '@'; + strBuf[i++] = '"'; } /// From c123a4f56e2c3d507f500802645b59ec50754cdf Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 12:19:21 -0700 Subject: [PATCH 27/44] Fixup calls and comments --- src/powershell/Program.cs | 115 +++++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 26 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 20f5a03ede1..3d5d7011e73 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -14,24 +14,41 @@ namespace Microsoft.PowerShell /// public sealed class ManagedPSEntry { + /// + /// Exception to signify an early startup failure. + /// private class StartupException : Exception { + /// + /// Construct a new startup exception instance. + /// + /// The name of the native call that failed. + /// The exit code the native call returned. public StartupException(string callName, int exitCode) { CallName = callName; ExitCode = exitCode; } + /// + /// The name of the native call that failed. + /// public string CallName { get; } + /// + /// The exit code returned by the failed native call. + /// public int ExitCode { get; } } + // Linux p/Invoke constants + private const int LINUX_PATH_MAX = 4096; + // MacOS p/Invoke constants - private const int CTL_KERN = 1; - private const int KERN_ARGMAX = 8; - private const int KERN_PROCARGS2 = 49; - private const int PROC_PIDPATHINFO_MAXSIZE = 4096; + private const int MACOS_CTL_KERN = 1; + private const int MACOS_KERN_ARGMAX = 8; + private const int MACOS_KERN_PROCARGS2 = 49; + private const int MACOS_PROC_PIDPATHINFO_MAXSIZE = 4096; /// /// Starts the managed MSH. @@ -42,42 +59,51 @@ public StartupException(string callName, int exitCode) public static int Main(string[] args) { #if UNIX - System.Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); - while (!System.Diagnostics.Debugger.IsAttached) - { - System.Threading.Thread.Sleep(500); - } - AttemptExecPwshLogin(args); #endif return UnmanagedPSEntry.Start(string.Empty, args, args.Length); } #if UNIX + /// + /// Checks whether pwsh has been started as a login shell + /// and if so, proceeds with the login process. + /// This method will return early if pwsh was not started as a login shell + /// and will throw if it detects a native call has failed. + /// In the event of success, we use an exec() call, so this method never returns. + /// + /// The startup arguments to pwsh. private static void AttemptExecPwshLogin(string[] args) { bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + // The first byte (ASCII char) of the name of this process, used to detect '-' for login byte procNameFirstByte; + // The path to the executable this process was started from string pwshPath; + // On Linux, we can simply use the /proc filesystem if (isLinux) { + // Read the process name byte using (FileStream fs = File.OpenRead("/proc/self/cmdline")) { procNameFirstByte = (byte)fs.ReadByte(); } + // Run login detection logic if (!IsLogin(procNameFirstByte, args, out int loginArgIndex)) { return; } - IntPtr linkPathPtr = Marshal.AllocHGlobal(PROC_PIDPATHINFO_MAXSIZE); - ReadLink("/proc/self/exe", linkPathPtr, (UIntPtr)PROC_PIDPATHINFO_MAXSIZE); - pwshPath = Marshal.PtrToStringAnsi(linkPathPtr); + // Read the symlink to the startup executable + IntPtr linkPathPtr = Marshal.AllocHGlobal(LINUX_PATH_MAX); + IntPtr size = ReadLink("/proc/self/exe", linkPathPtr, (UIntPtr)LINUX_PATH_MAX); + pwshPath = Marshal.PtrToStringAnsi(linkPathPtr, (int)size); Marshal.FreeHGlobal(linkPathPtr); - // Attempt to exec pwsh + // exec pwsh ThrowIfFails("exec", ExecPwshLogin(args, loginArgIndex, pwshPath, isMacOS: false)); return; } @@ -87,8 +113,8 @@ private static void AttemptExecPwshLogin(string[] args) // Set up the mib array and the query for process maximum args size Span mib = stackalloc int[3]; int mibLength = 2; - mib[0] = CTL_KERN; - mib[1] = KERN_ARGMAX; + mib[0] = MACOS_CTL_KERN; + mib[1] = MACOS_KERN_ARGMAX; int size = IntPtr.Size / 2; int argmax = 0; @@ -109,8 +135,8 @@ private static void AttemptExecPwshLogin(string[] args) IntPtr execPathPtr = IntPtr.Zero; try { - mib[0] = CTL_KERN; - mib[1] = KERN_PROCARGS2; + mib[0] = MACOS_CTL_KERN; + mib[1] = MACOS_KERN_PROCARGS2; mib[2] = pid; mibLength = 3; @@ -141,7 +167,7 @@ private static void AttemptExecPwshLogin(string[] args) // Get the pwshPath from exec_path pwshPath = Marshal.PtrToStringAnsi(execPathPtr); - // Attempt to exec pwsh + // exec pwsh ThrowIfFails("exec", ExecPwshLogin(args, loginArgIndex, pwshPath, isMacOS: true)); } finally @@ -321,11 +347,19 @@ private static int GetQuotedPathLength(string str) return length; } + /// + /// Implements a SpanAction<T> for string.Create() + /// that builds the shell invocation for the login pwsh session. + /// + /// The buffer of the string to be created. + /// The unquoted pwsh path. + /// The length the pwsh path will have when it's quoted. + /// Indicates whether the `exec` builtin supports "-a" to change the process name. private static void CreatePwshInvocation( Span strBuf, (string path, int quotedLength, bool supportsDashA) invocationInfo) { - // "exec -a +pwsh " + // "exec " int i = 0; strBuf[i++] = 'e'; strBuf[i++] = 'x'; @@ -335,6 +369,8 @@ private static void CreatePwshInvocation( if (invocationInfo.supportsDashA) { + // "-a +pwsh " + // We use this where -a is supported to prevent a second login check strBuf[i++] = '-'; strBuf[i++] = 'a'; strBuf[i++] = ' '; @@ -387,6 +423,11 @@ private static void QuoteAndWriteToSpan(string arg, Span span) span[j] = '\''; } + /// + /// If the given exit code is negative, throws a StartupException. + /// + /// The native call that was attempted. + /// The exit code it returned. private static void ThrowIfFails(string call, int code) { if (code < 0) @@ -413,23 +454,45 @@ private static void ThrowIfFails(string call, int code) SetLastError = true)] private static extern int Exec(string path, string[] args); + /// + /// The `readlink` syscall we use to read the symlink from /proc/self/exe + /// to get the executable path of pwsh on Linux. + /// + /// The path to the symlink to read. + /// Pointer to a buffer to fill with the result. + /// The size of the buffer we have supplied. + /// The number of bytes placed in the buffer. [DllImport("libc", - EntryPoint = "getpid", + EntryPoint="readlink", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - private static extern int GetPid(); + private static extern IntPtr ReadLink(string pathname, IntPtr buf, UIntPtr size); + /// + /// The `getpid` POSIX syscall we use to quickly get the current process PID on macOS. + /// + /// The pid of the current process. [DllImport("libc", - EntryPoint = "sysctl", + EntryPoint = "getpid", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - private static unsafe extern int SysCtl(int *mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp); + private static extern int GetPid(); + /// + /// The `sysctl` BSD sycall used to get system information on macOS. + /// + /// The Management Information Base name, used to query information. + /// The length of the MIB name. + /// The object passed out of sysctl (may be null) + /// The size of the object passed out of sysctl. + /// The object passed in to sysctl. + /// The length of the object passed in to sysctl. + /// [DllImport("libc", - EntryPoint="readlink", + EntryPoint = "sysctl", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - private static extern IntPtr ReadLink(string pathname, IntPtr buf, UIntPtr size); + private static unsafe extern int SysCtl(int *mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp); #endif } } From 3353537749bd96b29e88b6f05826ad92fc6e89de Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 12:27:15 -0700 Subject: [PATCH 28/44] Add test for -pwsh login invocation --- test/powershell/Host/ConsoleHost.Tests.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index 0d1358ff6d2..2997b5a6468 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -295,6 +295,13 @@ export $envVarName='$guid' $result | Should -Be $guid $LASTEXITCODE | Should -Be 0 } + + It "Starts as a login shell with '-' prepended to name" -Skip:([bool](Get-Command -Name bash -ErrorAction Ignore)) { + $quoteEscapedPwsh = $powershell.Replace("'", "''") + $result = bash -c "exec -a +pwsh '$quoteEscapedPwsh' -Command '`$env:$envVarName'" + $result | Should -Be $guid + $LASTEXITCODE | Should -Be 0 # Exit code will be PowerShell's since it was exec'd + } } Context "-SettingsFile Commandline switch" { From d2423405f9795cacb503e65310021a8b9e11bf52 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 12:31:32 -0700 Subject: [PATCH 29/44] Fix syntax and comment errors --- src/powershell/Program.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 3d5d7011e73..5144402b862 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -2,10 +2,9 @@ // Licensed under the MIT License. using System; +using System.IO; using System.Reflection; using System.Runtime.InteropServices; -using System.Text; -using System.IO; namespace Microsoft.PowerShell { @@ -99,8 +98,8 @@ private static void AttemptExecPwshLogin(string[] args) // Read the symlink to the startup executable IntPtr linkPathPtr = Marshal.AllocHGlobal(LINUX_PATH_MAX); - IntPtr size = ReadLink("/proc/self/exe", linkPathPtr, (UIntPtr)LINUX_PATH_MAX); - pwshPath = Marshal.PtrToStringAnsi(linkPathPtr, (int)size); + IntPtr bufSize = ReadLink("/proc/self/exe", linkPathPtr, (UIntPtr)LINUX_PATH_MAX); + pwshPath = Marshal.PtrToStringAnsi(linkPathPtr, (int)bufSize); Marshal.FreeHGlobal(linkPathPtr); // exec pwsh @@ -352,9 +351,7 @@ private static int GetQuotedPathLength(string str) /// that builds the shell invocation for the login pwsh session. /// /// The buffer of the string to be created. - /// The unquoted pwsh path. - /// The length the pwsh path will have when it's quoted. - /// Indicates whether the `exec` builtin supports "-a" to change the process name. + /// Information used to build the required string. private static void CreatePwshInvocation( Span strBuf, (string path, int quotedLength, bool supportsDashA) invocationInfo) @@ -385,7 +382,7 @@ private static void CreatePwshInvocation( // The quoted path to pwsh, like "'/opt/microsoft/powershell/7/pwsh'" Span pathSpan = strBuf.Slice(i, invocationInfo.quotedLength); QuoteAndWriteToSpan(invocationInfo.path, pathSpan); - i += invocationInfo.quotedLength + i += invocationInfo.quotedLength; // ' "$@"' the argument vector splat to pass pwsh arguments through strBuf[i++] = ' '; @@ -481,10 +478,10 @@ private static void ThrowIfFails(string call, int code) /// /// The `sysctl` BSD sycall used to get system information on macOS. /// - /// The Management Information Base name, used to query information. + /// The Management Information Base name, used to query information. /// The length of the MIB name. - /// The object passed out of sysctl (may be null) - /// The size of the object passed out of sysctl. + /// The object passed out of sysctl (may be null) + /// The size of the object passed out of sysctl. /// The object passed in to sysctl. /// The length of the object passed in to sysctl. /// From 7b0e08ed7561c9c0adbdf4339571c5286d4a9ccc Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 14:25:52 -0700 Subject: [PATCH 30/44] Add tests --- test/powershell/Host/ConsoleHost.Tests.ps1 | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index 2997b5a6468..a956db330a6 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -274,6 +274,18 @@ export $envVarName='$guid' $LASTEXITCODE | Should -Be 0 } + It "Doesn't falsely recognise -Login when elsewhere in the invocation" { + $result = & $powershell -nop -c 'Write-Output "-login"' + $result | Should -Be '-login' + $LASTEXITCODE | Should -Be 0 + } + + It "Doesn't falsely recognise -Login when used after -Command" { + $result = & $powershell -nop -c 'Write-Output' -Login + $result | Should -Be '-login' + $LASTEXITCODE | Should -Be 0 + } + It "Accepts the switch for -Login and behaves correctly" -TestCases @( @{ LoginSwitch = '-l' } @{ LoginSwitch = '-L' } @@ -284,7 +296,7 @@ export $envVarName='$guid' ) { param($LoginSwitch) - $result = & $powershell $LoginSwitch -Command "`$env:$envVarName" + $result = & $powershell -NoProfile $LoginSwitch -Command "`$env:$envVarName" if ($IsWindows) { $result | Should -BeNullOrEmpty @@ -296,9 +308,11 @@ export $envVarName='$guid' $LASTEXITCODE | Should -Be 0 } - It "Starts as a login shell with '-' prepended to name" -Skip:([bool](Get-Command -Name bash -ErrorAction Ignore)) { - $quoteEscapedPwsh = $powershell.Replace("'", "''") - $result = bash -c "exec -a +pwsh '$quoteEscapedPwsh' -Command '`$env:$envVarName'" + It "Starts as a login shell with '-' prepended to name" -Skip:(-not (Get-Command -Name /bin/bash -ErrorAction Ignore)) { + $quoteEscapedPwsh = $powershell.Replace("'", "\'") + $pwshCommand = "`$env:$envVarName" + $bashCommand = "exec -a '-pwsh' '$quoteEscapedPwsh' -NoProfile -Command '`$env:$envVarName' ''" + $result = /bin/bash -c $bashCommand $result | Should -Be $guid $LASTEXITCODE | Should -Be 0 # Exit code will be PowerShell's since it was exec'd } From 6e2937ae6c70c8dc02e746051c2823e9f363bada Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 14:34:16 -0700 Subject: [PATCH 31/44] Address @SteveL-MSFT's comments --- test/powershell/Host/ConsoleHost.Tests.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index a956db330a6..c4ea932a2a3 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -276,13 +276,13 @@ export $envVarName='$guid' It "Doesn't falsely recognise -Login when elsewhere in the invocation" { $result = & $powershell -nop -c 'Write-Output "-login"' - $result | Should -Be '-login' + $result | Should -BeExactly '-login' $LASTEXITCODE | Should -Be 0 } It "Doesn't falsely recognise -Login when used after -Command" { $result = & $powershell -nop -c 'Write-Output' -Login - $result | Should -Be '-login' + $result | Should -BeExactly '-login' $LASTEXITCODE | Should -Be 0 } @@ -304,7 +304,7 @@ export $envVarName='$guid' return } - $result | Should -Be $guid + $result | Should -BeExactly $guid $LASTEXITCODE | Should -Be 0 } @@ -313,7 +313,7 @@ export $envVarName='$guid' $pwshCommand = "`$env:$envVarName" $bashCommand = "exec -a '-pwsh' '$quoteEscapedPwsh' -NoProfile -Command '`$env:$envVarName' ''" $result = /bin/bash -c $bashCommand - $result | Should -Be $guid + $result | Should -BeExactly $guid $LASTEXITCODE | Should -Be 0 # Exit code will be PowerShell's since it was exec'd } } From b78528ae76e3076f878660d02d40fcbea09ef6b3 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 14:46:07 -0700 Subject: [PATCH 32/44] ifdef exception/constants --- src/powershell/Program.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 5144402b862..6c346dbcd0c 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -18,6 +18,7 @@ public sealed class ManagedPSEntry /// private class StartupException : Exception { +#if UNIX /// /// Construct a new startup exception instance. /// @@ -48,6 +49,7 @@ public StartupException(string callName, int exitCode) private const int MACOS_KERN_ARGMAX = 8; private const int MACOS_KERN_PROCARGS2 = 49; private const int MACOS_PROC_PIDPATHINFO_MAXSIZE = 4096; +#endif /// /// Starts the managed MSH. From 9371bd0e63d8abd2e5d4944206ba54a46ff44dd8 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 15:06:35 -0700 Subject: [PATCH 33/44] Fix casing --- test/powershell/Host/ConsoleHost.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index c4ea932a2a3..e81fc162b8f 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -282,7 +282,7 @@ export $envVarName='$guid' It "Doesn't falsely recognise -Login when used after -Command" { $result = & $powershell -nop -c 'Write-Output' -Login - $result | Should -BeExactly '-login' + $result | Should -BeExactly '-Login' $LASTEXITCODE | Should -Be 0 } From 6f54eada20ba0bca2052bedbd3508aedfc36f56f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 12 Jul 2019 15:26:13 -0700 Subject: [PATCH 34/44] Fix syntax error --- src/powershell/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 6c346dbcd0c..3e25b9f9824 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -13,12 +13,12 @@ namespace Microsoft.PowerShell /// public sealed class ManagedPSEntry { +#if UNIX /// /// Exception to signify an early startup failure. /// private class StartupException : Exception { -#if UNIX /// /// Construct a new startup exception instance. /// From 581d7e0ea8c40660af1879f4eeda351738995558 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 7 Aug 2019 21:45:50 -0700 Subject: [PATCH 35/44] Use env var to prevent needless login check --- src/powershell/Program.cs | 148 +++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 72 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 3e25b9f9824..8d34098978b 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -41,6 +41,10 @@ public StartupException(string callName, int exitCode) public int ExitCode { get; } } + // Environment variable used to short circuit second login check + private const string LOGIN_ENV_VAR_NAME = "__PWSH_LOGIN_CHECKED"; + private const string LOGIN_ENV_VAR_VALUE = "1"; + // Linux p/Invoke constants private const int LINUX_PATH_MAX = 4096; @@ -76,6 +80,13 @@ public static int Main(string[] args) /// The startup arguments to pwsh. private static void AttemptExecPwshLogin(string[] args) { + // If the login environment variable is set, we have already done the login logic and have been exec'd + if (Environment.GetEnvironmentVariable(LOGIN_ENV_VAR_NAME) != null) + { + Environment.SetEnvironmentVariable(LOGIN_ENV_VAR_NAME, null); + return; + } + bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); // The first byte (ASCII char) of the name of this process, used to detect '-' for login @@ -93,7 +104,7 @@ private static void AttemptExecPwshLogin(string[] args) } // Run login detection logic - if (!IsLogin(procNameFirstByte, args, out int loginArgIndex)) + if (!IsLogin(procNameFirstByte, args)) { return; } @@ -105,7 +116,7 @@ private static void AttemptExecPwshLogin(string[] args) Marshal.FreeHGlobal(linkPathPtr); // exec pwsh - ThrowIfFails("exec", ExecPwshLogin(args, loginArgIndex, pwshPath, isMacOS: false)); + ThrowIfFails("exec", ExecPwshLogin(args, pwshPath, isMacOS: false)); return; } @@ -131,6 +142,8 @@ private static void AttemptExecPwshLogin(string[] args) // Get the PID so we can query this process' args int pid = GetPid(); + // The following logic is based on https://gist.github.com/nonowarn/770696 + // Now read the process args into the allocated space IntPtr procargs = Marshal.AllocHGlobal(argmax); IntPtr execPathPtr = IntPtr.Zero; @@ -148,7 +161,25 @@ private static void AttemptExecPwshLogin(string[] args) ThrowIfFails(nameof(procargs), SysCtl(mibptr, mibLength, procargs.ToPointer(), &argmax, IntPtr.Zero, 0)); } - // Skip over argc, remember where exec_path is + // The memory block we're reading is a series of null-terminated strings + // that looks something like this: + // + // | argc | + // | exec_path | ... \0 + // | argv[0] | ... \0 + // | argv[1] | ... \0 + // ... + // + // We care about argv[0], since that's the name the process was started with + // If argv[0][0] == '-', we have been invoked as login. + // Doing this, the buffer we populated also recorded `exec_path`, + // which is the path to our executable. + // We can reuse this value later to prevent needing to call a .NET API + // to generate our exec invocation. + + + // We don't care about argc's value, since argv[0] must always exist + // Skip over argc, but remember where exec_path is for later execPathPtr = IntPtr.Add(procargs, sizeof(int)); // Skip over exec_path @@ -160,7 +191,7 @@ private static void AttemptExecPwshLogin(string[] args) procNameFirstByte = *argvPtr; } - if (!IsLogin(procNameFirstByte, args, out int loginArgIndex)) + if (!IsLogin(procNameFirstByte, args)) { return; } @@ -169,7 +200,7 @@ private static void AttemptExecPwshLogin(string[] args) pwshPath = Marshal.PtrToStringAnsi(execPathPtr); // exec pwsh - ThrowIfFails("exec", ExecPwshLogin(args, loginArgIndex, pwshPath, isMacOS: true)); + ThrowIfFails("exec", ExecPwshLogin(args, pwshPath, isMacOS: true)); } finally { @@ -182,26 +213,15 @@ private static void AttemptExecPwshLogin(string[] args) /// /// The first byte of the name of the currently running process. /// Arguments passed to the program. - /// The arg index (in argv) where -Login was found. /// private static bool IsLogin( byte procNameFirstByte, - string[] args, - out int loginIndex) + string[] args) { - loginIndex = -1; - - switch (procNameFirstByte) + // Process name starting with '-' means this is a login shell + if (procNameFirstByte == 0x2D) { - // '+' signifies we have already done login check - case 0x2B: - return false; - - // '-' means this is a login shell - case 0x2D: - return true; - - // For any other char, we check for a login parameter + return true; } // Parameter comparison strings, stackalloc'd for performance @@ -218,14 +238,14 @@ private static bool IsLogin( // Check for "-Login" or some prefix thereof if (IsParam(arg, "login", "LOGIN")) { - loginIndex = i; return true; } - // After -File and -Command, all parameters are passed + // After -File, -Command and -Version, all parameters are passed // to the invoked file or command, so we can stop looking. if (IsParam(arg, "file", "FILE") - || IsParam(arg, "command", "COMMAND")) + || IsParam(arg, "command", "COMMAND") + || IsParam(arg, "version", "VERSION")) { return false; } @@ -270,30 +290,25 @@ private static bool IsParam( /// Create the exec call to /bin/{z}sh -l -c 'exec -a +pwsh pwsh "$@"' and run it. /// /// The argument vector passed to pwsh. - /// The index of -Login in the argument vector. /// True if we are running on macOS. /// Absolute path to the pwsh executable. /// /// The exit code of exec if it fails. /// If exec succeeds, this process is overwritten so we never actually return. /// - private static int ExecPwshLogin(string[] args, int loginArgIndex, string pwshPath, bool isMacOS) + private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS) { // Create input for /bin/sh that execs pwsh int quotedPwshPathLength = GetQuotedPathLength(pwshPath); - // /bin/sh does not support the exec -a feature - int pwshExecInvocationLength = isMacOS - ? quotedPwshPathLength + 19 // exec -a +pwsh '{pwshPath}' "$@" - : quotedPwshPathLength + 10; // exec '{pwshPath}' "$@" - string pwshInvocation = string.Create( - pwshExecInvocationLength, - (pwshPath, quotedPwshPathLength, isMacOS), + quotedPwshPathLength + 10, // exec '{pwshPath}' "$@" + (pwshPath, quotedPwshPathLength), CreatePwshInvocation); // Set up the arguments for /bin/sh - var execArgs = new string[args.Length + 5]; + // We need to add the 5 /bin/sh invocation parts, plus 1 null terminator at the end + var execArgs = new string[args.Length + 6]; // execArgs[0] is set below to the correct shell executable @@ -308,19 +323,13 @@ private static int ExecPwshLogin(string[] args, int loginArgIndex, string pwshPa execArgs[4] = ""; // Within the shell, exec ignores $0 // Add the arguments passed to pwsh on the end - int i = 0; - int j = 5; - for (; i < args.Length; i++) - { - if (i == loginArgIndex) { continue; } - - execArgs[j] = args[i]; - j++; - } + args.CopyTo(execArgs, 5); // A null is required by exec execArgs[execArgs.Length - 1] = null; + ThrowIfFails("setenv", SetEnv(LOGIN_ENV_VAR_NAME, LOGIN_ENV_VAR_VALUE, overwrite: true)); + // On macOS, sh doesn't support login, so we run /bin/zsh in sh emulation mode if (isMacOS) { @@ -356,42 +365,22 @@ private static int GetQuotedPathLength(string str) /// Information used to build the required string. private static void CreatePwshInvocation( Span strBuf, - (string path, int quotedLength, bool supportsDashA) invocationInfo) + (string path, int quotedLength) invocationInfo) { // "exec " - int i = 0; - strBuf[i++] = 'e'; - strBuf[i++] = 'x'; - strBuf[i++] = 'e'; - strBuf[i++] = 'c'; - strBuf[i++] = ' '; + string prefix = "exec "; + prefix.AsSpan().CopyTo(strBuf); - if (invocationInfo.supportsDashA) - { - // "-a +pwsh " - // We use this where -a is supported to prevent a second login check - strBuf[i++] = '-'; - strBuf[i++] = 'a'; - strBuf[i++] = ' '; - strBuf[i++] = '+'; - strBuf[i++] = 'p'; - strBuf[i++] = 'w'; - strBuf[i++] = 's'; - strBuf[i++] = 'h'; - strBuf[i++] = ' '; - } - // The quoted path to pwsh, like "'/opt/microsoft/powershell/7/pwsh'" + int i = prefix.Length; Span pathSpan = strBuf.Slice(i, invocationInfo.quotedLength); QuoteAndWriteToSpan(invocationInfo.path, pathSpan); i += invocationInfo.quotedLength; // ' "$@"' the argument vector splat to pass pwsh arguments through - strBuf[i++] = ' '; - strBuf[i++] = '"'; - strBuf[i++] = '$'; - strBuf[i++] = '@'; - strBuf[i++] = '"'; + string suffix = " \"$@\""; + Span bufSuffix = strBuf.Slice(i); + suffix.AsSpan().CopyTo(bufSuffix); } /// @@ -431,12 +420,14 @@ private static void ThrowIfFails(string call, int code) { if (code < 0) { + code = Marshal.GetLastWin32Error(); + Console.Error.WriteLine($"Call to '{call}' failed with errno {code}"); throw new StartupException(call, code); } } /// - /// The `execv` syscall we use to exec /bin/sh. + /// The `execv` POSIX syscall we use to exec /bin/sh. /// /// The path to the executable to exec. /// @@ -454,7 +445,7 @@ private static void ThrowIfFails(string call, int code) private static extern int Exec(string path, string[] args); /// - /// The `readlink` syscall we use to read the symlink from /proc/self/exe + /// The `readlink` POSIX syscall we use to read the symlink from /proc/self/exe /// to get the executable path of pwsh on Linux. /// /// The path to the symlink to read. @@ -462,7 +453,7 @@ private static void ThrowIfFails(string call, int code) /// The size of the buffer we have supplied. /// The number of bytes placed in the buffer. [DllImport("libc", - EntryPoint="readlink", + EntryPoint = "readlink", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private static extern IntPtr ReadLink(string pathname, IntPtr buf, UIntPtr size); @@ -477,6 +468,19 @@ private static void ThrowIfFails(string call, int code) CharSet = CharSet.Ansi)] private static extern int GetPid(); + /// + /// The `setenv` POSIX syscall used to set an environment variable in the process. + /// + /// The name of the environment variable. + /// The value of the environment variable. + /// If true, will overwrite an existing environment variable of the same name. + /// 0 if successful, -1 on error. errno indicates the reason for failure. + [DllImport("libc", + EntryPoint = "setenv", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi)] + private static extern int SetEnv(string name, string value, bool overwrite); + /// /// The `sysctl` BSD sycall used to get system information on macOS. /// From 336dc8932eef5e84ef83b98996819e83bc8f8a6c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 7 Aug 2019 21:49:53 -0700 Subject: [PATCH 36/44] Address @daxian-dbw's comments --- src/powershell/Program.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 8d34098978b..62072b024a1 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -116,7 +116,7 @@ private static void AttemptExecPwshLogin(string[] args) Marshal.FreeHGlobal(linkPathPtr); // exec pwsh - ThrowIfFails("exec", ExecPwshLogin(args, pwshPath, isMacOS: false)); + ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: false)); return; } @@ -135,7 +135,7 @@ private static void AttemptExecPwshLogin(string[] args) { fixed (int *mibptr = mib) { - ThrowIfFails(nameof(argmax), SysCtl(mibptr, mibLength, &argmax, &size, IntPtr.Zero, 0)); + ThrowOnFailure(nameof(argmax), SysCtl(mibptr, mibLength, &argmax, &size, IntPtr.Zero, 0)); } } @@ -158,7 +158,7 @@ private static void AttemptExecPwshLogin(string[] args) { fixed (int *mibptr = mib) { - ThrowIfFails(nameof(procargs), SysCtl(mibptr, mibLength, procargs.ToPointer(), &argmax, IntPtr.Zero, 0)); + ThrowOnFailure(nameof(procargs), SysCtl(mibptr, mibLength, procargs.ToPointer(), &argmax, IntPtr.Zero, 0)); } // The memory block we're reading is a series of null-terminated strings @@ -200,7 +200,7 @@ private static void AttemptExecPwshLogin(string[] args) pwshPath = Marshal.PtrToStringAnsi(execPathPtr); // exec pwsh - ThrowIfFails("exec", ExecPwshLogin(args, pwshPath, isMacOS: true)); + ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: true)); } finally { @@ -315,8 +315,10 @@ private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS) // The command arguments // First argument is the command name. - // Setting this to /bin/sh enables sh emulation in zsh (which examines $0 to determine how it should behave). + // Even when executing zsh, we want to set this to /bin/sh + // because this tells zsh to run in sh emulation mode (it examines $0) execArgs[0] = "/bin/sh"; + execArgs[1] = "-l"; // Login flag execArgs[2] = "-c"; // Command parameter execArgs[3] = pwshInvocation; // Command to execute @@ -328,7 +330,7 @@ private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS) // A null is required by exec execArgs[execArgs.Length - 1] = null; - ThrowIfFails("setenv", SetEnv(LOGIN_ENV_VAR_NAME, LOGIN_ENV_VAR_VALUE, overwrite: true)); + ThrowOnFailure("setenv", SetEnv(LOGIN_ENV_VAR_NAME, LOGIN_ENV_VAR_VALUE, overwrite: true)); // On macOS, sh doesn't support login, so we run /bin/zsh in sh emulation mode if (isMacOS) @@ -416,7 +418,7 @@ private static void QuoteAndWriteToSpan(string arg, Span span) /// /// The native call that was attempted. /// The exit code it returned. - private static void ThrowIfFails(string call, int code) + private static void ThrowOnFailure(string call, int code) { if (code < 0) { From 09c424a01fd4f41e77c84959f31dbbe5d47fe04f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 8 Aug 2019 09:18:14 -0700 Subject: [PATCH 37/44] Add comment about setenv use --- src/powershell/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 62072b024a1..c41df6526ab 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -310,8 +310,6 @@ private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS) // We need to add the 5 /bin/sh invocation parts, plus 1 null terminator at the end var execArgs = new string[args.Length + 6]; - // execArgs[0] is set below to the correct shell executable - // The command arguments // First argument is the command name. @@ -330,6 +328,8 @@ private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS) // A null is required by exec execArgs[execArgs.Length - 1] = null; + // We can't use Environment.SetEnvironmentVariable() here + // See https://github.com/dotnet/corefx/issues/40130#issuecomment-519420648 ThrowOnFailure("setenv", SetEnv(LOGIN_ENV_VAR_NAME, LOGIN_ENV_VAR_VALUE, overwrite: true)); // On macOS, sh doesn't support login, so we run /bin/zsh in sh emulation mode From 147d2a788c5ec01a92aa5bd1ee0f493f300a1d02 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 8 Aug 2019 09:46:53 -0700 Subject: [PATCH 38/44] Add -Help exclusion --- src/powershell/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index c41df6526ab..669ff3f7561 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -245,7 +245,8 @@ private static bool IsLogin( // to the invoked file or command, so we can stop looking. if (IsParam(arg, "file", "FILE") || IsParam(arg, "command", "COMMAND") - || IsParam(arg, "version", "VERSION")) + || IsParam(arg, "version", "VERSION") + || IsParam(arg, "help", "HELP")) { return false; } From 826d94589daf6ad2d1572ac8f783c99bc2edae11 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 8 Aug 2019 10:15:27 -0700 Subject: [PATCH 39/44] Add detection of /? --- src/powershell/Program.cs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 669ff3f7561..2d1c1effe0a 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -224,29 +224,45 @@ private static bool IsLogin( return true; } - // Parameter comparison strings, stackalloc'd for performance + // Check the parameters in order for (int i = 0; i < args.Length; i++) { string arg = args[i]; - // Must look like '-' - if (arg == null || arg.Length < 2 || arg[0] != '-') + if (arg == null || arg.Length < 2) { continue; } + // '/?' is equivalent to '-help' + if (arg.Equals("/?")) + { + return false; + } + + // Must look like '-' + if (arg[0] != '-') + { + continue; + } + // Check for "-Login" or some prefix thereof if (IsParam(arg, "login", "LOGIN")) { + // It's possible that we might see -Help or -Version + // after -Login and do the login only to print help. + // However, this just causes a slow down and still works. + // In exchange, we speed up the login detection scenario in normal cases return true; } - // After -File, -Command and -Version, all parameters are passed + // After -File, -Command, -Version, -Help and -?, all parameters are passed // to the invoked file or command, so we can stop looking. if (IsParam(arg, "file", "FILE") || IsParam(arg, "command", "COMMAND") || IsParam(arg, "version", "VERSION") - || IsParam(arg, "help", "HELP")) + || IsParam(arg, "help", "HELP") + || IsParam(arg, "?", "?")) { return false; } From 50429fb11260c5f8ded6e17725fad06716e9ee89 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 8 Aug 2019 11:57:39 -0700 Subject: [PATCH 40/44] Update comments a bit --- src/powershell/Program.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 2d1c1effe0a..02965e3aa53 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -170,15 +170,15 @@ private static void AttemptExecPwshLogin(string[] args) // | argv[1] | ... \0 // ... // - // We care about argv[0], since that's the name the process was started with + // We care about argv[0], since that's the name the process was started with. // If argv[0][0] == '-', we have been invoked as login. // Doing this, the buffer we populated also recorded `exec_path`, - // which is the path to our executable. + // which is the path to our executable `pwsh`. // We can reuse this value later to prevent needing to call a .NET API // to generate our exec invocation. - // We don't care about argc's value, since argv[0] must always exist + // We don't care about argc's value, since argv[0] must always exist. // Skip over argc, but remember where exec_path is for later execPathPtr = IntPtr.Add(procargs, sizeof(int)); @@ -323,15 +323,15 @@ private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS) (pwshPath, quotedPwshPathLength), CreatePwshInvocation); - // Set up the arguments for /bin/sh - // We need to add the 5 /bin/sh invocation parts, plus 1 null terminator at the end + // Set up the arguments for '/bin/sh'. + // We need to add 5 slots for the '/bin/sh' invocation parts, plus 1 slot for the null terminator at the end var execArgs = new string[args.Length + 6]; // The command arguments // First argument is the command name. - // Even when executing zsh, we want to set this to /bin/sh - // because this tells zsh to run in sh emulation mode (it examines $0) + // Even when executing 'zsh', we want to set this to '/bin/sh' + // because this tells 'zsh' to run in sh emulation mode (it examines $0) execArgs[0] = "/bin/sh"; execArgs[1] = "-l"; // Login flag @@ -339,17 +339,17 @@ private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS) execArgs[3] = pwshInvocation; // Command to execute execArgs[4] = ""; // Within the shell, exec ignores $0 - // Add the arguments passed to pwsh on the end + // Add the arguments passed to pwsh on the end. args.CopyTo(execArgs, 5); - // A null is required by exec + // A null is required by exec. execArgs[execArgs.Length - 1] = null; - // We can't use Environment.SetEnvironmentVariable() here - // See https://github.com/dotnet/corefx/issues/40130#issuecomment-519420648 + // We can't use Environment.SetEnvironmentVariable() here. + // See https://github.com/dotnet/corefx/issues/40130#issuecomment-519420648. ThrowOnFailure("setenv", SetEnv(LOGIN_ENV_VAR_NAME, LOGIN_ENV_VAR_VALUE, overwrite: true)); - // On macOS, sh doesn't support login, so we run /bin/zsh in sh emulation mode + // On macOS, sh doesn't support login, so we run /bin/zsh in sh emulation mode. if (isMacOS) { return Exec("/bin/zsh", execArgs); From 07e248cec2d9da22b7382786b15aab97028ce82c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 8 Aug 2019 14:13:59 -0700 Subject: [PATCH 41/44] Amend login logic to read only first argument --- src/powershell/Program.cs | 83 ++++++++++++++------------------------- 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 02965e3aa53..22141f07e6e 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -146,7 +146,7 @@ private static void AttemptExecPwshLogin(string[] args) // Now read the process args into the allocated space IntPtr procargs = Marshal.AllocHGlobal(argmax); - IntPtr execPathPtr = IntPtr.Zero; + IntPtr executablePathPtr = IntPtr.Zero; try { mib[0] = MACOS_CTL_KERN; @@ -180,10 +180,10 @@ private static void AttemptExecPwshLogin(string[] args) // We don't care about argc's value, since argv[0] must always exist. // Skip over argc, but remember where exec_path is for later - execPathPtr = IntPtr.Add(procargs, sizeof(int)); + executablePathPtr = IntPtr.Add(procargs, sizeof(int)); // Skip over exec_path - byte *argvPtr = (byte *)execPathPtr; + byte *argvPtr = (byte *)executablePathPtr; while (*argvPtr != 0) { argvPtr++; } while (*argvPtr == 0) { argvPtr++; } @@ -197,7 +197,7 @@ private static void AttemptExecPwshLogin(string[] args) } // Get the pwshPath from exec_path - pwshPath = Marshal.PtrToStringAnsi(execPathPtr); + pwshPath = Marshal.PtrToStringAnsi(executablePathPtr); // exec pwsh ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: true)); @@ -224,51 +224,12 @@ private static bool IsLogin( return true; } - // Check the parameters in order - for (int i = 0; i < args.Length; i++) - { - string arg = args[i]; - - if (arg == null || arg.Length < 2) - { - continue; - } - - // '/?' is equivalent to '-help' - if (arg.Equals("/?")) - { - return false; - } - - // Must look like '-' - if (arg[0] != '-') - { - continue; - } - - // Check for "-Login" or some prefix thereof - if (IsParam(arg, "login", "LOGIN")) - { - // It's possible that we might see -Help or -Version - // after -Login and do the login only to print help. - // However, this just causes a slow down and still works. - // In exchange, we speed up the login detection scenario in normal cases - return true; - } - - // After -File, -Command, -Version, -Help and -?, all parameters are passed - // to the invoked file or command, so we can stop looking. - if (IsParam(arg, "file", "FILE") - || IsParam(arg, "command", "COMMAND") - || IsParam(arg, "version", "VERSION") - || IsParam(arg, "help", "HELP") - || IsParam(arg, "?", "?")) - { - return false; - } - } - - return false; + // Look at the first parameter to see if it is -Login + // NOTE: -Login is only supported as the first parameter to PowerShell + return args.Length > 0 + && args[0].Length > 1 + && args[0][0] == '-' + && IsParam(args[0], "login", "LOGIN"); } /// @@ -337,7 +298,17 @@ private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS) execArgs[1] = "-l"; // Login flag execArgs[2] = "-c"; // Command parameter execArgs[3] = pwshInvocation; // Command to execute - execArgs[4] = ""; // Within the shell, exec ignores $0 + + // The /bin/sh option spec looks like: + // sh -c command_string [command_name [argument...]] + // We must provide a command_name before arguments, + // but this is never used since "$@" takes argv[1] - argv[n] + // and the `exec` builtin provides its own argv[0]. + // See https://pubs.opengroup.org/onlinepubs/9699919799.2016edition/ + // + // Since command_name is ignored and we can't use null (it's the terminator) + // we use empty string + execArgs[4] = ""; // Add the arguments passed to pwsh on the end. args.CopyTo(execArgs, 5); @@ -474,7 +445,8 @@ private static void ThrowOnFailure(string call, int code) [DllImport("libc", EntryPoint = "readlink", CallingConvention = CallingConvention.Cdecl, - CharSet = CharSet.Ansi)] + CharSet = CharSet.Ansi, + SetLastError = true)] private static extern IntPtr ReadLink(string pathname, IntPtr buf, UIntPtr size); /// @@ -484,7 +456,8 @@ private static void ThrowOnFailure(string call, int code) [DllImport("libc", EntryPoint = "getpid", CallingConvention = CallingConvention.Cdecl, - CharSet = CharSet.Ansi)] + CharSet = CharSet.Ansi, + SetLastError = true)] private static extern int GetPid(); /// @@ -497,7 +470,8 @@ private static void ThrowOnFailure(string call, int code) [DllImport("libc", EntryPoint = "setenv", CallingConvention = CallingConvention.Cdecl, - CharSet = CharSet.Ansi)] + CharSet = CharSet.Ansi, + SetLastError = true)] private static extern int SetEnv(string name, string value, bool overwrite); /// @@ -513,7 +487,8 @@ private static void ThrowOnFailure(string call, int code) [DllImport("libc", EntryPoint = "sysctl", CallingConvention = CallingConvention.Cdecl, - CharSet = CharSet.Ansi)] + CharSet = CharSet.Ansi, + SetLastError = true)] private static unsafe extern int SysCtl(int *mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp); #endif } From 1d8e1c5f427f972ea7d3fbc29cdb16e9c6b371a8 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 8 Aug 2019 14:33:29 -0700 Subject: [PATCH 42/44] Update a stale comment about `exec -a` --- src/powershell/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 22141f07e6e..aab368c8194 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -265,7 +265,7 @@ private static bool IsParam( } /// - /// Create the exec call to /bin/{z}sh -l -c 'exec -a +pwsh pwsh "$@"' and run it. + /// Create the exec call to /bin/{z}sh -l -c 'exec pwsh "$@"' and run it. /// /// The argument vector passed to pwsh. /// True if we are running on macOS. From da0f75fd1f1f9bd2d7bc2ae1a11d0fa261dd971b Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 8 Aug 2019 14:36:52 -0700 Subject: [PATCH 43/44] Update tests --- test/powershell/Host/ConsoleHost.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index e81fc162b8f..f5547d4cabd 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -296,7 +296,7 @@ export $envVarName='$guid' ) { param($LoginSwitch) - $result = & $powershell -NoProfile $LoginSwitch -Command "`$env:$envVarName" + $result = & $powershell $LoginSwitch -NoProfile -Command "`$env:$envVarName" if ($IsWindows) { $result | Should -BeNullOrEmpty From 4b1f5214bf8bd6fd5dc80185f32665512bf9c690 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 14 Aug 2019 11:10:05 -0700 Subject: [PATCH 44/44] Address remaining comments --- .../resources/ManagedEntranceStrings.resx | 6 ++++-- src/powershell/Program.cs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx index acf466975bc..2b27cb19a4b 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx @@ -125,13 +125,13 @@ https://aka.ms/powershell Type 'help' to get help. - Usage: pwsh[.exe] [[-File] <filePath> [args]] + Usage: pwsh[.exe] [-Login] [[-File] <filePath> [args]] [-Command { - | <script-block> [-args <arg-array>] | <string> [<CommandParameters>] } ] [-ConfigurationName <string>] [-CustomPipeName <string>] [-EncodedCommand <Base64EncodedCommand>] [-ExecutionPolicy <ExecutionPolicy>] [-InputFormat {Text | XML}] - [-Interactive] [-Login] [-MTA] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile] + [-Interactive] [-MTA] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile] [-OutputFormat {Text | XML}] [-SettingsFile <filePath>] [-STA] [-Version] [-WindowStyle <style>] [-WorkingDirectory <directoryPath>] @@ -299,6 +299,8 @@ All parameters are case-insensitive. using /bin/sh to execute login profiles such as /etc/profile and ~/.profile. On Windows, this switch does nothing. + Note that "-Login" is only supported as the first parameter to pwsh. + -MTA Start the shell using a multi-threaded apartment. diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index aab368c8194..99834e6a95f 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -91,6 +91,7 @@ private static void AttemptExecPwshLogin(string[] args) // The first byte (ASCII char) of the name of this process, used to detect '-' for login byte procNameFirstByte; + // The path to the executable this process was started from string pwshPath;