/// <summary> /// Executes the <paramref name="executable"/> asynchronously and waits a maximum time of <paramref name="maxWaitMs"/> for completion. /// </summary> /// <param name="executable">Program to execute</param> /// <param name="arguments">Program arguments</param> /// <param name="idWrapper"><see cref="WindowsIdentityWrapper"/> used to impersonate the external process</param> /// <param name="debugLogger">Debug logger for debug output</param> /// <param name="priorityClass">Process priority</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <returns>> <see cref="ProcessExecutionResult"/> object that respresents the result of executing the Program</returns> /// <remarks> /// This method throws an exception only if process.Start() fails (in partiular, if the <paramref name="executable"/> doesn't exist). /// Any other error in managed code is signaled by the returned task being set to Faulted state. /// If the program itself does not result in an ExitCode of 0, the returned task ends in RanToCompletion state; /// the ExitCode of the program will be contained in the returned <see cref="ProcessExecutionResult"/>. /// This method is nearly identical to <see cref="ProcessUtils.ExecuteAsync"/>; it is necessary to have this code duplicated /// because AsyncImpersonationProcess hides several methods of the Process class and executing these methods on the base class does /// therefore not work. If this method is changed it is likely that <see cref="ProcessUtils.ExecuteAsync"/> also /// needs to be changed. /// </remarks> internal static Task <ProcessExecutionResult> ExecuteAsync(string executable, string arguments, WindowsIdentityWrapper idWrapper, ILogger debugLogger, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = ProcessUtils.DEFAULT_TIMEOUT) { var tcs = new TaskCompletionSource <ProcessExecutionResult>(); bool exited = false; var process = new AsyncImpersonationProcess(debugLogger) { StartInfo = new ProcessStartInfo(executable, arguments) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, StandardOutputEncoding = ProcessUtils.CONSOLE_ENCODING, StandardErrorEncoding = ProcessUtils.CONSOLE_ENCODING }, EnableRaisingEvents = true }; // We need to read standardOutput and standardError asynchronously to avoid a deadlock // when the buffer is not big enough to receive all the respective output. Otherwise the // process may block because the buffer is full and the Exited event below is never raised. var standardOutput = new StringBuilder(); var standardOutputResults = new TaskCompletionSource <string>(); process.OutputDataReceived += (sender, args) => { if (args.Data != null) { standardOutput.AppendLine(args.Data); } else { standardOutputResults.SetResult(standardOutput.Length > 0 ? ProcessUtils.RemoveEncodingPreamble(standardOutput.ToString()) : null); } }; var standardError = new StringBuilder(); var standardErrorResults = new TaskCompletionSource <string>(); process.ErrorDataReceived += (sender, args) => { if (args.Data != null) { standardError.AppendLine(args.Data); } else { standardErrorResults.SetResult(standardError.Length > 0 ? ProcessUtils.RemoveEncodingPreamble(standardError.ToString()) : null); } }; var processStart = new TaskCompletionSource <bool>(); // The Exited event is raised in any case when the process has finished, i.e. when it gracefully // finished (ExitCode = 0), finished with an error (ExitCode != 0) and when it was killed below. // That ensures disposal of the process object. process.Exited += async(sender, args) => { exited = true; try { await processStart.Task; // standardStreamTasksReady is only disposed when starting the process was not successful, // in which case the Exited event is never raised. // ReSharper disable once AccessToDisposedClosure tcs.TrySetResult(new ProcessExecutionResult { ExitCode = process.ExitCode, // standardStreamTasksReady makes sure that we do not access the standard stream tasks before they are initialized. // For the same reason it is intended that these tasks (as closures) are modified (i.e. initialized). // We need to take this cumbersome way because it is not possible to access the standard streams before the process // is started. If on the other hand the Exited event is raised before the tasks are initialized, we need to make // sure that this method waits until the tasks are initialized before they are accessed. // ReSharper disable PossibleNullReferenceException // ReSharper disable AccessToModifiedClosure StandardOutput = await standardOutputResults.Task, StandardError = await standardErrorResults.Task // ReSharper restore AccessToModifiedClosure // ReSharper restore PossibleNullReferenceException }); } catch (Exception e) { tcs.TrySetException(e); } finally { process.Dispose(); } }; bool processStarted = false; using (var tokenWrapper = idWrapper.TokenWrapper) processStarted = process.StartAsUser(tokenWrapper.Token); processStart.SetResult(processStarted); if (processStarted) { process.BeginOutputReadLine(); process.BeginErrorReadLine(); try { // This call may throw an exception if the process has already exited when we get here. // In that case the Exited event has already set tcs to RanToCompletion state so that // the TrySetException call below does not change the state of tcs anymore. This is correct // as it doesn't make sense to change the priority of the process if it is already finished. // Any other "real" error sets the state of tcs to Faulted below. process.PriorityClass = priorityClass; } catch (InvalidOperationException e) { // This exception indicates that the process is no longer available which is probably // because the process has exited already. The exception should not be logged because // there is no guarantee that the exited event has finished setting the task to the // RanToCompletion state before this exception sets it to the Faulted state. if (!exited && !process.HasExited && tcs.TrySetException(e)) { debugLogger.Error("AsyncImpersonationProcess ({0}): Exception while setting the PriorityClass", e, executable); } } catch (Exception e) { if (tcs.TrySetException(e)) { debugLogger.Error("AsyncImpersonationProcess ({0}): Exception while setting the PriorityClass", e, executable); } } } else { exited = true; standardOutputResults.SetResult(null); standardErrorResults.SetResult(null); debugLogger.Error("AsyncImpersonationProcess ({0}): Could not start process", executable); return(Task.FromResult(new ProcessExecutionResult { ExitCode = Int32.MinValue })); } // Here we take care of the maximum time to wait for the process if such was requested. if (maxWaitMs != ProcessUtils.INFINITE) { Task.Delay(maxWaitMs).ContinueWith(task => { try { // Cancel the state of tcs if it was not set to Faulted or // RanToCompletion before. tcs.TrySetCanceled(); // Always kill the process if is running. if (!exited && !process.HasExited) { process.Kill(); debugLogger.Warn("AsyncImpersonationProcess ({0}): Process was killed because maxWaitMs was reached.", executable); } } // An exception is thrown in process.Kill() when the external process exits // while we set tcs to canceled. In that case there is nothing to do anymore. // This is not an error. In case of other errors that may happen, we log it anyways catch (Exception e) { debugLogger.Error("AsyncImpersonationProcess ({0}): Exception while trying to kill the process", e, executable); } }); } return(tcs.Task); }
/// <summary> /// Executes the <paramref name="executable"/> asynchronously and waits a maximum time of <paramref name="maxWaitMs"/> for completion. /// </summary> /// <param name="executable">Program to execute</param> /// <param name="arguments">Program arguments</param> /// <param name="idWrapper"><see cref="WindowsIdentityWrapper"/> used to impersonate the external process</param> /// <param name="debugLogger">Debug logger for debug output</param> /// <param name="priorityClass">Process priority</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <returns>> <see cref="ProcessExecutionResult"/> object that respresents the result of executing the Program</returns> /// <remarks> /// This method throws an exception only if process.Start() fails (in partiular, if the <paramref name="executable"/> doesn't exist). /// Any other error in managed code is signaled by the returned task being set to Faulted state. /// If the program itself does not result in an ExitCode of 0, the returned task ends in RanToCompletion state; /// the ExitCode of the program will be contained in the returned <see cref="ProcessExecutionResult"/>. /// This method is nearly identical to <see cref="ProcessUtils.ExecuteAsync"/>; it is necessary to have this code duplicated /// because AsyncImpersonationProcess hides several methods of the Process class and executing these methods on the base class does /// therefore not work. If this method is changed it is likely that <see cref="ProcessUtils.ExecuteAsync"/> also /// needs to be changed. /// </remarks> internal static Task<ProcessExecutionResult> ExecuteAsync(string executable, string arguments, WindowsIdentityWrapper idWrapper, ILogger debugLogger, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = ProcessUtils.DEFAULT_TIMEOUT) { var tcs = new TaskCompletionSource<ProcessExecutionResult>(); var process = new AsyncImpersonationProcess(debugLogger) { StartInfo = new ProcessStartInfo(executable, arguments) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = ProcessUtils.CONSOLE_ENCODING, StandardErrorEncoding = ProcessUtils.CONSOLE_ENCODING }, EnableRaisingEvents = true }; // We need to read standardOutput and standardError asynchronously to avoid a deadlock // when the buffer is not big enough to receive all the respective output. Otherwise the // process may block because the buffer is full and the Exited event below is never raised. Task<string> standardOutputTask = null; Task<string> standardErrorTask = null; var standardStreamTasksReady = new ManualResetEventSlim(); // The Exited event is raised in any case when the process has finished, i.e. when it gracefully // finished (ExitCode = 0), finished with an error (ExitCode != 0) and when it was killed below. // That ensures disposal of the process object. process.Exited += (sender, args) => { try { // standardStreamTasksReady is only disposed when starting the process was not successful, // in which case the Exited event is never raised. // ReSharper disable once AccessToDisposedClosure standardStreamTasksReady.Wait(); tcs.TrySetResult(new ProcessExecutionResult { ExitCode = process.ExitCode, // standardStreamTasksReady makes sure that we do not access the standard stream tasks before they are initialized. // For the same reason it is intended that these tasks (as closures) are modified (i.e. initialized). // We need to take this cumbersome way because it is not possible to access the standard streams before the process // is started. If on the other hand the Exited event is raised before the tasks are initialized, we need to make // sure that this method waits until the tasks are initialized before they are accessed. // ReSharper disable PossibleNullReferenceException // ReSharper disable AccessToModifiedClosure StandardOutput = standardOutputTask.Result, StandardError = standardErrorTask.Result // ReSharper restore AccessToModifiedClosure // ReSharper restore PossibleNullReferenceException }); } catch (Exception e) { debugLogger.Error("AsyncImpersonationProcess ({0}): Exception while executing the Exited handler", e, executable); tcs.TrySetException(e); } finally { process.Dispose(); } }; using (var tokenWrapper = idWrapper.TokenWrapper) if (!process.StartAsUser(tokenWrapper.Token)) { debugLogger.Error("AsyncImpersonationProcess ({0}): Could not start process", executable); standardStreamTasksReady.Dispose(); return Task.FromResult(new ProcessExecutionResult { ExitCode = int.MinValue }); } try { // This call may throw an exception if the process has already exited when we get here. // In that case the Exited event has already set tcs to RanToCompletion state so that // the TrySetException call below does not change the state of tcs anymore. This is correct // as it doesn't make sense to change the priority of the process if it is already finished. // Any other "real" error sets the state of tcs to Faulted below. process.PriorityClass = priorityClass; } catch (Exception e) { debugLogger.Error("AsyncImpersonationProcess ({0}): Exception while setting the PriorityClass", e, executable); tcs.TrySetException(e); } standardOutputTask = process.StandardOutput.ReadToEndAsync(); standardErrorTask = process.StandardError.ReadToEndAsync(); standardStreamTasksReady.Set(); // Here we take care of the maximum time to wait for the process if such was requested. if (maxWaitMs != ProcessUtils.INFINITE) Task.Delay(maxWaitMs).ContinueWith(task => { try { // We only kill the process if the state of tcs was not set to Faulted or // RanToCompletion before. if (tcs.TrySetCanceled()) { process.Kill(); debugLogger.Warn("AsyncImpersonationProcess ({0}): Process was killed because maxWaitMs was reached.", executable); } } // An exception is thrown in process.Kill() when the external process exits // while we set tcs to canceled. In that case there is nothing to do anymore. // This is not an error. In case of other errors that may happen, we log it anyways catch (Exception e) { debugLogger.Error("AsyncImpersonationProcess ({0}): Exception while trying to kill the process", e, executable); } }); return tcs.Task; }