From 5411438741c15e196a9be1c316921b14bac40586 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Wed, 18 Mar 2026 08:23:55 -0700 Subject: [PATCH 1/2] fix #2743 --- .../CustomParsingTests.cs | 17 +++++++++++++++++ src/System.CommandLine/Argument{T}.cs | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/src/System.CommandLine.Tests/CustomParsingTests.cs b/src/System.CommandLine.Tests/CustomParsingTests.cs index 7a1d14936d..dcc7fce18a 100644 --- a/src/System.CommandLine.Tests/CustomParsingTests.cs +++ b/src/System.CommandLine.Tests/CustomParsingTests.cs @@ -561,6 +561,23 @@ public void Custom_parser_is_called_once_per_parse_operation_when_input_is_provi i.Should().Be(2); } + [Fact] // https://github.com/dotnet/command-line-api/issues/2743 + public void Setting_CustomParser_to_null_reverts_to_default_parsing() + { + Argument argument = new("int") + { + CustomParser = (_) => 0 + }; + + argument.CustomParser = null; + + var command = new RootCommand { argument }; + + var result = command.Parse("123"); + + result.GetValue(argument).Should().Be(123); + } + [Fact] public void Default_value_factory_is_called_once_per_parse_operation_when_no_input_is_provided() { diff --git a/src/System.CommandLine/Argument{T}.cs b/src/System.CommandLine/Argument{T}.cs index 999ac5604f..2ea7b3d726 100644 --- a/src/System.CommandLine/Argument{T}.cs +++ b/src/System.CommandLine/Argument{T}.cs @@ -78,6 +78,10 @@ public Func? DefaultValueFactory } }; } + else + { + ConvertArguments = null; + } } } From 7241883fc16ce7cb6921f1e6799fc41099017a2f Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Wed, 18 Mar 2026 10:09:56 -0700 Subject: [PATCH 2/2] fix intermittent race condition in InvocationHPipeline --- .../Invocation/InvocationPipeline.cs | 7 ++++--- .../Invocation/ProcessTerminationHandler.cs | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/System.CommandLine/Invocation/InvocationPipeline.cs b/src/System.CommandLine/Invocation/InvocationPipeline.cs index c41d2686d6..a2b50afe9c 100644 --- a/src/System.CommandLine/Invocation/InvocationPipeline.cs +++ b/src/System.CommandLine/Invocation/InvocationPipeline.cs @@ -44,21 +44,22 @@ internal static async Task InvokeAsync(ParseResult parseResult, Cancellatio return syncAction.Invoke(parseResult); case AsynchronousCommandLineAction asyncAction: - var startedInvocation = asyncAction.InvokeAsync(parseResult, cts.Token); - var timeout = parseResult.InvocationConfiguration.ProcessTerminationTimeout; if (timeout.HasValue) { - terminationHandler = new(cts, startedInvocation, timeout.Value); + terminationHandler = new(cts, timeout.Value); } + var startedInvocation = asyncAction.InvokeAsync(parseResult, cts.Token); + if (terminationHandler is null) { return await startedInvocation; } else { + terminationHandler.StartedHandler = startedInvocation; // Handlers may not implement cancellation. // In such cases, when CancelOnProcessTermination is configured and user presses Ctrl+C, // ProcessTerminationCompletionSource completes first, with the result equal to native exit code for given signal. diff --git a/src/System.CommandLine/Invocation/ProcessTerminationHandler.cs b/src/System.CommandLine/Invocation/ProcessTerminationHandler.cs index ead35b4eb0..3ff7e7c8ee 100644 --- a/src/System.CommandLine/Invocation/ProcessTerminationHandler.cs +++ b/src/System.CommandLine/Invocation/ProcessTerminationHandler.cs @@ -14,20 +14,20 @@ internal sealed class ProcessTerminationHandler : IDisposable internal readonly TaskCompletionSource ProcessTerminationCompletionSource; private readonly CancellationTokenSource _handlerCancellationTokenSource; - private readonly Task _startedHandler; + private Task? _startedHandler; private readonly TimeSpan _processTerminationTimeout; #if NET7_0_OR_GREATER private readonly IDisposable? _sigIntRegistration, _sigTermRegistration; #endif - + + internal Task StartedHandler { set => Volatile.Write(ref _startedHandler, value); } + internal ProcessTerminationHandler( CancellationTokenSource handlerCancellationTokenSource, - Task startedHandler, TimeSpan processTerminationTimeout) { ProcessTerminationCompletionSource = new (); _handlerCancellationTokenSource = handlerCancellationTokenSource; - _startedHandler = startedHandler; _processTerminationTimeout = processTerminationTimeout; #if NET7_0_OR_GREATER // we prefer the new API as they allow for cancelling SIGTERM @@ -86,8 +86,9 @@ void Cancel(int forcedTerminationExitCode) try { + var startedHandler = Volatile.Read(ref _startedHandler); // wait for the configured interval - if (!_startedHandler.Wait(_processTerminationTimeout)) + if (startedHandler is null || !startedHandler.Wait(_processTerminationTimeout)) { // if the handler does not finish within configured time, // use the completion source to signal forced completion (preserving native exit code)