public DurationLogger(string operationName, LogSource logSource, bool isEnabled) { Helpers.Argument.ValidateIsNotNull(logSource, nameof(logSource)); Helpers.Argument.ValidateIsNotNullOrWhitespace(operationName, nameof(operationName)); _operationName = operationName; _logSource = logSource; // We enable the duration logging feature to be easily switched off via flag. if (!isEnabled) { return; } _stopwatch = new Stopwatch(); _stopwatch.Start(); _logSource.Debug($"{_operationName} - started."); }
internal void Start() { if (Process != null) { throw new InvalidOperationException("The instance has already been started."); } _log.Debug($"Executing: {ExecutablePath} {CensoredArguments}"); StreamWriter outputFileWriter = null; if (!string.IsNullOrWhiteSpace(OutputFilePath)) { // Make sure the file can be created - parent directory exists. var parent = Path.GetDirectoryName(OutputFilePath); // No need to create it if it is a relative path with no parent. if (!string.IsNullOrWhiteSpace(parent)) { Directory.CreateDirectory(parent); } // Create the file. outputFileWriter = File.CreateText(OutputFilePath); } try { var startInfo = new ProcessStartInfo { Arguments = Arguments, ErrorDialog = false, FileName = ExecutablePath, RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, WorkingDirectory = WorkingDirectory, CreateNoWindow = true }; if (EnvironmentVariables != null) { foreach (var pair in EnvironmentVariables) { startInfo.EnvironmentVariables[pair.Key] = pair.Value; } } if (_standardInputProvider != null) { startInfo.RedirectStandardInput = true; } string standardError = null; string standardOutput = null; var runtime = Stopwatch.StartNew(); using (new CrashDialogSuppressionBlock()) Process = Process.Start(startInfo); // We default all external tools to below normal because they are, as a rule, less // important than fast responsive UX, so the system should not be bogged down by them. try { Process.PriorityClass = ProcessPriorityClass.BelowNormal; } catch (InvalidOperationException) { // If the process has already exited, we get this on Windows. This is fine. } catch (Win32Exception ex) when(ex.NativeErrorCode == 3) // "No such process" { // If the process has already exited, we get this on Linux. This is fine. } // These are only set if they are created by ExternalTool - we don't care about user threads. Thread standardErrorReader = null; Thread standardOutputReader = null; if (_standardErrorConsumer != null) { // Caller wants to have it. Okay, fine. Helpers.Async.BackgroundThreadInvoke(delegate { _standardErrorConsumer(Process.StandardError.BaseStream); }); } else { // We'll store it ourselves. standardErrorReader = new Thread((ThreadStart) delegate { // This should be safe if the process we are starting is well-behaved (i.e. not ADB). standardError = Process.StandardError.ReadToEnd(); }); standardErrorReader.Start(); } if (_standardOutputConsumer != null) { // Caller wants to have it. Okay, fine. We do not need to track this thread. Helpers.Async.BackgroundThreadInvoke(delegate { _standardOutputConsumer(Process.StandardOutput.BaseStream); }); } else { // We'll store it ourselves. standardOutputReader = new Thread((ThreadStart) delegate { // This should be safe if the process we are starting is well-behaved (i.e. not ADB). standardOutput = Process.StandardOutput.ReadToEnd(); }); standardOutputReader.Start(); } if (_standardInputProvider != null) { // We don't care about monitoring this later, since ExternalTool does not need to touch stdin. Helpers.Async.BackgroundThreadInvoke(delegate { // Closing stdin after providing input is critical or the app may just hang forever. using (var stdin = Process.StandardInput.BaseStream) _standardInputProvider(stdin); }); } var resultThread = new Thread((ThreadStart) delegate { Process.WaitForExit(); runtime.Stop(); // NB! Streams may stay open and blocked after process exits. // This happens e.g. if you go cmd.exe -> start.exe. // Even if you kill cmd.exe, start.exe remains and keeps the pipes open. standardErrorReader?.Join(); standardOutputReader?.Join(); if (outputFileWriter != null) { if (standardOutput != null) { outputFileWriter.WriteLine(standardOutput); } if (standardError != null) { outputFileWriter.WriteLine(standardError); } outputFileWriter.Dispose(); } _result.TrySetResult(new ExternalToolResult(this, standardOutput, standardError, Process.ExitCode, runtime.Elapsed)); }); // All the rest happens in the result thread, which waits for the process to exit. resultThread.Start(); } catch (Exception) { // Don't leave this lingering if starting the process fails. outputFileWriter?.Dispose(); throw; } }