mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
pwsh -Login support (#10050)
This commit is contained in:
@@ -183,7 +183,7 @@ namespace Microsoft.PowerShell
|
||||
"file",
|
||||
"help",
|
||||
"inputformat",
|
||||
"loadprofile",
|
||||
"login",
|
||||
"noexit",
|
||||
"nologo",
|
||||
"noninteractive",
|
||||
@@ -710,21 +710,23 @@ namespace Microsoft.PowerShell
|
||||
_noExit = false;
|
||||
break;
|
||||
}
|
||||
else if (MatchSwitch(switchKey, "help", "h") || MatchSwitch(switchKey, "?", "?"))
|
||||
|
||||
if (MatchSwitch(switchKey, "help", "h") || MatchSwitch(switchKey, "?", "?"))
|
||||
{
|
||||
_showHelp = true;
|
||||
_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;
|
||||
noexitSeen = true;
|
||||
}
|
||||
else if (MatchSwitch(switchKey, "loadprofile", "l"))
|
||||
{
|
||||
_skipUserInit = false;
|
||||
}
|
||||
else if (MatchSwitch(switchKey, "noprofile", "nop"))
|
||||
{
|
||||
_skipUserInit = true;
|
||||
|
||||
@@ -125,14 +125,14 @@ https://aka.ms/powershell
|
||||
Type 'help' to get help.</value>
|
||||
</data>
|
||||
<data name="UsageHelp" xml:space="preserve">
|
||||
<value>Usage: pwsh[.exe] [[-File] <filePath> [args]]
|
||||
<value>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] [-LoadProfile] [-MTA] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile]
|
||||
[-OutputFormat {Text | XML}] [-SettingsFile <filePath>] [-STA] [-Version]
|
||||
[-Interactive] [-MTA] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile]
|
||||
[-OutputFormat {Text | XML}] [-SettingsFile <filePath>] [-STA] [-Version]
|
||||
[-WindowStyle <style>] [-WorkingDirectory <directoryPath>]
|
||||
|
||||
pwsh[.exe] -h | -Help | -? | /?
|
||||
@@ -293,10 +293,13 @@ All parameters are case-insensitive.</value>
|
||||
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.
|
||||
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.
|
||||
|
||||
Note that "-Login" is only supported as the first parameter to pwsh.
|
||||
|
||||
-MTA
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.PowerShell
|
||||
{
|
||||
@@ -11,6 +13,48 @@ namespace Microsoft.PowerShell
|
||||
/// </summary>
|
||||
public sealed class ManagedPSEntry
|
||||
{
|
||||
#if UNIX
|
||||
/// <summary>
|
||||
/// Exception to signify an early startup failure.
|
||||
/// </summary>
|
||||
private class StartupException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a new startup exception instance.
|
||||
/// </summary>
|
||||
/// <param name="callName">The name of the native call that failed.</param>
|
||||
/// <param name="exitCode">The exit code the native call returned.</param>
|
||||
public StartupException(string callName, int exitCode)
|
||||
{
|
||||
CallName = callName;
|
||||
ExitCode = exitCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the native call that failed.
|
||||
/// </summary>
|
||||
public string CallName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The exit code returned by the failed native call.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
// MacOS p/Invoke constants
|
||||
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;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Starts the managed MSH.
|
||||
/// </summary>
|
||||
@@ -19,7 +63,434 @@ namespace Microsoft.PowerShell
|
||||
/// </param>
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
#if UNIX
|
||||
AttemptExecPwshLogin(args);
|
||||
#endif
|
||||
return UnmanagedPSEntry.Start(string.Empty, args, args.Length);
|
||||
}
|
||||
|
||||
#if UNIX
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="args">The startup arguments to pwsh.</param>
|
||||
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
|
||||
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))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the symlink to the startup executable
|
||||
IntPtr linkPathPtr = Marshal.AllocHGlobal(LINUX_PATH_MAX);
|
||||
IntPtr bufSize = ReadLink("/proc/self/exe", linkPathPtr, (UIntPtr)LINUX_PATH_MAX);
|
||||
pwshPath = Marshal.PtrToStringAnsi(linkPathPtr, (int)bufSize);
|
||||
Marshal.FreeHGlobal(linkPathPtr);
|
||||
|
||||
// exec pwsh
|
||||
ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: false));
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point, we are on macOS
|
||||
|
||||
// Set up the mib array and the query for process maximum args size
|
||||
Span<int> mib = stackalloc int[3];
|
||||
int mibLength = 2;
|
||||
mib[0] = MACOS_CTL_KERN;
|
||||
mib[1] = MACOS_KERN_ARGMAX;
|
||||
int size = IntPtr.Size / 2;
|
||||
int argmax = 0;
|
||||
|
||||
// Get the process args size
|
||||
unsafe
|
||||
{
|
||||
fixed (int *mibptr = mib)
|
||||
{
|
||||
ThrowOnFailure(nameof(argmax), SysCtl(mibptr, mibLength, &argmax, &size, IntPtr.Zero, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// 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 executablePathPtr = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
mib[0] = MACOS_CTL_KERN;
|
||||
mib[1] = MACOS_KERN_PROCARGS2;
|
||||
mib[2] = pid;
|
||||
mibLength = 3;
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (int *mibptr = mib)
|
||||
{
|
||||
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
|
||||
// that looks something like this:
|
||||
//
|
||||
// | argc | <int>
|
||||
// | 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 `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.
|
||||
// Skip over argc, but remember where exec_path is for later
|
||||
executablePathPtr = IntPtr.Add(procargs, sizeof(int));
|
||||
|
||||
// Skip over exec_path
|
||||
byte *argvPtr = (byte *)executablePathPtr;
|
||||
while (*argvPtr != 0) { argvPtr++; }
|
||||
while (*argvPtr == 0) { argvPtr++; }
|
||||
|
||||
// First char in argv[0]
|
||||
procNameFirstByte = *argvPtr;
|
||||
}
|
||||
|
||||
if (!IsLogin(procNameFirstByte, args))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the pwshPath from exec_path
|
||||
pwshPath = Marshal.PtrToStringAnsi(executablePathPtr);
|
||||
|
||||
// exec pwsh
|
||||
ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: true));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(procargs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks args to see if -Login has been specified.
|
||||
/// </summary>
|
||||
/// <param name="procNameFirstByte">The first byte of the name of the currently running process.</param>
|
||||
/// <param name="args">Arguments passed to the program.</param>
|
||||
/// <returns></returns>
|
||||
private static bool IsLogin(
|
||||
byte procNameFirstByte,
|
||||
string[] args)
|
||||
{
|
||||
// Process name starting with '-' means this is a login shell
|
||||
if (procNameFirstByte == 0x2D)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a given parameter is the one we're looking for.
|
||||
/// Assumes any prefix determines that parameter (true for -l, -c and -f).
|
||||
/// </summary>
|
||||
/// <param name="arg">The argument to check.</param>
|
||||
/// <param name="paramToCheck">The lowercase name of the parameter to check.</param>
|
||||
/// <param name="paramToCheckUpper">The uppercase name of the parameter to check.</param>
|
||||
/// <returns></returns>
|
||||
private static bool IsParam(
|
||||
string arg,
|
||||
string paramToCheck,
|
||||
string 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; i++)
|
||||
{
|
||||
if (arg[i] != paramToCheck[i-1]
|
||||
&& arg[i] != paramToCheckUpper[i-1])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the exec call to /bin/{z}sh -l -c 'exec pwsh "$@"' and run it.
|
||||
/// </summary>
|
||||
/// <param name="args">The argument vector passed to pwsh.</param>
|
||||
/// <param name="isMacOS">True if we are running on macOS.</param>
|
||||
/// <param name="pwshPath">Absolute path to the pwsh executable.</param>
|
||||
/// <returns>
|
||||
/// The exit code of exec if it fails.
|
||||
/// If exec succeeds, this process is overwritten so we never actually return.
|
||||
/// </returns>
|
||||
private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS)
|
||||
{
|
||||
// Create input for /bin/sh that execs pwsh
|
||||
int quotedPwshPathLength = GetQuotedPathLength(pwshPath);
|
||||
|
||||
string pwshInvocation = string.Create(
|
||||
quotedPwshPathLength + 10, // exec '{pwshPath}' "$@"
|
||||
(pwshPath, quotedPwshPathLength),
|
||||
CreatePwshInvocation);
|
||||
|
||||
// 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)
|
||||
execArgs[0] = "/bin/sh";
|
||||
|
||||
execArgs[1] = "-l"; // Login flag
|
||||
execArgs[2] = "-c"; // Command parameter
|
||||
execArgs[3] = pwshInvocation; // Command to execute
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
if (isMacOS)
|
||||
{
|
||||
return Exec("/bin/zsh", execArgs);
|
||||
}
|
||||
|
||||
return Exec("/bin/sh", execArgs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets what the length of the given string will be if it's
|
||||
/// quote escaped for /bin/sh.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to quote escape.</param>
|
||||
/// <returns>The length of the string when it's quote escaped.</returns>
|
||||
private static int GetQuotedPathLength(string str)
|
||||
{
|
||||
int length = 2;
|
||||
foreach (char c in str)
|
||||
{
|
||||
length++;
|
||||
if (c == '\'') { length++; }
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements a SpanAction<T> for string.Create()
|
||||
/// that builds the shell invocation for the login pwsh session.
|
||||
/// </summary>
|
||||
/// <param name="strBuf">The buffer of the string to be created.</param>
|
||||
/// <param name="invocationInfo">Information used to build the required string.</param>
|
||||
private static void CreatePwshInvocation(
|
||||
Span<char> strBuf,
|
||||
(string path, int quotedLength) invocationInfo)
|
||||
{
|
||||
// "exec "
|
||||
string prefix = "exec ";
|
||||
prefix.AsSpan().CopyTo(strBuf);
|
||||
|
||||
// The quoted path to pwsh, like "'/opt/microsoft/powershell/7/pwsh'"
|
||||
int i = prefix.Length;
|
||||
Span<char> pathSpan = strBuf.Slice(i, invocationInfo.quotedLength);
|
||||
QuoteAndWriteToSpan(invocationInfo.path, pathSpan);
|
||||
i += invocationInfo.quotedLength;
|
||||
|
||||
// ' "$@"' the argument vector splat to pass pwsh arguments through
|
||||
string suffix = " \"$@\"";
|
||||
Span<char> bufSuffix = strBuf.Slice(i);
|
||||
suffix.AsSpan().CopyTo(bufSuffix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quotes (and sh quote escapes) a string and writes it to the given span.
|
||||
/// </summary>
|
||||
/// <param name="arg">The string to quote.</param>
|
||||
/// <param name="span">The span to write to.</param>
|
||||
private static void QuoteAndWriteToSpan(string arg, Span<char> span)
|
||||
{
|
||||
span[0] = '\'';
|
||||
|
||||
int i = 0;
|
||||
int j = 1;
|
||||
for (; i < arg.Length; i++, j++)
|
||||
{
|
||||
char c = arg[i];
|
||||
|
||||
if (c == '\'')
|
||||
{
|
||||
// /bin/sh quote escaping uses backslashes
|
||||
span[j] = '\\';
|
||||
j++;
|
||||
}
|
||||
|
||||
span[j] = c;
|
||||
}
|
||||
|
||||
span[j] = '\'';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the given exit code is negative, throws a StartupException.
|
||||
/// </summary>
|
||||
/// <param name="call">The native call that was attempted.</param>
|
||||
/// <param name="code">The exit code it returned.</param>
|
||||
private static void ThrowOnFailure(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The `execv` POSIX syscall we use to exec /bin/sh.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the executable to exec.</param>
|
||||
/// <param name="args">
|
||||
/// The arguments to send through to the executable.
|
||||
/// Array must have its final element be null.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// An exit code if exec failed, but if successful the calling process will be overwritten.
|
||||
/// </returns>
|
||||
[DllImport("libc",
|
||||
EntryPoint = "execv",
|
||||
CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi,
|
||||
SetLastError = true)]
|
||||
private static extern int Exec(string path, string[] args);
|
||||
|
||||
/// <summary>
|
||||
/// The `readlink` POSIX syscall we use to read the symlink from /proc/self/exe
|
||||
/// to get the executable path of pwsh on Linux.
|
||||
/// </summary>
|
||||
/// <param name="pathname">The path to the symlink to read.</param>
|
||||
/// <param name="buf">Pointer to a buffer to fill with the result.</param>
|
||||
/// <param name="size">The size of the buffer we have supplied.</param>
|
||||
/// <returns>The number of bytes placed in the buffer.</returns>
|
||||
[DllImport("libc",
|
||||
EntryPoint = "readlink",
|
||||
CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi,
|
||||
SetLastError = true)]
|
||||
private static extern IntPtr ReadLink(string pathname, IntPtr buf, UIntPtr size);
|
||||
|
||||
/// <summary>
|
||||
/// The `getpid` POSIX syscall we use to quickly get the current process PID on macOS.
|
||||
/// </summary>
|
||||
/// <returns>The pid of the current process.</returns>
|
||||
[DllImport("libc",
|
||||
EntryPoint = "getpid",
|
||||
CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi,
|
||||
SetLastError = true)]
|
||||
private static extern int GetPid();
|
||||
|
||||
/// <summary>
|
||||
/// The `setenv` POSIX syscall used to set an environment variable in the process.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the environment variable.</param>
|
||||
/// <param name="value">The value of the environment variable.</param>
|
||||
/// <param name="overwrite">If true, will overwrite an existing environment variable of the same name.</param>
|
||||
/// <returns>0 if successful, -1 on error. errno indicates the reason for failure.</returns>
|
||||
[DllImport("libc",
|
||||
EntryPoint = "setenv",
|
||||
CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi,
|
||||
SetLastError = true)]
|
||||
private static extern int SetEnv(string name, string value, bool overwrite);
|
||||
|
||||
/// <summary>
|
||||
/// The `sysctl` BSD sycall used to get system information on macOS.
|
||||
/// </summary>
|
||||
/// <param name="mib">The Management Information Base name, used to query information.</param>
|
||||
/// <param name="mibLength">The length of the MIB name.</param>
|
||||
/// <param name="oldp">The object passed out of sysctl (may be null)</param>
|
||||
/// <param name="oldlenp">The size of the object passed out of sysctl.</param>
|
||||
/// <param name="newp">The object passed in to sysctl.</param>
|
||||
/// <param name="newlenp">The length of the object passed in to sysctl.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("libc",
|
||||
EntryPoint = "sysctl",
|
||||
CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi,
|
||||
SetLastError = true)]
|
||||
private static unsafe extern int SysCtl(int *mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,42 +245,79 @@ 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'
|
||||
"@
|
||||
}
|
||||
|
||||
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> switch" -TestCases @(
|
||||
@{ switch = "-l"},
|
||||
@{ switch = "-loadprofile"}
|
||||
){
|
||||
param($switch)
|
||||
It "Doesn't run the login profile when -Login not used" {
|
||||
$result = & $powershell -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 "Doesn't falsely recognise -Login when elsewhere in the invocation" {
|
||||
$result = & $powershell -nop -c 'Write-Output "-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 -BeExactly '-Login'
|
||||
$LASTEXITCODE | Should -Be 0
|
||||
}
|
||||
|
||||
It "Accepts the <LoginSwitch> switch for -Login and behaves correctly" -TestCases @(
|
||||
@{ LoginSwitch = '-l' }
|
||||
@{ LoginSwitch = '-L' }
|
||||
@{ LoginSwitch = '-login' }
|
||||
@{ LoginSwitch = '-Login' }
|
||||
@{ LoginSwitch = '-LOGIN' }
|
||||
@{ LoginSwitch = '-log' }
|
||||
) {
|
||||
param($LoginSwitch)
|
||||
|
||||
$result = & $powershell $LoginSwitch -NoProfile -Command "`$env:$envVarName"
|
||||
|
||||
if ($IsWindows) {
|
||||
$result | Should -BeNullOrEmpty
|
||||
$LASTEXITCODE | Should -Be 0
|
||||
return
|
||||
}
|
||||
|
||||
$result | Should -BeExactly $guid
|
||||
$LASTEXITCODE | Should -Be 0
|
||||
}
|
||||
|
||||
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 -BeExactly $guid
|
||||
$LASTEXITCODE | Should -Be 0 # Exit code will be PowerShell's since it was exec'd
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Context "-SettingsFile Commandline switch" {
|
||||
|
||||
BeforeAll {
|
||||
|
||||
Reference in New Issue
Block a user