File: System\Diagnostics\ProcessUtils.Unix.cs
Web Access
Project: src\src\libraries\System.Diagnostics.Process\src\System.Diagnostics.Process.csproj (System.Diagnostics.Process)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;
 
namespace System.Diagnostics
{
    internal static partial class ProcessUtils
    {
        private static volatile bool s_initialized;
        private static readonly object s_initializedGate = new object();
 
        internal static bool SupportsAtomicNonInheritablePipeCreation => Interop.Sys.IsAtomicNonInheritablePipeCreationSupported;
 
        internal static bool PlatformDoesNotSupportProcessStartAndKill
            => (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || OperatingSystem.IsTvOS();
 
        private static bool IsExecutable(string fullPath)
        {
            Interop.Sys.FileStatus fileinfo;
 
            if (Interop.Sys.Stat(fullPath, out fileinfo) < 0)
            {
                return false;
            }
 
            // Check if the path is a directory.
            if ((fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR)
            {
                return false;
            }
 
            const UnixFileMode AllExecute = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
 
            UnixFileMode permissions = ((UnixFileMode)fileinfo.Mode) & AllExecute;
 
            // Avoid checking user/group when permission.
            if (permissions == AllExecute)
            {
                return true;
            }
            else if (permissions == 0)
            {
                return false;
            }
 
            uint euid = Interop.Sys.GetEUid();
 
            if (euid == 0)
            {
                return true; // We're root.
            }
 
            if (euid == fileinfo.Uid)
            {
                // We own the file.
                return (permissions & UnixFileMode.UserExecute) != 0;
            }
 
            bool groupCanExecute = (permissions & UnixFileMode.GroupExecute) != 0;
            bool otherCanExecute = (permissions & UnixFileMode.OtherExecute) != 0;
 
            // Avoid group check when group and other have same permissions.
            if (groupCanExecute == otherCanExecute)
            {
                return groupCanExecute;
            }
 
            if (Interop.Sys.IsMemberOfGroup(fileinfo.Gid))
            {
                return groupCanExecute;
            }
            else
            {
                return otherCanExecute;
            }
        }
 
        internal static unsafe void EnsureInitialized()
        {
            if (s_initialized)
            {
                return;
            }
 
            lock (s_initializedGate)
            {
                if (!s_initialized)
                {
                    if (!Interop.Sys.InitializeTerminalAndSignalHandling())
                    {
                        throw new Win32Exception();
                    }
 
                    // Register our callback.
                    Interop.Sys.RegisterForSigChld(&OnSigChild);
                    SetDelayedSigChildConsoleConfigurationHandler();
 
                    s_initialized = true;
                }
            }
        }
 
        internal static (uint userId, uint groupId, uint[] groups) GetUserAndGroupIds(ProcessStartInfo startInfo)
        {
            Debug.Assert(!string.IsNullOrEmpty(startInfo.UserName));
 
            (uint? userId, uint? groupId) = GetUserAndGroupIds(startInfo.UserName);
 
            Debug.Assert(userId.HasValue == groupId.HasValue, "userId and groupId both need to have values, or both need to be null.");
            if (!userId.HasValue)
            {
                throw new Win32Exception(SR.Format(SR.UserDoesNotExist, startInfo.UserName));
            }
 
            uint[]? groups = Interop.Sys.GetGroupList(startInfo.UserName, groupId!.Value);
            if (groups == null)
            {
                throw new Win32Exception(SR.Format(SR.UserGroupsCannotBeDetermined, startInfo.UserName));
            }
 
            return (userId.Value, groupId.Value, groups);
        }
 
        private static unsafe (uint? userId, uint? groupId) GetUserAndGroupIds(string userName)
        {
            Interop.Sys.Passwd? passwd;
            // First try with a buffer that should suffice for 99% of cases.
            // Note: on CentOS/RedHat 7.1 systems, getpwnam_r returns 'user not found' if the buffer is too small
            // see https://bugs.centos.org/view.php?id=7324
            const int BufLen = Interop.Sys.Passwd.InitialBufferSize;
            byte* stackBuf = stackalloc byte[BufLen];
            if (TryGetPasswd(userName, stackBuf, BufLen, out passwd))
            {
                if (passwd == null)
                {
                    return (null, null);
                }
                return (passwd.Value.UserId, passwd.Value.GroupId);
            }
 
            // Fallback to heap allocations if necessary, growing the buffer until
            // we succeed.  TryGetPasswd will throw if there's an unexpected error.
            int lastBufLen = BufLen;
            while (true)
            {
                lastBufLen *= 2;
                byte[] heapBuf = new byte[lastBufLen];
                fixed (byte* buf = &heapBuf[0])
                {
                    if (TryGetPasswd(userName, buf, heapBuf.Length, out passwd))
                    {
                        if (passwd == null)
                        {
                            return (null, null);
                        }
                        return (passwd.Value.UserId, passwd.Value.GroupId);
                    }
                }
            }
        }
 
        private static unsafe bool TryGetPasswd(string name, byte* buf, int bufLen, out Interop.Sys.Passwd? passwd)
        {
            // Call getpwnam_r to get the passwd struct
            Interop.Sys.Passwd tempPasswd;
            int error = Interop.Sys.GetPwNamR(name, out tempPasswd, buf, bufLen);
 
            // If the call succeeds, give back the passwd retrieved
            if (error == 0)
            {
                passwd = tempPasswd;
                return true;
            }
 
            // If the current user's entry could not be found, give back null,
            // but still return true as false indicates the buffer was too small.
            if (error == -1)
            {
                passwd = null;
                return true;
            }
 
            var errorInfo = new Interop.ErrorInfo(error);
 
            // If the call failed because the buffer was too small, return false to
            // indicate the caller should try again with a larger buffer.
            if (errorInfo.Error == Interop.Error.ERANGE)
            {
                passwd = null;
                return false;
            }
 
            // Otherwise, fail.
            throw new Win32Exception(errorInfo.RawErrno, errorInfo.GetErrorMessage());
        }
 
        internal static string? ResolveExecutableForShellExecute(string filename, string? workingDirectory)
        {
            // Determine if filename points to an executable file.
            // filename may be an absolute path, a relative path or a uri.
 
            string? resolvedFilename = null;
            // filename is an absolute path
            if (Path.IsPathRooted(filename))
            {
                if (File.Exists(filename))
                {
                    resolvedFilename = filename;
                }
            }
            // filename is a uri
            else if (Uri.TryCreate(filename, UriKind.Absolute, out Uri? uri))
            {
                if (uri.IsFile && uri.Host == "" && File.Exists(uri.LocalPath))
                {
                    resolvedFilename = uri.LocalPath;
                }
            }
            // filename is relative
            else
            {
                // The WorkingDirectory property specifies the location of the executable.
                // If WorkingDirectory is an empty string, the current directory is understood to contain the executable.
                workingDirectory = workingDirectory != null ? Path.GetFullPath(workingDirectory) :
                                                              Directory.GetCurrentDirectory();
                string filenameInWorkingDirectory = Path.Combine(workingDirectory, filename);
                // filename is a relative path in the working directory
                if (File.Exists(filenameInWorkingDirectory))
                {
                    resolvedFilename = filenameInWorkingDirectory;
                }
                // find filename on PATH
                else
                {
                    resolvedFilename = FindProgramInPath(filename);
                }
            }
 
            if (resolvedFilename == null)
            {
                return null;
            }
 
            if (Interop.Sys.Access(resolvedFilename, Interop.Sys.AccessMode.X_OK) == 0)
            {
                return resolvedFilename;
            }
            else
            {
                return null;
            }
        }
 
        [UnmanagedCallersOnly]
        private static int OnSigChild(int reapAll, int configureConsole)
        {
            // configureConsole is non zero when there are PosixSignalRegistrations that
            // may Cancel the terminal configuration that happens when there are no more
            // children using the terminal.
            // When the registrations don't cancel the terminal configuration,
            // DelayedSigChildConsoleConfiguration will be called.
 
            // Lock to avoid races with Process.Start
            s_processStartLock.EnterWriteLock();
            try
            {
                bool childrenUsingTerminalPre = AreChildrenUsingTerminal;
                ProcessWaitState.CheckChildren(reapAll != 0, configureConsole != 0);
                bool childrenUsingTerminalPost = AreChildrenUsingTerminal;
 
                // return whether console configuration was skipped.
                return childrenUsingTerminalPre && !childrenUsingTerminalPost && configureConsole == 0 ? 1 : 0;
            }
            finally
            {
                s_processStartLock.ExitWriteLock();
            }
        }
 
        /// <summary>Converts the filename and arguments information from a ProcessStartInfo into an argv array.</summary>
        /// <param name="psi">The ProcessStartInfo.</param>
        /// <param name="resolvedExe">Resolved executable to open ProcessStartInfo.FileName</param>
        /// <param name="ignoreArguments">Don't pass ProcessStartInfo.Arguments</param>
        /// <returns>The argv array.</returns>
        internal static string[] ParseArgv(ProcessStartInfo psi, string? resolvedExe = null, bool ignoreArguments = false)
        {
            if (string.IsNullOrEmpty(resolvedExe) &&
                (ignoreArguments || (string.IsNullOrEmpty(psi.Arguments) && !psi.HasArgumentList)))
            {
                return new string[] { psi.FileName };
            }
 
            var argvList = new List<string>();
            if (!string.IsNullOrEmpty(resolvedExe))
            {
                argvList.Add(resolvedExe);
                if (resolvedExe.Contains("kfmclient"))
                {
                    argvList.Add("openURL"); // kfmclient needs OpenURL
                }
            }
 
            argvList.Add(psi.FileName);
 
            if (!ignoreArguments)
            {
                if (!string.IsNullOrEmpty(psi.Arguments))
                {
                    ParseArgumentsIntoList(psi.Arguments, argvList);
                }
                else if (psi.HasArgumentList)
                {
                    argvList.AddRange(psi.ArgumentList);
                }
            }
            return argvList.ToArray();
        }
 
        /// <summary>Resolves a path to the filename passed to ProcessStartInfo. </summary>
        /// <param name="filename">The filename.</param>
        /// <returns>The resolved path. It can return null in case of URLs.</returns>
        internal static string? ResolvePath(string filename)
        {
            // Follow the same resolution that Windows uses with CreateProcess:
            // 1. First try the exact path provided
            // 2. Then try the file relative to the executable directory
            // 3. Then try the file relative to the current directory
            // 4. then try the file in each of the directories specified in PATH
            // Windows does additional Windows-specific steps between 3 and 4,
            // and we ignore those here.
 
            // If the filename is a complete path, use it, regardless of whether it exists.
            if (Path.IsPathRooted(filename))
            {
                // In this case, it doesn't matter whether the file exists or not;
                // it's what the caller asked for, so it's what they'll get
                return filename;
            }
 
            // Then check the executable's directory
            string? path = Environment.ProcessPath;
            if (path != null)
            {
                try
                {
                    path = Path.Combine(Path.GetDirectoryName(path)!, filename);
                    if (File.Exists(path))
                    {
                        return path;
                    }
                }
                catch (ArgumentException) { } // ignore any errors in data that may come from the exe path
            }
 
            // Then check the current directory
            path = Path.Combine(Directory.GetCurrentDirectory(), filename);
            if (File.Exists(path))
            {
                return path;
            }
 
            // Then check each directory listed in the PATH environment variables
            return FindProgramInPath(filename);
        }
 
        /// <summary>Parses a command-line argument string into a list of arguments.</summary>
        /// <param name="arguments">The argument string.</param>
        /// <param name="results">The list into which the component arguments should be stored.</param>
        /// <remarks>
        /// This follows the rules outlined in "Parsing C++ Command-Line Arguments" at
        /// https://msdn.microsoft.com/en-us/library/17w5ykft.aspx.
        /// </remarks>
        private static void ParseArgumentsIntoList(string arguments, List<string> results)
        {
            // Iterate through all of the characters in the argument string.
            for (int i = 0; i < arguments.Length; i++)
            {
                while (i < arguments.Length && (arguments[i] == ' ' || arguments[i] == '\t'))
                    i++;
 
                if (i == arguments.Length)
                    break;
 
                results.Add(GetNextArgument(arguments, ref i));
            }
        }
 
        private static string GetNextArgument(string arguments, ref int i)
        {
            var currentArgument = new ValueStringBuilder(stackalloc char[256]);
            bool inQuotes = false;
 
            while (i < arguments.Length)
            {
                // From the current position, iterate through contiguous backslashes.
                int backslashCount = 0;
                while (i < arguments.Length && arguments[i] == '\\')
                {
                    i++;
                    backslashCount++;
                }
 
                if (backslashCount > 0)
                {
                    if (i >= arguments.Length || arguments[i] != '"')
                    {
                        // Backslashes not followed by a double quote:
                        // they should all be treated as literal backslashes.
                        currentArgument.Append('\\', backslashCount);
                    }
                    else
                    {
                        // Backslashes followed by a double quote:
                        // - Output a literal slash for each complete pair of slashes
                        // - If one remains, use it to make the subsequent quote a literal.
                        currentArgument.Append('\\', backslashCount / 2);
                        if (backslashCount % 2 != 0)
                        {
                            currentArgument.Append('"');
                            i++;
                        }
                    }
 
                    continue;
                }
 
                char c = arguments[i];
 
                // If this is a double quote, track whether we're inside of quotes or not.
                // Anything within quotes will be treated as a single argument, even if
                // it contains spaces.
                if (c == '"')
                {
                    if (inQuotes && i < arguments.Length - 1 && arguments[i + 1] == '"')
                    {
                        // Two consecutive double quotes inside an inQuotes region should result in a literal double quote
                        // (the parser is left in the inQuotes region).
                        // This behavior is not part of the spec of code:ParseArgumentsIntoList, but is compatible with CRT
                        // and .NET Framework.
                        currentArgument.Append('"');
                        i++;
                    }
                    else
                    {
                        inQuotes = !inQuotes;
                    }
 
                    i++;
                    continue;
                }
 
                // If this is a space/tab and we're not in quotes, we're done with the current
                // argument, it should be added to the results and then reset for the next one.
                if ((c == ' ' || c == '\t') && !inQuotes)
                {
                    break;
                }
 
                // Nothing special; add the character to the current argument.
                currentArgument.Append(c);
                i++;
            }
 
            return currentArgument.ToString();
        }
    }
}