Skip to content

Conversation

@mdh1418
Copy link
Member

@mdh1418 mdh1418 commented Oct 29, 2025

With more tests validating output of diagnostic tools, useful console output from helpers should be redirectable as well.

This PR does the following:

  • Switch CounterMonitor back to using IConsole for output instead of TextWriters (see commit message for more details Update System.CommandLine version #5217 had switched from IConsole to TextWriters)
  • Enable redirecting CommandUtils helpers console output to be captured in tests

Previously, CounterMonitor had been using IConsole from System.CommandLine
instead of the in-house IConsole. As a result, during work to bump to a
System.CommandLine version where IConsole was deprecated, CounterMonitor
switched to writing to specified TextWriters. Although that decision was
fine, this change switches to the in-house IConsole to prepare for
redirecting CommandUtils console output to facilitate output validation
in tooling tests.
@mdh1418 mdh1418 requested review from hoyosjs and noahfalk October 29, 2025 00:15
@mdh1418 mdh1418 requested a review from a team as a code owner October 29, 2025 00:15
internal sealed class DefaultConsole : IConsole
{
private readonly bool _useAnsi;
public DefaultConsole(bool useAnsi)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public DefaultConsole(bool useAnsi)
public DefaultConsole(bool useAnsi = false)

If we are going to be using the DefaultConsole repeatedly and always passing false we may as well make it the default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I defaulted CollectLinux and Collect's to use false in prior PRs, but I'm not sure if they should try to emit ansi. Changed the default for DefaultConsole as false

Console.Error.WriteLine($"[ERROR] {e.Message}");
return -1;
}
catch (FormatException fe)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was considering changing IpcEndpointConfig.Parse to throw CommandLineErrorException as well, but it seems like a breaking change if any users had used a public API that eventually invoked IpcEndpoingConfig.Pargse and specifically conditioned on FormatException, for example

IpcEndpointConfig portConfig = IpcEndpointConfig.Parse(diagnosticPort);
.

Copy link
Member

@lateralusX lateralusX Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the point why that would be needed, it doesn't log anything directly to console, but instead throws different exceptions with message based on identified error conditions, and its up to the caller to handle the exceptions it might throw and take actions based on exception. I believe this falls into comment above that API's should throw exceptions that makes sense for the errors identified and callers should handle them accordingly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I was just thinking it would be good to have consistency across the Tools, where any invalid combination of user passed-in args results in a CommandLineErrorException.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree such consistency would be good, we just need to do the CommandLineErrorException on the tool side of the code rather than in M.D.NetCore.Client library. From what I can see we've got at least three different helpers that play a role in converting the command-line args into a DiagnosticClient: ProcessLauncher.Launcher, DiagnosticsClientBuilder, and CommandUtils.ResolveProcessForAttach. Our tools don't use them consistently nor do they all handle errors in a consistent way, but those things could be fixed. With refactoring all the tools might converge to a unified helper something like:

// this does any work needed to get an active connection to the target process including
// launching a new process, binding by name, binding by id, binding by port,
// routing through dsrouter, resuming the process, and hooking up child process
// IO to the current console.
// If anything goes wrong it throws CommandLineErrorException
using (var target = new ProcessTarget(commandLineArgs, console, cancellationToken))
{
    // do whatever tool specific work we want
    // target can have any APIs needed to describe or interact with the target process
    target.DiagnosticClient.SendWhateverCommand(...);
    ...
}

I'm not encouraging you to do that in the scope of this PR, but I hope we are making progress in that direction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we should at least stay to conventional error handling on the library side. If we would like to do something different on the tool side where we previously would have passed around IConsole to log to stdout and/or stderr and we would like to not do that if a utility function only logs errors and return an error code, then fine. In that case its more like a generic DiagnosticToolException that could be thrown that would only be known to tools and something that can be catched and logged to stderr.

{
Console.WriteLine("There are more than one active processes with the given name: {0}", name);
return -1;
throw new CommandLineErrorException($"There are more than one active processes with the given name: {name}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe CommandLineErrorException is a little misplaced in a utility function, data might input parameter might come for command line or some other area. We should create a specific exception for these errors or use some more generic exception type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its reasonable to use CommandLineErrorException when the code is sufficiently constrained that we know its going to called by the command handlers and we know we want any errors to get shown on the console afterwards. I think anything under Commands/Utils probably fits that condition. IMO creating more generic Exceptions will just lead to every caller changing a 1 line call into an 8 line try/catch to convert the exception type back:

try
{
   UtilityFunction();
}
catch(SomeOtherExceptionType e)
{
    throw CommandLineErrorException(some_message);
}

{
Console.WriteLine("None of the --name, --process-id, or --diagnostic-port options may be specified when launching a child process.");
return false;
throw new CommandLineErrorException("None of the --name, --process-id, or --diagnostic-port options may be specified when launching a child process.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use argument exception instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommandLineErrorException means that we know the error message is suitable for display on the console, we've anticipated this error occuring, and there is no bug in the code. ArgumentException often implies the code has a bug and the error messages aren't intended for the end-user. I think CommandLineErrorException is appropriate here.

#5570 (comment)

Copy link
Member

@lateralusX lateralusX Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should rename the exception then, clearly showing the intent, its not a command line error, but an error suitable to display on the console.

Copy link
Member

@noahfalk noahfalk Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to do that. The current name was intended to be interpreted as an error that will be displayed on the command line, not necessarily an error caused by the command line. If another name coveys that better we can switch anytime.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiagnosticToolException?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted all CommandLineErrorException to DiagnosticToolException

/// <param name="resolvedProcessId">resolvedProcessId</param>
/// <returns></returns>
public static bool ResolveProcessForAttach(int processId, string name, string port, string dsrouter, out int resolvedProcessId)
public static void ResolveProcessForAttach(int processId, string name, string port, string dsrouter, out int resolvedProcessId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably shouldn't use the same exception for all errors. We should also do specific exceptions for the LaunchDSRouter errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think multiple Exception types make sense if we expect callers to write catch blocks that discriminate based on those types. Is that the case here?

Copy link
Member Author

@mdh1418 mdh1418 Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the LaunchDSRouter errors are not purely a problem with the args the user passes, so it overloads CommandLineErrorException to mean more than "user passed in invalid combination of args". (Though if they didnt pass in --dsrouter they wouldn't hit the error 😄). Then again, I think the catch behavior would be the same, we would emit the error message and stop the tool. So I'm conflicted in that it's a different class of error, yet I don't think this necessarily warrants a new copy/paste catch block.

Edit: After thinking of CommandLineErrorException as error message suitable to be displayed on console https://github.com/dotnet/diagnostics/pull/5622/files#r2472507775, it seems like we don't necessarily need a new catch block if in either case we just want to inform the user that something is wrong with their setup.

}
else
{
return (int)ReturnCode.ArgumentError;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we lose this error case, when function returns argument error code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it changes the return error code to TracingError because of the catch at

catch (CommandLineErrorException e)
{
Console.Error.WriteLine($"[ERROR] {e.Message}");
collectionStopped = true;
ret = (int)ReturnCode.TracingError;
}
. Maybe CommandLineErrorException, which I'm understanding represents an invalid combination of user's passed-in args, should all result in ReturnCode.ArgumentError instead? Otherwise, if we need to differentiate between some CommandLineErrorExceptions throwing ArgumentError and others throwing TracingError, that seems like the CommandLineErrorException is overloaded

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like the ReturnCode is really being consumed anywhere except for the recently added functional tests. Are there known instances of users invoking dotnet-trace programmatically and checking the exit code?

Regarding #5622 (comment), it seems like the failure to launch DSRouter isn't an argument error, it feels more aligned with TracingError. If we want to treat the instances of CommandLineErrorException that are a result of invalid arguments as ReturnCode.ArgumentError, we can check the message for the dsrouter launch failure to differentiate, but 1) that doesn't feel like a clean resolution, and 2) changing the invalid args to ArgumentError would be breaking behavior if anyone consumed the exit codes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there known instances of users invoking dotnet-trace programmatically and checking the exit code?

Not that I am aware of, though that doesn't mean it never happens. My thought would be make sure scripts can distinguish success from failure but don't worry about categorizing different kinds of error with different numeric codes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants