/// <summary> /// Tries to impersonate a <see cref="WindowsIdentityWrapper"/> /// </summary> /// <param name="idWrapper"><see cref="WindowsIdentityWrapper"/> to impersonate</param> /// <param name="ctxWrapper"><see cref="WindowsImpersonationContextWrapper"/> resulting from the impersonation</param> /// <returns><c>true</c> if impersonation was successful; otherwiese <c>false</c></returns> private bool TryImpersonate(WindowsIdentityWrapper idWrapper, out WindowsImpersonationContextWrapper ctxWrapper) { try { ctxWrapper = idWrapper.Impersonate(); return(true); } catch (Exception e) { _debugLogger.Error("ImpersonationService: Error when trying to impersonate User '{0}' (Domain '{1}')", e, idWrapper.UserName, idWrapper.Domain); ctxWrapper = null; return(false); } }
/// <summary> /// Tries to impersonate a <see cref="WindowsIdentityWrapper"/> /// </summary> /// <param name="idWrapper"><see cref="WindowsIdentityWrapper"/> to impersonate</param> /// <param name="ctxWrapper"><see cref="WindowsImpersonationContextWrapper"/> resulting from the impersonation</param> /// <returns><c>true</c> if impersonation was successful; otherwiese <c>false</c></returns> private bool TryImpersonate(WindowsIdentityWrapper idWrapper, out WindowsImpersonationContextWrapper ctxWrapper) { try { ctxWrapper = idWrapper.Impersonate(); return true; } catch (Exception e) { _debugLogger.Error("ImpersonationService: Error when trying to impersonate User '{0}' (Domain '{1}')", e, idWrapper.UserName, idWrapper.Domain); ctxWrapper = null; return false; } }
/// <summary> /// Registers a <see cref="NetworkCredential"/> for a given <see cref="ResourcePath"/> /// </summary> /// <param name="path"> /// For this <see cref="ResourcePath"/> and all subpaths the <see cref="credential"/> is impersonated, /// assuming that no better matching path is registered. /// </param> /// <param name="credential"><see cref="NetworkCredential"/> to impersonate for accessing <paramref name="path"/></param> /// <returns><c>true</c> if registration was successful; otherwise <c>false</c></returns> public bool TryRegisterCredential(ResourcePath path, NetworkCredential credential) { _debugLogger.Info("ImpersonationService: Trying to register credential (User: '******' Domain: '{1}') for ResourcePath '{2}'", credential.UserName, credential.Domain, path); // If there is already a credential registered for exactly the same ResourcePath, // we unregister the old credential and log a warning. It should have been // unregistered with TryUnregisterCredential before. WindowsIdentityWrapper oldIdWrapper; if (_ids.TryRemove(path, out oldIdWrapper)) { _debugLogger.Warn("ImpersonationService: There was already a credential registered For ResourcePath '{0}'. The old credential was unregistered.", path); oldIdWrapper.Dispose(); } var logonHelper = new LogonHelper(_debugLogger); WindowsIdentity id; // We use LogonType.NewCredentials because this logon type allows the caller to clone its current token // and specify new credentials only for outbound connections. The new logon session has the same local // identifier but uses different credentials for other network connections. // This logon type is only supported by LogonProvider.WinNt50. if (logonHelper.TryLogon(credential, LogonHelper.LogonType.NewCredentials, LogonHelper.LogonProvider.WinNt50, out id)) { var idWrapper = new WindowsIdentityWrapper(id, credential); if (!_ids.TryAdd(path, idWrapper)) { // In a multithreaded environment, a new credential could have been added // despite the TryUnregisterCredential call above in the meantime. _debugLogger.Error("ImpersonationService: For ResourcePath '{0}' there was already a credential registered. Cannot register new credential.", path); idWrapper.Dispose(); return(false); } _debugLogger.Info("ImpersonationService: Successfully registered credential for ResourcePath '{0}': User: '******' (Domain: '{2}')", path, idWrapper.UserName, idWrapper.Domain); return(true); } _debugLogger.Error("ImpersonationService: Could not register credential for ResourcePath '{0}': User: '******' (Domain: '{2}')", path, credential.UserName, credential.Domain); return(false); }
/// <summary> /// Tries to find the best matching <see cref="WindowsIdentityWrapper"/> for a given <see cref="ResourcePath"/> /// </summary> /// <param name="path"><see cref="ResourcePath"/> for which a <see cref="WindowsIdentityWrapper"/> is needed</param> /// <param name="idWrapper"><see cref="WindowsIdentityWrapper"/> that matches best for <see cref="path"/></param> /// <returns><c>true</c> if a <see cref="WindowsIdentityWrapper"/> was found; otherwise <c>false</c></returns> /// <remarks> /// Assuming the following credentials are registered: /// - User1: {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/ /// - User2: {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/Directory_X/ /// This method returns the following results for the given <see cref="ResourcePath"/>s: /// - User1 for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/ /// - User1 for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/Directory_Y/ /// - User2 for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/Directory_X/ /// - User2 for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/Directory_X/Subdirectory/ /// - null for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_B/ /// </remarks> private bool TryGetBestMatchingIdentityForPath(ResourcePath path, out WindowsIdentityWrapper idWrapper) { idWrapper = null; var pathLength = 0; var pathString = path.ToString(); foreach (var kvp in _ids) { var keyString = kvp.Key.ToString(); if (!pathString.StartsWith(keyString)) { continue; } var keyLength = keyString.Length; if (keyLength <= pathLength) { continue; } pathLength = keyLength; idWrapper = kvp.Value; } return(idWrapper != null); }
/// <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); }
/// <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> /// Registers a <see cref="NetworkCredential"/> for a given <see cref="ResourcePath"/> /// </summary> /// <param name="path"> /// For this <see cref="ResourcePath"/> and all subpaths the <see cref="credential"/> is impersonated, /// assuming that no better matching path is registered. /// </param> /// <param name="credential"><see cref="NetworkCredential"/> to impersonate for accessing <paramref name="path"/></param> /// <returns><c>true</c> if registration was successful; otherwise <c>false</c></returns> public bool TryRegisterCredential(ResourcePath path, NetworkCredential credential) { _debugLogger.Info("ImpersonationService: Trying to register credential (User: '******' Domain: '{1}') for ResourcePath '{2}'", credential.UserName, credential.Domain, path); // If there is already a credential registered for exactly the same ResourcePath, // we unregister the old credential and log a warning. It should have been // unregistered with TryUnregisterCredential before. WindowsIdentityWrapper oldIdWrapper; if (_ids.TryRemove(path, out oldIdWrapper)) { _debugLogger.Warn("ImpersonationService: There was already a credential registered For ResourcePath '{0}'. The old credential was unregistered.", path); oldIdWrapper.Dispose(); } var logonHelper = new LogonHelper(_debugLogger); WindowsIdentity id; // We use LogonType.NewCredentials because this logon type allows the caller to clone its current token // and specify new credentials only for outbound connections. The new logon session has the same local // identifier but uses different credentials for other network connections. // This logon type is only supported by LogonProvider.WinNt50. if (logonHelper.TryLogon(credential, LogonHelper.LogonType.NewCredentials, LogonHelper.LogonProvider.WinNt50, out id)) { var idWrapper = new WindowsIdentityWrapper(id, credential); if(!_ids.TryAdd(path, idWrapper)) { // In a multithreaded environment, a new credential could have been added // despite the TryUnregisterCredential call above in the meantime. _debugLogger.Error("ImpersonationService: For ResourcePath '{0}' there was already a credential registered. Cannot register new credential.", path); idWrapper.Dispose(); return false; } _debugLogger.Info("ImpersonationService: Successfully registered credential for ResourcePath '{0}': User: '******' (Domain: '{2}')", path, idWrapper.UserName, idWrapper.Domain); return true; } _debugLogger.Error("ImpersonationService: Could not register credential for ResourcePath '{0}': User: '******' (Domain: '{2}')", path, credential.UserName, credential.Domain); return false; }
/// <summary> /// Tries to find the best matching <see cref="WindowsIdentityWrapper"/> for a given <see cref="ResourcePath"/> /// </summary> /// <param name="path"><see cref="ResourcePath"/> for which a <see cref="WindowsIdentityWrapper"/> is needed</param> /// <param name="idWrapper"><see cref="WindowsIdentityWrapper"/> that matches best for <see cref="path"/></param> /// <returns><c>true</c> if a <see cref="WindowsIdentityWrapper"/> was found; otherwise <c>false</c></returns> /// <remarks> /// Assuming the following credentials are registered: /// - User1: {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/ /// - User2: {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/Directory_X/ /// This method returns the following results for the given <see cref="ResourcePath"/>s: /// - User1 for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/ /// - User1 for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/Directory_Y/ /// - User2 for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/Directory_X/ /// - User2 for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_A/Directory_X/Subdirectory/ /// - null for {03dd2da6-4da8-4d3e-9e55-80e3165729a3}:////Computer/Share_B/ /// </remarks> private bool TryGetBestMatchingIdentityForPath(ResourcePath path, out WindowsIdentityWrapper idWrapper) { idWrapper = null; var pathLength = 0; var pathString = path.ToString(); foreach (var kvp in _ids) { var keyString = kvp.Key.ToString(); if (!pathString.StartsWith(keyString)) continue; var keyLength = keyString.Length; if (keyLength <= pathLength) continue; pathLength = keyLength; idWrapper = kvp.Value; } return (idWrapper != null); }
/// <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; }