File: Microsoft\Win32\SafeHandles\SafeProcessHandle.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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
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 Microsoft.Win32.SafeHandles
{
    public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        // On Windows, SafeProcessHandle represents the actual OS handle for the process.
        // On Unix, there's no such concept.  Instead, the implementation manufactures
        // a WaitHandle that it manually sets when the process completes; SafeProcessHandle
        // then just wraps that same WaitHandle instance.  This allows consumers that use
        // Process.{Safe}Handle to initialize and use a WaitHandle to successfully use it on
        // Unix as well to wait for the process to complete.
 
        private readonly SafeWaitHandle? _handle;
        private readonly bool _releaseRef;
        private readonly ProcessWaitState.Holder? _waitStateHolder;
 
        internal SafeProcessHandle(ProcessWaitState.Holder waitStateHolder) : base(ownsHandle: true)
        {
            _waitStateHolder = waitStateHolder;
            _handle = _waitStateHolder._state.EnsureExitedEvent().GetSafeWaitHandle();
            _handle.DangerousAddRef(ref _releaseRef);
            SetHandle(_handle.DangerousGetHandle());
        }
 
        /// <summary>
        /// Gets the process ID.
        /// </summary>
        public int ProcessId
        {
            get
            {
                Validate();
 
                if (_waitStateHolder is null)
                {
                    throw new InvalidOperationException(SR.InvalidProcessHandle);
                }
 
                return _waitStateHolder._state._processId;
            }
        }
 
        /// <summary>
        /// Sets a value indicating whether the process has been terminated due to timeout or cancellation.
        /// </summary>
        private bool Canceled
        {
            set => GetWaitState()._canceled = value;
        }
 
        protected override bool ReleaseHandle()
        {
            if (_releaseRef)
            {
                Debug.Assert(_handle != null);
                _handle.DangerousRelease();
            }
            _waitStateHolder?.Dispose();
            return true;
        }
 
        private bool SignalCore(PosixSignal signal)
        {
            if (!ProcessUtils.PlatformSupportsProcessStartAndKill)
            {
                throw new PlatformNotSupportedException();
            }
 
            int signalNumber = Interop.Sys.GetPlatformSignalNumber(signal);
            if (signalNumber == 0)
            {
                throw new PlatformNotSupportedException();
            }
 
            int killResult = Interop.Sys.Kill(ProcessId, signalNumber);
            if (killResult != 0)
            {
                Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
 
                // Return false if the process has already exited (or never existed).
                if (errorInfo.Error == Interop.Error.ESRCH)
                {
                    return false;
                }
 
                throw new Win32Exception(errorInfo.RawErrno); // same exception as on Windows
            }
 
            return true;
        }
 
        private ProcessExitStatus WaitForExitCore()
        {
            ProcessWaitState waitState = GetWaitState();
            waitState.WaitForExit(Timeout.Infinite);
 
            return GetExitStatus(waitState);
        }
 
        private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus)
        {
            ProcessWaitState waitState = GetWaitState();
            if (!waitState.WaitForExit(milliseconds))
            {
                exitStatus = null;
                return false;
            }
 
            exitStatus = GetExitStatus(waitState);
            return true;
        }
 
        private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds)
        {
            ProcessWaitState waitState = GetWaitState();
 
            if (!waitState.WaitForExit(milliseconds))
            {
                waitState._canceled = true;
                SignalCore(PosixSignal.SIGKILL);
                waitState.WaitForExit(Timeout.Infinite);
            }
 
            return GetExitStatus(waitState);
        }
 
        private ManualResetEvent GetWaitHandle() => GetWaitState().EnsureExitedEvent();
 
        private ProcessWaitState GetWaitState()
        {
            if (_waitStateHolder is null)
            {
                throw new InvalidOperationException(SR.InvalidProcessHandle);
            }
 
            if (!_waitStateHolder._state._isChild)
            {
                throw new PlatformNotSupportedException(SR.NotSupportedForNonChildProcess);
            }
 
            return _waitStateHolder._state;
        }
 
        private ProcessExitStatus GetExitStatus() => GetExitStatus(GetWaitState());
 
        private static ProcessExitStatus GetExitStatus(ProcessWaitState waitState)
        {
            // GetWaitState ensures the process is a child process, so obtaining the exit status should never fail.
            bool exited = waitState.GetExited(out ProcessExitStatus? exitStatus, refresh: false);
            Debug.Assert(exited);
            Debug.Assert(exitStatus is not null);
            return exitStatus ?? throw new InvalidOperationException();
        }
 
        private delegate SafeProcessHandle StartWithShellExecuteDelegate(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder);
        private static StartWithShellExecuteDelegate? s_startWithShellExecute;
 
        private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandlesSnapshot = null)
            => StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandlesSnapshot, out _);
 
        internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle,
            SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, out ProcessWaitState.Holder? waitStateHolder)
        {
            waitStateHolder = null;
 
            ProcessUtils.EnsureInitialized();
 
            if (startInfo.UseShellExecute)
            {
                return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle, out waitStateHolder);
            }
 
            string? filename;
            string[] argv;
 
            IDictionary<string, string?> env = startInfo.Environment;
            string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null;
 
            bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName);
            uint userId = 0;
            uint groupId = 0;
            uint[]? groups = null;
            if (setCredentials)
            {
                (userId, groupId, groups) = ProcessUtils.GetUserAndGroupIds(startInfo);
            }
 
            bool usesTerminal = UsesTerminal(stdinHandle, stdoutHandle, stderrHandle);
 
            filename = ProcessUtils.ResolvePath(startInfo.FileName);
            argv = ProcessUtils.ParseArgv(startInfo);
            if (Directory.Exists(filename))
            {
                throw new Win32Exception(SR.DirectoryNotValidAsInput);
            }
 
            return ForkAndExecProcess(
                startInfo, filename, argv, env, cwd,
                setCredentials, userId, groupId, groups,
                stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
                inheritedHandles,
                out waitStateHolder);
        }
 
        private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle,
            SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder)
        {
            IDictionary<string, string?> env = startInfo.Environment;
            string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null;
 
            bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName);
            uint userId = 0;
            uint groupId = 0;
            uint[]? groups = null;
            if (setCredentials)
            {
                (userId, groupId, groups) = ProcessUtils.GetUserAndGroupIds(startInfo);
            }
 
            bool usesTerminal = UsesTerminal(stdinHandle, stdoutHandle, stderrHandle);
 
            string verb = startInfo.Verb;
            if (verb != string.Empty &&
                !string.Equals(verb, "open", StringComparison.OrdinalIgnoreCase))
            {
                throw new Win32Exception(Interop.Errors.ERROR_NO_ASSOCIATION);
            }
 
            // On Windows, UseShellExecute of executables and scripts causes those files to be executed.
            // To achieve this on Unix, we check if the file is executable (x-bit).
            // Some files may have the x-bit set even when they are not executable. This happens for example
            // when a Windows filesystem is mounted on Linux. To handle that, treat it as a regular file
            // when exec returns ENOEXEC (file format cannot be executed).
            string? filename = ProcessUtils.ResolveExecutableForShellExecute(startInfo.FileName, cwd);
            if (filename != null)
            {
                string[] argv = ProcessUtils.ParseArgv(startInfo);
 
                SafeProcessHandle processHandle = ForkAndExecProcess(
                    startInfo, filename, argv, env, cwd,
                    setCredentials, userId, groupId, groups,
                    stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
                    null,
                    out waitStateHolder,
                    throwOnNoExec: false); // return invalid handle instead of throwing on ENOEXEC
 
                if (!processHandle.IsInvalid)
                {
                    return processHandle;
                }
 
                // ENOEXEC: the process was not started on this path; dispose the holder and try the fallback.
                waitStateHolder?.Dispose();
            }
 
            // use default program to open file/url
            filename = Process.GetPathToOpenFile();
            string[] openFileArgv = ProcessUtils.ParseArgv(startInfo, filename, ignoreArguments: true);
 
            SafeProcessHandle result = ForkAndExecProcess(
                startInfo, filename, openFileArgv, env, cwd,
                setCredentials, userId, groupId, groups,
                stdinHandle, stdoutHandle, stderrHandle, usesTerminal,
                null,
                out waitStateHolder);
 
            return result;
        }
 
        // .NET applications don't echo characters unless there is a Console.Read operation.
        // Unix applications expect the terminal to be in an echoing state by default.
        // To support processes that interact with the terminal (e.g. 'vi'), we need to configure the
        // terminal to echo. We keep this configuration as long as there are children possibly using the terminal.
        private static bool UsesTerminal(SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle)
            => ProcessUtils.IsTerminal(stdinHandle) || ProcessUtils.IsTerminal(stdoutHandle) || ProcessUtils.IsTerminal(stderrHandle);
 
        private static SafeProcessHandle ForkAndExecProcess(
            ProcessStartInfo startInfo, string? resolvedFilename, string[] argv,
            IDictionary<string, string?> env, string? cwd, bool setCredentials, uint userId,
            uint groupId, uint[]? groups,
            SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle,
            bool usesTerminal, SafeHandle[]? inheritedHandles, out ProcessWaitState.Holder? waitStateHolder, bool throwOnNoExec = true)
        {
            waitStateHolder = null;
 
            if (string.IsNullOrEmpty(resolvedFilename))
            {
                Interop.ErrorInfo error = Interop.Error.ENOENT.Info();
                throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, cwd);
            }
 
            int childPid, errno;
 
            // Lock to avoid races with OnSigChild
            // By using a ReaderWriterLock we allow multiple processes to start concurrently.
            ProcessUtils.s_processStartLock.EnterReadLock();
            try
            {
                if (usesTerminal)
                {
                    ProcessUtils.ConfigureTerminalForChildProcesses(1);
                }
 
                // Invoke the shim fork/execve routine.  It will fork a child process,
                // map the provided file handles onto the appropriate stdin/stdout/stderr
                // descriptors, and execve to execute the requested process.  The shim implementation
                // is used to fork/execve as executing managed code in a forked process is not safe (only
                // the calling thread will transfer, thread IDs aren't stable across the fork, etc.)
                errno = Interop.Sys.ForkAndExecProcess(
                    resolvedFilename, argv, env, cwd,
                    setCredentials, userId, groupId, groups,
                    out childPid, stdinHandle, stdoutHandle, stderrHandle,
                    startInfo.StartDetached, inheritedHandles);
 
                if (errno == 0)
                {
                    // Create the wait state holder while still holding the read lock.
                    // This ensures the child process is registered in s_childProcessWaitStates
                    // before the lock is released. If SIGCHLD fires after the lock is released,
                    // CheckChildren will find the child in the table and reap it properly.
                    // Without this, there is a race: SIGCHLD could fire after the lock is released
                    // but before the child is registered, causing WaitForExit to hang indefinitely.
                    waitStateHolder = new ProcessWaitState.Holder(childPid, isNewChild: true, usesTerminal);
                }
            }
            finally
            {
                ProcessUtils.s_processStartLock.ExitReadLock();
            }
 
            if (errno != 0)
            {
                if (usesTerminal)
                {
                    // We failed to launch a child that could use the terminal.
                    ProcessUtils.s_processStartLock.EnterWriteLock();
                    ProcessUtils.ConfigureTerminalForChildProcesses(-1);
                    ProcessUtils.s_processStartLock.ExitWriteLock();
                }
 
                if (!throwOnNoExec &&
                    new Interop.ErrorInfo(errno).Error == Interop.Error.ENOEXEC)
                {
                    return InvalidHandle;
                }
 
                throw ProcessUtils.CreateExceptionForErrorStartingProcess(new Interop.ErrorInfo(errno).GetErrorMessage(), errno, resolvedFilename, cwd);
            }
 
            return new SafeProcessHandle(waitStateHolder!);
        }
    }
}