/// <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;
    }