private bool ValidateAndSetProcessId(int processId, string name) { if (!ProcessLauncher.Launcher.HasChildProc) { if (name != null) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(false); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(false); } } else if (processId == 0) { Console.WriteLine("Must specify either --name or --process-id option to start collecting."); return(false); } } _processId = processId; return(true); }
public async Task <int> Collect(CancellationToken ct, List <string> counter_list, IConsole console, int processId, int refreshInterval, CountersExportFormat format, string output, string name) { if (name != null) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(0); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(0); } } try { _ct = ct; _counterList = counter_list; // NOTE: This variable name has an underscore because that's the "name" that the CLI displays. System.CommandLine doesn't like it if we change the variable to camelcase. _console = console; _processId = processId; _interval = refreshInterval; _output = output; _diagnosticsClient = new DiagnosticsClient(processId); if (_output.Length == 0) { _console.Error.WriteLine("Output cannot be an empty string"); return(0); } if (format == CountersExportFormat.csv) { _renderer = new CSVExporter(output); } else if (format == CountersExportFormat.json) { // Try getting the process name. string processName = ""; try { processName = Process.GetProcessById(_processId).ProcessName; } catch (Exception) { } _renderer = new JSONExporter(output, processName);; } else { _console.Error.WriteLine($"The output format {format} is not a valid output format."); return(0); } return(await Start()); } catch (OperationCanceledException) { } return(1); }
public async Task <int> Monitor(CancellationToken ct, List <string> counter_list, IConsole console, int processId, int refreshInterval, string name) { if (name != null) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(0); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(0); } } try { _ct = ct; _counterList = counter_list; // NOTE: This variable name has an underscore because that's the "name" that the CLI displays. System.CommandLine doesn't like it if we change the variable to camelcase. _console = console; _processId = processId; _interval = refreshInterval; _renderer = new ConsoleWriter(); _diagnosticsClient = new DiagnosticsClient(processId); return(await Start()); } catch (OperationCanceledException) { try { _session.Stop(); } catch (Exception) {} // Swallow all exceptions for now. console.Out.WriteLine($"Complete"); return(1); } }
/// <summary> /// Collects a diagnostic trace from a currently running process. /// </summary> /// <param name="ct">The cancellation token</param> /// <param name="console"></param> /// <param name="processId">The process to collect the trace from.</param> /// <param name="name">The name of process to collect the trace from.</param> /// <param name="output">The output path for the collected trace data.</param> /// <param name="buffersize">Sets the size of the in-memory circular buffer in megabytes.</param> /// <param name="providers">A list of EventPipe providers to be enabled. This is in the form 'Provider[,Provider]', where Provider is in the form: 'KnownProviderName[:Flags[:Level][:KeyValueArgs]]', and KeyValueArgs is in the form: '[key1=value1][;key2=value2]'</param> /// <param name="profile">A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly.</param> /// <param name="format">The desired format of the created trace file.</param> /// <param name="duration">The duration of trace to be taken. </param> /// <param name="clrevents">A list of CLR events to be emitted.</param> /// <param name="clreventlevel">The verbosity level of CLR events</param> /// <returns></returns> private static async Task <int> Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name) { try { Debug.Assert(output != null); Debug.Assert(profile != null); // Either processName or processId has to be specified. if (name != null) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(ErrorCodes.ArgumentError); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(ErrorCodes.ArgumentError); } } if (processId < 0) { Console.Error.WriteLine("Process ID should not be negative."); return(ErrorCodes.ArgumentError); } else if (processId == 0) { Console.Error.WriteLine("--process-id is required"); return(ErrorCodes.ArgumentError); } if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { Console.Out.WriteLine("No profile or providers specified, defaulting to trace profile 'cpu-sampling'"); profile = "cpu-sampling"; } Dictionary <string, string> enabledBy = new Dictionary <string, string>(); var providerCollection = Extensions.ToProviders(providers); foreach (EventPipeProvider providerCollectionProvider in providerCollection) { enabledBy[providerCollectionProvider.Name] = "--providers "; } if (profile.Length != 0) { var selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); if (selectedProfile == null) { Console.Error.WriteLine($"Invalid profile name: {profile}"); return(ErrorCodes.ArgumentError); } Profile.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); } // Parse --clrevents parameter if (clrevents.Length != 0) { // Ignore --clrevents if CLR event provider was already specified via --profile or --providers command. if (enabledBy.ContainsKey(Extensions.CLREventProviderName)) { Console.WriteLine($"The argument --clrevents {clrevents} will be ignored because the CLR provider was configured via either --profile or --providers command."); } else { var clrProvider = Extensions.ToCLREventPipeProvider(clrevents, clreventlevel); providerCollection.Add(clrProvider); enabledBy[Extensions.CLREventProviderName] = "--clrevents"; } } if (providerCollection.Count <= 0) { Console.Error.WriteLine("No providers were specified to start a trace."); return(ErrorCodes.ArgumentError); } PrintProviders(providerCollection, enabledBy); var process = Process.GetProcessById(processId); var shouldExit = new ManualResetEvent(false); var shouldStopAfterDuration = duration != default(TimeSpan); var rundownRequested = false; System.Timers.Timer durationTimer = null; ct.Register(() => shouldExit.Set()); var diagnosticsClient = new DiagnosticsClient(processId); using (VirtualTerminalMode vTermMode = VirtualTerminalMode.TryEnable()) { EventPipeSession session = null; try { session = diagnosticsClient.StartEventPipeSession(providerCollection, true, (int)buffersize); } catch (DiagnosticsClientException e) { Console.Error.WriteLine($"Unable to start a tracing session: {e.ToString()}"); } if (session == null) { Console.Error.WriteLine("Unable to create session."); return(ErrorCodes.SessionCreationError); } if (shouldStopAfterDuration) { durationTimer = new System.Timers.Timer(duration.TotalMilliseconds); durationTimer.Elapsed += (s, e) => shouldExit.Set(); durationTimer.AutoReset = false; } var stopwatch = new Stopwatch(); durationTimer?.Start(); stopwatch.Start(); LineRewriter rewriter = null; using (var fs = new FileStream(output.FullName, FileMode.Create, FileAccess.Write)) { Console.Out.WriteLine($"Process : {process.MainModule.FileName}"); Console.Out.WriteLine($"Output File : {fs.Name}"); if (shouldStopAfterDuration) { Console.Out.WriteLine($"Trace Duration : {duration.ToString(@"dd\:hh\:mm\:ss")}"); } Console.Out.WriteLine("\n\n"); var fileInfo = new FileInfo(output.FullName); Task copyTask = session.EventStream.CopyToAsync(fs); if (!Console.IsOutputRedirected) { rewriter = new LineRewriter { LineToClear = Console.CursorTop - 1 }; Console.CursorVisible = false; } Action printStatus = () => { if (!Console.IsOutputRedirected) { rewriter?.RewriteConsoleLine(); } fileInfo.Refresh(); Console.Out.WriteLine($"[{stopwatch.Elapsed.ToString(@"dd\:hh\:mm\:ss")}]\tRecording trace {GetSize(fileInfo.Length)}"); Console.Out.WriteLine("Press <Enter> or <Ctrl+C> to exit..."); if (rundownRequested) { Console.Out.WriteLine("Stopping the trace. This may take up to minutes depending on the application being traced."); } }; while (!shouldExit.WaitOne(100) && !(!Console.IsInputRedirected && Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Enter)) { printStatus(); } // Behavior concerning Enter moving text in the terminal buffer when at the bottom of the buffer // is different between Console/Terminals on Windows and Mac/Linux if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !Console.IsOutputRedirected && rewriter != null && Math.Abs(Console.CursorTop - Console.BufferHeight) == 1) { rewriter.LineToClear--; } durationTimer?.Stop(); rundownRequested = true; session.Stop(); do { printStatus(); } while (!copyTask.Wait(100)); } Console.Out.WriteLine("\nTrace completed."); if (format != TraceFileFormat.NetTrace) { TraceFileFormatConverter.ConvertToFormat(format, output.FullName); } } } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); return(ErrorCodes.TracingError); } finally { if (console.GetTerminal() != null) { Console.CursorVisible = true; } } return(await Task.FromResult(0)); }
/// <summary> /// Reports a stack trace /// </summary> /// <param name="ct">The cancellation token</param> /// <param name="console"></param> /// <param name="processId">The process to report the stack from.</param> /// <param name="name">The name of process to report the stack from.</param> /// <param name="output">The output path for the collected trace data.</param> /// <param name="duration">The duration of to trace the target for. </param> /// <returns></returns> private static async Task <int> Report(CancellationToken ct, IConsole console, int processId, string name, TimeSpan duration) { string tempNetTraceFilename = Path.GetRandomFileName() + ".nettrace"; string tempEtlxFilename = ""; try { // Either processName or processId has to be specified. if (!string.IsNullOrEmpty(name)) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(-1); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(-1); } } if (processId < 0) { console.Error.WriteLine("Process ID should not be negative."); return(-1); } else if (processId == 0) { console.Error.WriteLine("--process-id is required"); return(-1); } var client = new DiagnosticsClient(processId); var providers = new List <EventPipeProvider>() { new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational) }; // collect a *short* trace with stack samples // the hidden '--duration' flag can increase the time of this trace in case 10ms // is too short in a given environment, e.g., resource constrained systems // N.B. - This trace INCLUDES rundown. For sufficiently large applications, it may take non-trivial time to collect // the symbol data in rundown. using (EventPipeSession session = client.StartEventPipeSession(providers)) using (FileStream fs = File.OpenWrite(tempNetTraceFilename)) { Task copyTask = session.EventStream.CopyToAsync(fs); await Task.Delay(duration); session.Stop(); // check if rundown is taking more than 5 seconds and add comment to report Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(5)); Task completedTask = await Task.WhenAny(copyTask, timeoutTask); if (completedTask == timeoutTask) { console.Out.WriteLine($"# Sufficiently large applications can cause this command to take non-trivial amounts of time"); } await copyTask; } // using the generated trace file, symbolocate and compute stacks. tempEtlxFilename = TraceLog.CreateFromEventPipeDataFile(tempNetTraceFilename); using (var symbolReader = new SymbolReader(System.IO.TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath }) using (var eventLog = new TraceLog(tempEtlxFilename)) { var stackSource = new MutableTraceEventStackSource(eventLog) { OnlyManagedCodeStacks = true }; var computer = new SampleProfilerThreadTimeComputer(eventLog, symbolReader); computer.GenerateThreadTimeStacks(stackSource); var samplesForThread = new Dictionary <int, List <StackSourceSample> >(); stackSource.ForEach((sample) => { var stackIndex = sample.StackIndex; while (!stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), false).StartsWith("Thread (")) { stackIndex = stackSource.GetCallerIndex(stackIndex); } // long form for: int.Parse(threadFrame["Thread (".Length..^1)]) // Thread id is in the frame name as "Thread (<ID>)" string template = "Thread ("; string threadFrame = stackSource.GetFrameName(stackSource.GetFrameIndex(stackIndex), false); int threadId = int.Parse(threadFrame.Substring(template.Length, threadFrame.Length - (template.Length + 1))); if (samplesForThread.TryGetValue(threadId, out var samples)) { samples.Add(sample); } else { samplesForThread[threadId] = new List <StackSourceSample>() { sample }; } }); // For every thread recorded in our trace, print the first stack foreach (var(threadId, samples) in samplesForThread) { #if DEBUG console.Out.WriteLine($"Found {samples.Count} stacks for thread 0x{threadId:X}"); #endif PrintStack(console, threadId, samples[0], stackSource); } } } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); return(-1); } finally { if (File.Exists(tempNetTraceFilename)) { File.Delete(tempNetTraceFilename); } if (File.Exists(tempEtlxFilename)) { File.Delete(tempEtlxFilename); } } return(0); }
/// <summary> /// Collects a gcdump from a currently running process. /// </summary> /// <param name="ct">The cancellation token</param> /// <param name="console"></param> /// <param name="processId">The process to collect the gcdump from.</param> /// <param name="output">The output path for the collected gcdump.</param> /// <returns></returns> private static async Task <int> Collect(CancellationToken ct, IConsole console, int processId, string output, int timeout, bool verbose, string name) { if (name != null) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(-1); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(-1); } } try { if (processId < 0) { Console.Out.WriteLine($"The PID cannot be negative: {processId}"); return(-1); } if (processId == 0) { Console.Out.WriteLine("-p|--process-id is required"); return(-1); } output = string.IsNullOrEmpty(output) ? $"{DateTime.Now:yyyyMMdd\\_HHmmss}_{processId}.gcdump" : output; FileInfo outputFileInfo = new FileInfo(output); if (outputFileInfo.Exists) { outputFileInfo.Delete(); } if (string.IsNullOrEmpty(outputFileInfo.Extension) || outputFileInfo.Extension != ".gcdump") { outputFileInfo = new FileInfo(outputFileInfo.FullName + ".gcdump"); } Console.Out.WriteLine($"Writing gcdump to '{outputFileInfo.FullName}'..."); var dumpTask = Task.Run(() => { if (TryCollectMemoryGraph(ct, processId, timeout, verbose, out var memoryGraph)) { GCHeapDump.WriteMemoryGraph(memoryGraph, outputFileInfo.FullName, "dotnet-gcdump"); return(true); } return(false); }); var fDumpSuccess = await dumpTask; if (fDumpSuccess) { outputFileInfo.Refresh(); Console.Out.WriteLine($"\tFinished writing {outputFileInfo.Length} bytes."); return(0); } else if (ct.IsCancellationRequested) { Console.Out.WriteLine("\tCancelled."); return(-1); } else { Console.Out.WriteLine("\tFailed to collect gcdump. Try running with '-v' for more information."); return(-1); } } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); return(-1); } }
/// <summary> /// Collects a diagnostic trace from a currently running process. /// </summary> /// <param name="ct">The cancellation token</param> /// <param name="console"></param> /// <param name="processId">The process to collect the trace from.</param> /// <param name="name">The name of process to collect the trace from.</param> /// <param name="output">The output path for the collected trace data.</param> /// <param name="buffersize">Sets the size of the in-memory circular buffer in megabytes.</param> /// <param name="providers">A list of EventPipe providers to be enabled. This is in the form 'Provider[,Provider]', where Provider is in the form: 'KnownProviderName[:Flags[:Level][:KeyValueArgs]]', and KeyValueArgs is in the form: '[key1=value1][;key2=value2]'</param> /// <param name="profile">A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly.</param> /// <param name="format">The desired format of the created trace file.</param> /// <param name="duration">The duration of trace to be taken. </param> /// <param name="clrevents">A list of CLR events to be emitted.</param> /// <param name="clreventlevel">The verbosity level of CLR events</param> /// <returns></returns> private static async Task <int> Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name) { try { Debug.Assert(output != null); Debug.Assert(profile != null); // Either processName or processId has to be specified. if (name != null) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(ErrorCodes.ArgumentError); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(ErrorCodes.ArgumentError); } } if (processId < 0) { Console.Error.WriteLine("Process ID should not be negative."); return(ErrorCodes.ArgumentError); } else if (processId == 0) { Console.Error.WriteLine("--process-id is required"); return(ErrorCodes.ArgumentError); } bool hasConsole = console.GetTerminal() != null; if (hasConsole) { Console.Clear(); } if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { Console.Out.WriteLine("No profile or providers specified, defaulting to trace profile 'cpu-sampling'"); profile = "cpu-sampling"; } Dictionary <string, string> enabledBy = new Dictionary <string, string>(); var providerCollection = Extensions.ToProviders(providers); foreach (EventPipeProvider providerCollectionProvider in providerCollection) { enabledBy[providerCollectionProvider.Name] = "--providers "; } if (profile.Length != 0) { var selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); if (selectedProfile == null) { Console.Error.WriteLine($"Invalid profile name: {profile}"); return(ErrorCodes.ArgumentError); } Profile.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); } // Parse --clrevents parameter if (clrevents.Length != 0) { // Ignore --clrevents if CLR event provider was already specified via --profile or --providers command. if (enabledBy.ContainsKey(Extensions.CLREventProviderName)) { Console.WriteLine($"The argument --clrevents {clrevents} will be ignored because the CLR provider was configured via either --profile or --providers command."); } else { var clrProvider = Extensions.ToCLREventPipeProvider(clrevents, clreventlevel); providerCollection.Add(clrProvider); enabledBy[Extensions.CLREventProviderName] = "--clrevents"; } } if (providerCollection.Count <= 0) { Console.Error.WriteLine("No providers were specified to start a trace."); return(ErrorCodes.ArgumentError); } PrintProviders(providerCollection, enabledBy); var process = Process.GetProcessById(processId); var shouldExit = new ManualResetEvent(false); var shouldStopAfterDuration = duration != default(TimeSpan); var failed = false; var terminated = false; var rundownRequested = false; System.Timers.Timer durationTimer = null; ct.Register(() => shouldExit.Set()); var diagnosticsClient = new DiagnosticsClient(processId); using (VirtualTerminalMode vTermMode = VirtualTerminalMode.TryEnable()) { EventPipeSession session = null; try { session = diagnosticsClient.StartEventPipeSession(providerCollection, true); } catch (DiagnosticsClientException e) { Console.Error.WriteLine($"Unable to start a tracing session: {e.ToString()}"); } if (session == null) { Console.Error.WriteLine("Unable to create session."); return(ErrorCodes.SessionCreationError); } if (shouldStopAfterDuration) { durationTimer = new System.Timers.Timer(duration.TotalMilliseconds); durationTimer.Elapsed += (s, e) => shouldExit.Set(); durationTimer.AutoReset = false; } var collectingTask = new Task(() => { try { var stopwatch = new Stopwatch(); durationTimer?.Start(); stopwatch.Start(); using (var fs = new FileStream(output.FullName, FileMode.Create, FileAccess.Write)) { Console.Out.WriteLine($"Process : {process.MainModule.FileName}"); Console.Out.WriteLine($"Output File : {fs.Name}"); if (shouldStopAfterDuration) { Console.Out.WriteLine($"Trace Duration : {duration.ToString(@"dd\:hh\:mm\:ss")}"); } Console.Out.WriteLine("\n\n"); var buffer = new byte[16 * 1024]; while (true) { int nBytesRead = session.EventStream.Read(buffer, 0, buffer.Length); if (nBytesRead <= 0) { break; } fs.Write(buffer, 0, nBytesRead); if (!rundownRequested) { if (hasConsole) { lineToClear = Console.CursorTop - 1; ResetCurrentConsoleLine(vTermMode.IsEnabled); } Console.Out.WriteLine($"[{stopwatch.Elapsed.ToString(@"dd\:hh\:mm\:ss")}]\tRecording trace {GetSize(fs.Length)}"); Console.Out.WriteLine("Press <Enter> or <Ctrl+C> to exit..."); Debug.WriteLine($"PACKET: {Convert.ToBase64String(buffer, 0, nBytesRead)} (bytes {nBytesRead})"); } } } } catch (Exception ex) { failed = true; Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); } finally { terminated = true; shouldExit.Set(); } }); collectingTask.Start(); do { while (!Console.KeyAvailable && !shouldExit.WaitOne(250)) { } } while (!shouldExit.WaitOne(0) && Console.ReadKey(true).Key != ConsoleKey.Enter); if (!terminated) { durationTimer?.Stop(); if (hasConsole) { lineToClear = Console.CursorTop; ResetCurrentConsoleLine(vTermMode.IsEnabled); } Console.Out.WriteLine("Stopping the trace. This may take up to minutes depending on the application being traced."); rundownRequested = true; session.Stop(); } await collectingTask; } Console.Out.WriteLine(); Console.Out.WriteLine("Trace completed."); if (format != TraceFileFormat.NetTrace) { TraceFileFormatConverter.ConvertToFormat(format, output.FullName); } return(failed ? ErrorCodes.TracingError : 0); } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); return(ErrorCodes.UnknownError); } }
public int Collect(IConsole console, int processId, string output, bool diag, DumpTypeOption type, string name) { Console.WriteLine(name); if (name != null) { if (processId != 0) { Console.WriteLine("Can only specify either --name or --process-id option."); return(0); } processId = CommandUtils.FindProcessIdWithName(name); if (processId < 0) { return(0); } } if (processId == 0) { console.Error.WriteLine("ProcessId is required."); return(1); } try { if (output == null) { // Build timestamp based file path string timestamp = $"{DateTime.Now:yyyyMMdd_HHmmss}"; output = Path.Combine(Directory.GetCurrentDirectory(), RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"dump_{timestamp}.dmp" : $"core_{timestamp}"); } // Make sure the dump path is NOT relative. This path could be sent to the runtime // process on Linux which may have a different current directory. output = Path.GetFullPath(output); // Display the type of dump and dump path string dumpTypeMessage = null; switch (type) { case DumpTypeOption.Full: dumpTypeMessage = "full"; break; case DumpTypeOption.Heap: dumpTypeMessage = "dump with heap"; break; case DumpTypeOption.Mini: dumpTypeMessage = "dump"; break; } console.Out.WriteLine($"Writing {dumpTypeMessage} to {output}"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // Get the process Process process = Process.GetProcessById(processId); Windows.CollectDump(process, output, type); } else { var client = new DiagnosticsClient(processId); DumpType dumpType = DumpType.Normal; switch (type) { case DumpTypeOption.Full: dumpType = DumpType.Full; break; case DumpTypeOption.Heap: dumpType = DumpType.WithHeap; break; case DumpTypeOption.Mini: dumpType = DumpType.Normal; break; } // Send the command to the runtime to initiate the core dump client.WriteDump(dumpType, output, diag); } } catch (Exception ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException || ex is UnauthorizedAccessException || ex is PlatformNotSupportedException || ex is InvalidDataException || ex is InvalidOperationException || ex is NotSupportedException || ex is DiagnosticsClientException) { console.Error.WriteLine($"{ex.Message}"); return(1); } console.Out.WriteLine($"Complete"); return(0); }