Team Ai
Datasetpublic

MegaBites-AI/Windows-powershell

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes308downloads
Program.cs501 linesDownload Raw Back to powershell
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&lt;T&gt; 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