public void Dispose() { if (!m_disposed) { using (var semaphoreReleaser = m_syncSemaphore.AcquireSemaphore()) { StopWaiting(semaphoreReleaser); WaitUntilErrorAndOutputEof(true, semaphoreReleaser).GetAwaiter().GetResult(); if (m_processInjector != null) { // We may have already called Stop() in CompletionCallback, but that's okay. m_processInjector.Stop().GetAwaiter().GetResult(); m_processInjector.Dispose(); m_processInjector = null; } if (m_processHandle != null) { m_processHandle.Dispose(); m_processHandle = null; } if (m_job != null) { m_job.Dispose(); m_job = null; } } m_syncSemaphore.Dispose(); m_disposed = true; } }
private async void CompletionCallback(object context, bool timedOut) { if (timedOut) { Volatile.Write(ref m_timedout, true); // Attempt to dump the timed out process before killing it if (!m_processHandle.IsInvalid && !m_processHandle.IsClosed && m_workingDirectory != null) { DumpFileDirectory = m_timeoutDumpDirectory; Exception dumpCreationException; try { Directory.CreateDirectory(DumpFileDirectory); } catch (Exception ex) { DumpCreationException = ex; } if (DumpCreationException == null && !ProcessDumper.TryDumpProcessAndChildren( parentProcessId: m_processId, dumpDirectory: DumpFileDirectory, primaryDumpCreationException: out dumpCreationException)) { DumpCreationException = dumpCreationException; } } Kill(ExitCodes.Timeout); using (await m_syncSemaphore.AcquireAsync()) { if (m_processWaitHandle != null) { await m_processWaitHandle; m_exited = true; } } } else { m_exited = true; } using (var semaphoreReleaser = await m_syncSemaphore.AcquireAsync()) { Contract.Assume(m_waiting, "CompletionCallback should only be triggered once."); StopWaiting(semaphoreReleaser); } try { await Task.Run( async() => { // Before waiting on anything, we call the processExiting callback. // This callback happens to be responsible for triggering or forcing // cleanup of all processes in this job. We can't finish waiting on pipe EOF // (error, output, report, and process-injector pipes) until all handles // to the write-sides are closed. Func <Task> processExiting = m_processExitingAsync; if (processExiting != null) { await processExiting(); } using (var semaphoreReleaser = await m_syncSemaphore.AcquireAsync()) { // Error and output pipes: Finish reading and then expect EOF (see above). await WaitUntilErrorAndOutputEof(false, semaphoreReleaser); // Stop process injection service. This finishes reading the injector control pipe (for injection requests). // Since we don't get to the 'end' of the pipe until all child-processes holding on to it exit, we must // perform this wait after processExiting() above. if (m_processInjector != null) { // Stop() discards all unhandled requests. That is only safe to do since we are assuming that all processes // in the job have exited (so those requests aren't relevant anymore) await m_processInjector.Stop(); m_hasDetoursFailures = m_processInjector.HasDetoursInjectionFailures; m_processInjector.Dispose(); m_processInjector = null; } } // Now, callback for additional cleanup (can safely wait on extra pipes, such as the SandboxedProcess report pipe, // if processExiting causes process tree teardown; see above). var processExited = m_processExited; if (processExited != null) { await processExited(); } }); } catch (Exception exception) { // Something above may fail and that has to be observed. Unfortunately, throwing a normal exception in a continuation // just means someone has to observe the continuation. So we tug on some bootstraps by killing the process here. // TODO: It'd be nice if we had a FailFast equivalent that went through AppDomain.UnhandledExceptionEvent for logging. ExceptionHandling.OnFatalException(exception, "FailFast in DetouredProcess completion callback"); } }
public void Start( Guid payloadGuid, ArraySegment <byte> payloadData, SafeFileHandle inheritableReportHandle, string dllNameX64, string dllNameX86) { using (m_syncSemaphore.AcquireSemaphore()) { if (m_starting || m_disposed) { throw new InvalidOperationException("Cannot invoke start process more than once or after this instance has been Disposed."); } m_starting = true; // The process creation flags // We use CREATE_DEFAULT_ERROR_MODE to ensure that the hard error mode of the child process (i.e., GetErrorMode) // is deterministic. Inheriting error mode is the default, but there may be some concurrent operation that temporarily // changes it (process global). The CLR has been observed to do so. // We use CREATE_NO_WINDOW in case BuildXL is attached to a console windows to prevent child processes from messing up // the console window. If BuildXL itself is started without a console window the flag is not set to prevent creating // extra conhost.exe processes. int creationFlags = ((s_consoleWindow == IntPtr.Zero && !this.m_disableConHostSharing) ? 0 : Native.Processes.ProcessUtilities.CREATE_NO_WINDOW) | Native.Processes.ProcessUtilities.CREATE_DEFAULT_ERROR_MODE; SafeFileHandle standardInputWritePipeHandle = null; SafeFileHandle standardOutputReadPipeHandle = null; SafeFileHandle standardErrorReadPipeHandle = null; try { // set up the environment block parameter var environmentHandle = default(GCHandle); var payloadHandle = default(GCHandle); SafeFileHandle hStdInput = null; SafeFileHandle hStdOutput = null; SafeFileHandle hStdError = null; SafeThreadHandle threadHandle = null; try { IntPtr environmentPtr = IntPtr.Zero; if (m_unicodeEnvironmentBlock != null) { creationFlags |= Native.Processes.ProcessUtilities.CREATE_UNICODE_ENVIRONMENT; environmentHandle = GCHandle.Alloc(m_unicodeEnvironmentBlock, GCHandleType.Pinned); environmentPtr = environmentHandle.AddrOfPinnedObject(); } Pipes.CreateInheritablePipe( Pipes.PipeInheritance.InheritRead, Pipes.PipeFlags.WriteSideAsync, readHandle: out hStdInput, writeHandle: out standardInputWritePipeHandle); Pipes.CreateInheritablePipe( Pipes.PipeInheritance.InheritWrite, Pipes.PipeFlags.ReadSideAsync, readHandle: out standardOutputReadPipeHandle, writeHandle: out hStdOutput); Pipes.CreateInheritablePipe( Pipes.PipeInheritance.InheritWrite, Pipes.PipeFlags.ReadSideAsync, readHandle: out standardErrorReadPipeHandle, writeHandle: out hStdError); // We want a per-process job primarily. If nested job support is not available, then we make sure to not have a BuildXL-level job. if (JobObject.OSSupportsNestedJobs) { JobObject.SetTerminateOnCloseOnCurrentProcessJob(); } // Initialize the injector m_processInjector = new ProcessTreeContext(payloadGuid, inheritableReportHandle, payloadData, dllNameX64, dllNameX86, m_loggingContext); // If path remapping is enabled then we wrap the job object in a container, so the filter drivers get // configured (and they get cleaned up when the container is disposed) if (m_containerConfiguration.IsIsolationEnabled) { m_job = new Container( name: null, containerConfiguration: m_containerConfiguration, loggingContext: m_loggingContext); } else { m_job = new JobObject(null); } // We want the effects of SEM_NOGPFAULTERRORBOX on all children (but can't set that with CreateProcess). // That's not set otherwise (even if set in this process) due to CREATE_DEFAULT_ERROR_MODE above. m_job.SetLimitInformation(terminateOnClose: true, failCriticalErrors: false); m_processInjector.Listen(); if (m_containerConfiguration.IsIsolationEnabled) { // After calling SetLimitInformation, start up the container if present // This will throw if the container is not set up properly m_job.StartContainerIfPresent(); } // The call to the CreateDetouredProcess below will add a newly created process to the job. System.Diagnostics.Stopwatch m_startUpTimeWatch = System.Diagnostics.Stopwatch.StartNew(); var detouredProcessCreationStatus = Native.Processes.ProcessUtilities.CreateDetouredProcess( m_commandLine, creationFlags, environmentPtr, m_workingDirectory, hStdInput, hStdOutput, hStdError, m_job, m_processInjector.Injector, m_containerConfiguration.IsIsolationEnabled, out m_processHandle, out threadHandle, out m_processId, out int errorCode); m_startUpTimeWatch.Stop(); m_startUpTime = m_startUpTimeWatch.ElapsedMilliseconds; if (detouredProcessCreationStatus != CreateDetouredProcessStatus.Succeeded) { // TODO: Indicating user vs. internal errors (and particular phase failures e.g. adding to job object or injecting detours) // is good progress on the transparency into these failures. But consider making this indication visible beyond this // function without throwing exceptions; consider returning a structured value or logging events. string message; if (detouredProcessCreationStatus.IsDetoursSpecific()) { message = string.Format( CultureInfo.InvariantCulture, "Internal error during process creation: {0:G}", detouredProcessCreationStatus); } else if (detouredProcessCreationStatus == CreateDetouredProcessStatus.ProcessCreationFailed) { message = "Process creation failed"; } else { message = string.Format( CultureInfo.InvariantCulture, "Process creation failed: {0:G}", detouredProcessCreationStatus); } throw new BuildXLException( message, new NativeWin32Exception(errorCode)); } // TODO: We should establish good post-conditions for CreateDetouredProcess. As a temporary measure, it would be nice // to determine if we are sometimes getting invalid process handles with retVal == true. So for now we differentiate // that possible case with a unique error string. if (m_processHandle.IsInvalid) { throw new BuildXLException("Unable to start or detour a process (process handle invalid)", new NativeWin32Exception(errorCode)); } } finally { if (environmentHandle.IsAllocated) { environmentHandle.Free(); } if (payloadHandle.IsAllocated) { payloadHandle.Free(); } if (hStdInput != null && !hStdInput.IsInvalid) { hStdInput.Dispose(); } if (hStdOutput != null && !hStdOutput.IsInvalid) { hStdOutput.Dispose(); } if (hStdError != null && !hStdError.IsInvalid) { hStdError.Dispose(); } if (inheritableReportHandle != null && !inheritableReportHandle.IsInvalid) { inheritableReportHandle.Dispose(); } if (threadHandle != null && !threadHandle.IsInvalid) { threadHandle.Dispose(); } } var standardInputStream = new FileStream(standardInputWritePipeHandle, FileAccess.Write, m_bufferSize, isAsync: true); m_standardInputWriter = new StreamWriter(standardInputStream, m_standardInputEncoding, m_bufferSize) { AutoFlush = true }; var standardOutputFile = AsyncFileFactory.CreateAsyncFile( standardOutputReadPipeHandle, FileDesiredAccess.GenericRead, ownsHandle: true, kind: FileKind.Pipe); m_outputReader = new AsyncPipeReader(standardOutputFile, m_outputDataReceived, m_standardOutputEncoding, m_bufferSize); m_outputReader.BeginReadLine(); var standardErrorFile = AsyncFileFactory.CreateAsyncFile( standardErrorReadPipeHandle, FileDesiredAccess.GenericRead, ownsHandle: true, kind: FileKind.Pipe); m_errorReader = new AsyncPipeReader(standardErrorFile, m_errorDataReceived, m_standardErrorEncoding, m_bufferSize); m_errorReader.BeginReadLine(); Contract.Assert(!m_processHandle.IsInvalid); m_processWaitHandle = new SafeWaitHandleFromSafeHandle(m_processHandle); m_waiting = true; TimeSpan timeout = m_timeout ?? Timeout.InfiniteTimeSpan; m_registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject( m_processWaitHandle, CompletionCallback, null, timeout, true); m_started = true; } catch (Exception) { // Dispose pipe handles in case they are not assigned to streams. if (m_standardInputWriter == null) { standardInputWritePipeHandle?.Dispose(); } if (m_outputReader == null) { standardOutputReadPipeHandle?.Dispose(); } if (m_errorReader == null) { standardErrorReadPipeHandle?.Dispose(); } throw; } } }