Skip to content

Conversation

@mdh1418
Copy link
Member

@mdh1418 mdh1418 commented Oct 29, 2025

With more tests validating output of diagnostic tools, errors from CommandUtils helpers should be validated as well. In facilitating error checking, src/Tools/Common/Commands/Utils.cs has been refactored to break unnecessary dependencies (e.g. using CommandUtils.FindProcessIdWithName requires adding IConsole.cs to project Compile items because LineRewriter depends on IConsole, regardless of whether LineRewriter is used)

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)
  • Rename CommandLineErrorException -> DiagnosticToolException to suggest a more generic invalid usage of diagnostic tools, rather specifically a command line exception.
  • Update CommandUtils helpers to throw DiagnosticToolException to allow callers to decide how to handle the message.
  • Extend DiagnosticToolException (formerly CommandLineErrorException) to specify ReturnCode
  • Break ReturnCode + LineRewriter out of Utils.cs to have discrete dependency chains for Project compile items.
  • Rename Utils.cs -> CommandUtils.cs
  • Keep src/Tools/Common/Commands command-only

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
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.

}
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.

@mdh1418
Copy link
Member Author

mdh1418 commented Nov 3, 2025

@lateralusX @noahfalk Could this get another review?

noahfalk
noahfalk previously approved these changes Nov 4, 2025
Copy link
Member

@noahfalk noahfalk left a comment

Choose a reason for hiding this comment

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

Looked good to me other than one comment inline

ret = (int)ReturnCode.ArgumentError;
if (dte.Message.StartsWith("Failed to launch dsrouter", StringComparison.OrdinalIgnoreCase))
{
ret = (int)ReturnCode.TracingError;
Copy link
Member

@noahfalk noahfalk Nov 4, 2025

Choose a reason for hiding this comment

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

@lateralusX - do you think it is important to preserve this distinction in error codes? I'm not expecting it matters much.

If we do want to assign specific return codes I'd suggest add an int ReturnCode property to the DiagnosticToolException, return that value here, and set it when the Exception is constructed. Using error message content for control flow looks very fragile.

Copy link
Member

Choose a reason for hiding this comment

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

We should definitely not depend on message output to convert to error codes, either we have sub classes of exception types so we can distinguish or have an error code on DiagnosticToolException.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added the ReturnCode, I opted to just comment that the default maps to ReturnCode.ArgumentError, because otherwise, adding a dependency on Microsoft.Internal.Common.Utils to DiagnosticToolException will propagate and require additional manual compile items for all of Microsoft.Internal.Common.Utils dependencies. (atleast for dotnet-dsrouter which just uses DiagnosticToolException and doesn't use Utils)

Copy link
Member

@lateralusX lateralusX Nov 5, 2025

Choose a reason for hiding this comment

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

I think we should move out the ReturnCode into its own source file under Common, maybe name it DiagnosticToolReturnCode.cs, that way it could be used by tools without additional dependencies. We should probably do the same with LineReWriter and maybe rename utils.cs to then name of the class it actually holds, CommandUtils.

Before, if any file wanted to use ReturnCode, it's project would need
to include all dependencies of Utils.cs, regardless of usage. By
moving ReturnCode into it's own source, we break the dependency.
LineRewriter depends on IConsole, so whenever Tools used
CommandUtils, they would need that dependency regardless
of whether they actually used IConsole. Like ReturnCode,
moving LineRewriter breaks unnecessary dependencies.
CommandUtils isn't a real Command, unlike ProcessStatus.
It's more similar to other Common sources, so move and
rename for consistency with class.
@mdh1418 mdh1418 changed the title Enable command utils console redirection [ToolsCommon] Refactor and Facilitate Console Redirection Nov 5, 2025
@mdh1418 mdh1418 changed the title [ToolsCommon] Refactor and Facilitate Console Redirection [ToolsCommon] Refactor Utils.cs Nov 5, 2025
lateralusX
lateralusX previously approved these changes Nov 6, 2025
Copy link
Member

@lateralusX lateralusX left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Member

@noahfalk noahfalk left a comment

Choose a reason for hiding this comment

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

👍

@mdh1418 mdh1418 merged commit 8220dc7 into dotnet:main Nov 6, 2025
19 checks passed
@mdh1418 mdh1418 deleted the enable_command_utils_console_redirection branch November 6, 2025 20:54
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