MegaBites-AI/Windows-powershell
0308
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4#nullable enable5 6using System;7using System.IO;8using System.Management.Automation;9using System.Reflection;10using System.Runtime.InteropServices;11 12namespace Microsoft.PowerShell13{14 /// <summary>15 /// Defines an entry point for the .NET CLI "powershell" app.16 /// </summary>17 public sealed class ManagedPSEntry18 {19#if UNIX20 /// <summary>21 /// Exception to signify an early startup failure.22 /// </summary>23 private sealed class StartupException : Exception24 {25 /// <summary>26 /// Construct a new startup exception instance.27 /// </summary>28 /// <param name="callName">The name of the native call that failed.</param>29 /// <param name="exitCode">The exit code the native call returned.</param>30 public StartupException(string callName, int exitCode)31 {32 CallName = callName;33 ExitCode = exitCode;34 }35 36 /// <summary>37 /// The name of the native call that failed.38 /// </summary>39 public string CallName { get; }40 41 /// <summary>42 /// The exit code returned by the failed native call.43 /// </summary>44 public int ExitCode { get; }45 }46 47 // Environment variable used to short circuit second login check48 private const string LOGIN_ENV_VAR_NAME = "__PWSH_LOGIN_CHECKED";49 private const string LOGIN_ENV_VAR_VALUE = "1";50 51 // Linux p/Invoke constants52 private const int LINUX_PATH_MAX = 4096;53 54 // MacOS p/Invoke constants55 private const int MACOS_CTL_KERN = 1;56 private const int MACOS_KERN_ARGMAX = 8;57 private const int MACOS_KERN_PROCARGS2 = 49;58 private const int MACOS_PROC_PIDPATHINFO_MAXSIZE = 4096;59#endif60 61 /// <summary>62 /// Starts PowerShell.63 /// </summary>64 /// <param name="args">65 /// Command line arguments to PowerShell66 /// </param>67 public static int Main(string[] args)68 {69#if UNIX70 AttemptExecPwshLogin(args);71#endif72 return UnmanagedPSEntry.Start(args, args.Length);73 }74 75#if UNIX76 /// <summary>77 /// Checks whether pwsh has been started as a login shell78 /// and if so, proceeds with the login process.79 /// This method will return early if pwsh was not started as a login shell80 /// and will throw if it detects a native call has failed.81 /// In the event of success, we use an exec() call, so this method never returns.82 /// </summary>83 /// <param name="args">The startup arguments to pwsh.</param>84 private static void AttemptExecPwshLogin(string[] args)85 {86 // If the login environment variable is set, we have already done the login logic and have been exec'd87 if (Environment.GetEnvironmentVariable(LOGIN_ENV_VAR_NAME) != null)88 {89 Environment.SetEnvironmentVariable(LOGIN_ENV_VAR_NAME, null);90 return;91 }92 93 bool isLinux = Platform.IsLinux;94 95 // The first byte (ASCII char) of the name of this process, used to detect '-' for login96 byte procNameFirstByte;97 98 // The path to the executable this process was started from99 string? pwshPath;100 101 // On Linux, we can simply use the /proc filesystem102 if (isLinux)103 {104 // Read the process name byte105 using (FileStream fs = File.OpenRead("/proc/self/cmdline"))106 {107 procNameFirstByte = (byte)fs.ReadByte();108 }109 110 // Run login detection logic111 if (!IsLogin(procNameFirstByte, args))112 {113 return;114 }115 116 // Read the symlink to the startup executable117 IntPtr linkPathPtr = Marshal.AllocHGlobal(LINUX_PATH_MAX);118 IntPtr bufSize = ReadLink("/proc/self/exe", linkPathPtr, (UIntPtr)LINUX_PATH_MAX);119 pwshPath = Marshal.PtrToStringAnsi(linkPathPtr, (int)bufSize);120 Marshal.FreeHGlobal(linkPathPtr);121 122 ArgumentNullException.ThrowIfNull(pwshPath);123 124 // exec pwsh125 ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: false));126 return;127 }128 129 // At this point, we are on macOS130 131 // Set up the mib array and the query for process maximum args size132 Span<int> mib = [MACOS_CTL_KERN, MACOS_KERN_ARGMAX];133 int size = IntPtr.Size / 2;134 int argmax = 0;135 136 // Get the process args size137 unsafe138 {139 fixed (int *mibptr = mib)140 {141 ThrowOnFailure(nameof(argmax), SysCtl(mibptr, mib.Length, &argmax, &size, IntPtr.Zero, 0));142 }143 }144 145 // Get the PID so we can query this process' args146 int pid = GetPid();147 148 // The following logic is based on https://gist.github.com/nonowarn/770696149 150 // Now read the process args into the allocated space151 IntPtr procargs = Marshal.AllocHGlobal(argmax);152 IntPtr executablePathPtr = IntPtr.Zero;153 try154 {155 mib = new int[] { MACOS_CTL_KERN, MACOS_KERN_PROCARGS2, pid };156 157 unsafe158 {159 fixed (int *mibptr = mib)160 {161 ThrowOnFailure(nameof(procargs), SysCtl(mibptr, mib.Length, procargs.ToPointer(), &argmax, IntPtr.Zero, 0));162 }163 164 // The memory block we're reading is a series of null-terminated strings165 // that looks something like this:166 //167 // | argc | <int>168 // | exec_path | ... \0169 // | argv[0] | ... \0170 // | argv[1] | ... \0171 // ...172 //173 // We care about argv[0], since that's the name the process was started with.174 // If argv[0][0] == '-', we have been invoked as login.175 // Doing this, the buffer we populated also recorded `exec_path`,176 // which is the path to our executable `pwsh`.177 // We can reuse this value later to prevent needing to call a .NET API178 // to generate our exec invocation.179 180 // We don't care about argc's value, since argv[0] must always exist.181 // Skip over argc, but remember where exec_path is for later182 executablePathPtr = IntPtr.Add(procargs, sizeof(int));183 184 // Skip over exec_path185 byte *argvPtr = (byte *)executablePathPtr;186 while (*argvPtr != 0) { argvPtr++; }187 while (*argvPtr == 0) { argvPtr++; }188 189 // First char in argv[0]190 procNameFirstByte = *argvPtr;191 }192 193 if (!IsLogin(procNameFirstByte, args))194 {195 return;196 }197 198 // Get the pwshPath from exec_path199 pwshPath = Marshal.PtrToStringAnsi(executablePathPtr);200 201 ArgumentNullException.ThrowIfNull(pwshPath);202 203 // exec pwsh204 ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: true));205 }206 finally207 {208 Marshal.FreeHGlobal(procargs);209 }210 }211 212 /// <summary>213 /// Checks args to see if -Login has been specified.214 /// </summary>215 /// <param name="procNameFirstByte">The first byte of the name of the currently running process.</param>216 /// <param name="args">Arguments passed to the program.</param>217 /// <returns></returns>218 private static bool IsLogin(219 byte procNameFirstByte,220 string[] args)221 {222 // Process name starting with '-' means this is a login shell223 if (procNameFirstByte == 0x2D)224 {225 return true;226 }227 228 // Look at the first parameter to see if it is -Login229 // NOTE: -Login is only supported as the first parameter to PowerShell230 return args.Length > 0231 && args[0].Length > 1232 && args[0][0] == '-'233 && IsParam(args[0], "login", "LOGIN");234 }235 236 /// <summary>237 /// Determines if a given parameter is the one we're looking for.238 /// Assumes any prefix determines that parameter (true for -l, -c and -f).239 /// </summary>240 /// <param name="arg">The argument to check.</param>241 /// <param name="paramToCheck">The lowercase name of the parameter to check.</param>242 /// <param name="paramToCheckUpper">The uppercase name of the parameter to check.</param>243 /// <returns></returns>244 private static bool IsParam(245 string arg,246 string paramToCheck,247 string paramToCheckUpper)248 {249 // Quick fail if the argument is longer than the parameter250 if (arg.Length > paramToCheck.Length + 1)251 {252 return false;253 }254 255 // Check arg chars in order and allow prefixes256 for (int i = 1; i < arg.Length; i++)257 {258 if (arg[i] != paramToCheck[i - 1]259 && arg[i] != paramToCheckUpper[i - 1])260 {261 return false;262 }263 }264 265 return true;266 }267 268 /// <summary>269 /// Create the exec call to /bin/{z}sh -l -c 'exec pwsh "$@"' and run it.270 /// </summary>271 /// <param name="args">The argument vector passed to pwsh.</param>272 /// <param name="isMacOS">True if we are running on macOS.</param>273 /// <param name="pwshPath">Absolute path to the pwsh executable.</param>274 /// <returns>275 /// The exit code of exec if it fails.276 /// If exec succeeds, this process is overwritten so we never actually return.277 /// </returns>278 private static int ExecPwshLogin(string[] args, string pwshPath, bool isMacOS)279 {280 // Create input for /bin/sh that execs pwsh281 int quotedPwshPathLength = GetQuotedPathLength(pwshPath);282 283 string pwshInvocation = string.Create(284 quotedPwshPathLength + 10, // exec '{pwshPath}' "$@"285 (pwshPath, quotedPwshPathLength),286 CreatePwshInvocation);287 288 // Set up the arguments for '/bin/sh'.289 // We need to add 5 slots for the '/bin/sh' invocation parts, plus 1 slot for the null terminator at the end290 var execArgs = new string?[args.Length + 6];291 292 // The command arguments293 294 // First argument is the command name.295 // Even when executing 'zsh', we want to set this to '/bin/sh'296 // because this tells 'zsh' to run in sh emulation mode (it examines $0)297 execArgs[0] = "/bin/sh";298 299 execArgs[1] = "-l"; // Login flag300 execArgs[2] = "-c"; // Command parameter301 execArgs[3] = pwshInvocation; // Command to execute302 303 // The /bin/sh option spec looks like:304 // sh -c command_string [command_name [argument...]]305 // We must provide a command_name before arguments,306 // but this is never used since "$@" takes argv[1] - argv[n]307 // and the `exec` builtin provides its own argv[0].308 // See https://pubs.opengroup.org/onlinepubs/9699919799.2016edition/309 //310 // Since command_name is ignored and we can't use null (it's the terminator)311 // we use empty string312 execArgs[4] = string.Empty;313 314 // Add the arguments passed to pwsh on the end.315 args.CopyTo(execArgs, 5);316 317 // A null is required by exec.318 execArgs[execArgs.Length - 1] = null;319 320 // We can't use Environment.SetEnvironmentVariable() here.321 // See https://github.com/dotnet/corefx/issues/40130#issuecomment-519420648.322 ThrowOnFailure("setenv", SetEnv(LOGIN_ENV_VAR_NAME, LOGIN_ENV_VAR_VALUE, overwrite: true));323 324 // On macOS, sh doesn't support login, so we run /bin/zsh in sh emulation mode.325 if (isMacOS)326 {327 return Exec("/bin/zsh", execArgs);328 }329 330 return Exec("/bin/sh", execArgs);331 }332 333 /// <summary>334 /// Gets what the length of the given string will be if it's335 /// quote escaped for /bin/sh.336 /// </summary>337 /// <param name="str">The string to quote escape.</param>338 /// <returns>The length of the string when it's quote escaped.</returns>339 private static int GetQuotedPathLength(string str)340 {341 int length = 2;342 foreach (char c in str)343 {344 length++;345 346 if (c == '\'')347 {348 length++;349 }350 }351 352 return length;353 }354 355 /// <summary>356 /// Implements a SpanAction<T> for string.Create()357 /// that builds the shell invocation for the login pwsh session.358 /// </summary>359 /// <param name="strBuf">The buffer of the string to be created.</param>360 /// <param name="invocationInfo">Information used to build the required string.</param>361 private static void CreatePwshInvocation(362 Span<char> strBuf,363 (string path, int quotedLength) invocationInfo)364 {365 // "exec "366 const string prefix = "exec ";367 prefix.AsSpan().CopyTo(strBuf);368 369 // The quoted path to pwsh, like "'/opt/microsoft/powershell/7/pwsh'"370 int i = prefix.Length;371 Span<char> pathSpan = strBuf.Slice(i, invocationInfo.quotedLength);372 QuoteAndWriteToSpan(invocationInfo.path, pathSpan);373 i += invocationInfo.quotedLength;374 375 // ' "$@"' the argument vector splat to pass pwsh arguments through376 const string suffix = " \"$@\"";377 Span<char> bufSuffix = strBuf.Slice(i);378 suffix.AsSpan().CopyTo(bufSuffix);379 }380 381 /// <summary>382 /// Quotes (and sh quote escapes) a string and writes it to the given span.383 /// </summary>384 /// <param name="arg">The string to quote.</param>385 /// <param name="span">The span to write to.</param>386 private static void QuoteAndWriteToSpan(string arg, Span<char> span)387 {388 span[0] = '\'';389 390 int i = 0;391 int j = 1;392 for (; i < arg.Length; i++, j++)393 {394 char c = arg[i];395 396 if (c == '\'')397 {398 // /bin/sh quote escaping uses backslashes399 span[j] = '\\';400 j++;401 }402 403 span[j] = c;404 }405 406 span[j] = '\'';407 }408 409 /// <summary>410 /// If the given exit code is negative, throws a StartupException.411 /// </summary>412 /// <param name="call">The native call that was attempted.</param>413 /// <param name="code">The exit code it returned.</param>414 private static void ThrowOnFailure(string call, int code)415 {416 if (code < 0)417 {418 code = Marshal.GetLastWin32Error();419 Console.Error.WriteLine($"Call to '{call}' failed with errno {code}");420 throw new StartupException(call, code);421 }422 }423 424 /// <summary>425 /// The `execv` POSIX syscall we use to exec /bin/sh.426 /// </summary>427 /// <param name="path">The path to the executable to exec.</param>428 /// <param name="args">429 /// The arguments to send through to the executable.430 /// Array must have its final element be null.431 /// </param>432 /// <returns>433 /// An exit code if exec failed, but if successful the calling process will be overwritten.434 /// </returns>435 [DllImport("libc",436 EntryPoint = "execv",437 CallingConvention = CallingConvention.Cdecl,438 CharSet = CharSet.Ansi,439 SetLastError = true)]440 private static extern int Exec(string path, string?[] args);441 442 /// <summary>443 /// The `readlink` POSIX syscall we use to read the symlink from /proc/self/exe444 /// to get the executable path of pwsh on Linux.445 /// </summary>446 /// <param name="pathname">The path to the symlink to read.</param>447 /// <param name="buf">Pointer to a buffer to fill with the result.</param>448 /// <param name="size">The size of the buffer we have supplied.</param>449 /// <returns>The number of bytes placed in the buffer.</returns>450 [DllImport("libc",451 EntryPoint = "readlink",452 CallingConvention = CallingConvention.Cdecl,453 CharSet = CharSet.Ansi,454 SetLastError = true)]455 private static extern IntPtr ReadLink(string pathname, IntPtr buf, UIntPtr size);456 457 /// <summary>458 /// The `getpid` POSIX syscall we use to quickly get the current process PID on macOS.459 /// </summary>460 /// <returns>The pid of the current process.</returns>461 [DllImport("libc",462 EntryPoint = "getpid",463 CallingConvention = CallingConvention.Cdecl,464 CharSet = CharSet.Ansi,465 SetLastError = true)]466 private static extern int GetPid();467 468 /// <summary>469 /// The `setenv` POSIX syscall used to set an environment variable in the process.470 /// </summary>471 /// <param name="name">The name of the environment variable.</param>472 /// <param name="value">The value of the environment variable.</param>473 /// <param name="overwrite">If true, will overwrite an existing environment variable of the same name.</param>474 /// <returns>0 if successful, -1 on error. errno indicates the reason for failure.</returns>475 [DllImport("libc",476 EntryPoint = "setenv",477 CallingConvention = CallingConvention.Cdecl,478 CharSet = CharSet.Ansi,479 SetLastError = true)]480 private static extern int SetEnv(string name, string value, bool overwrite);481 482 /// <summary>483 /// The `sysctl` BSD sycall used to get system information on macOS.484 /// </summary>485 /// <param name="mib">The Management Information Base name, used to query information.</param>486 /// <param name="mibLength">The length of the MIB name.</param>487 /// <param name="oldp">The object passed out of sysctl (may be null)</param>488 /// <param name="oldlenp">The size of the object passed out of sysctl.</param>489 /// <param name="newp">The object passed in to sysctl.</param>490 /// <param name="newlenp">The length of the object passed in to sysctl.</param>491 /// <returns></returns>492 [DllImport("libc",493 EntryPoint = "sysctl",494 CallingConvention = CallingConvention.Cdecl,495 CharSet = CharSet.Ansi,496 SetLastError = true)]497 private static extern unsafe int SysCtl(int *mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp);498#endif499 }500}501 