Skip to content

Commit f1f43df

Browse files
authored
Adding in cancellation support for InvokeAsync (#1502)
* Adding in cancellation support for InvokeAsync Moving the storage of the cancellation token to the InvocationContext Adding InvocationContext tests Updating the approved public API Fixes after rebase Reworking the code so it doesn't register for events without a cancellation token * Handling TODO, to dispose of CancellationTokenSource * Update with suggestions to better handled linked token * Rebased and fixed conflicts * Missing using after rebase.
1 parent 981292f commit f1f43df

14 files changed

+264
-126
lines changed

src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ System.CommandLine
6666
public static class CommandExtensions
6767
public static System.Int32 Invoke(this Command command, System.String[] args, IConsole console = null)
6868
public static System.Int32 Invoke(this Command command, System.String commandLine, IConsole console = null)
69-
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String[] args, IConsole console = null)
70-
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String commandLine, IConsole console = null)
69+
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String[] args, IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
70+
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String commandLine, IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
7171
public static ParseResult Parse(this Command command, System.String[] args)
7272
public static ParseResult Parse(this Command command, System.String commandLine)
7373
public class CommandLineBuilder
@@ -357,8 +357,8 @@ System.CommandLine.Help
357357
System.CommandLine.Invocation
358358
public interface IInvocationResult
359359
public System.Void Apply(InvocationContext context)
360-
public class InvocationContext
361-
.ctor(System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null)
360+
public class InvocationContext, System.IDisposable
361+
.ctor(System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
362362
public System.CommandLine.Binding.BindingContext BindingContext { get; }
363363
public System.CommandLine.IConsole Console { get; set; }
364364
public System.Int32 ExitCode { get; set; }
@@ -368,6 +368,7 @@ System.CommandLine.Invocation
368368
public System.CommandLine.Parsing.Parser Parser { get; }
369369
public System.CommandLine.ParseResult ParseResult { get; set; }
370370
public System.Threading.CancellationToken GetCancellationToken()
371+
public System.Void LinkToken(System.Threading.CancellationToken token)
371372
public delegate InvocationMiddleware : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable
372373
.ctor(System.Object object, System.IntPtr method)
373374
public System.IAsyncResult BeginInvoke(InvocationContext context, System.Func<InvocationContext,System.Threading.Tasks.Task> next, System.AsyncCallback callback, System.Object object)
@@ -450,12 +451,12 @@ System.CommandLine.Parsing
450451
public static System.String Diagram(this System.CommandLine.ParseResult parseResult)
451452
public static System.Boolean HasOption(this System.CommandLine.ParseResult parseResult, System.CommandLine.Option option)
452453
public static System.Int32 Invoke(this System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null)
453-
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null)
454+
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
454455
public static class ParserExtensions
455456
public static System.Int32 Invoke(this Parser parser, System.String commandLine, System.CommandLine.IConsole console = null)
456457
public static System.Int32 Invoke(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null)
457-
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String commandLine, System.CommandLine.IConsole console = null)
458-
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null)
458+
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String commandLine, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
459+
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
459460
public static System.CommandLine.ParseResult Parse(this Parser parser, System.String commandLine)
460461
public abstract class SymbolResult
461462
public System.Collections.Generic.IReadOnlyList<SymbolResult> Children { get; }

src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using FluentAssertions;
5+
using System.CommandLine.Invocation;
46
using System.CommandLine.Parsing;
57
using System.CommandLine.Tests.Utility;
68
using System.Diagnostics;
79
using System.Runtime.InteropServices;
810
using System.Threading.Tasks;
9-
using FluentAssertions;
1011
using Xunit;
1112
using Process = System.Diagnostics.Process;
1213

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using FluentAssertions;
2+
using System.CommandLine;
3+
using System.CommandLine.Invocation;
4+
using System.CommandLine.Parsing;
5+
using System.Threading;
6+
using Xunit;
7+
8+
namespace System.CommandLine.Tests.Invocation
9+
{
10+
public class InvocationContextTests
11+
{
12+
[Fact]
13+
public void InvocationContext_with_cancellation_token_returns_it()
14+
{
15+
using CancellationTokenSource cts = new();
16+
var parseResult = new CommandLineBuilder(new RootCommand())
17+
.Build()
18+
.Parse("");
19+
using InvocationContext context = new(parseResult, cancellationToken: cts.Token);
20+
21+
var token = context.GetCancellationToken();
22+
23+
token.IsCancellationRequested.Should().BeFalse();
24+
cts.Cancel();
25+
token.IsCancellationRequested.Should().BeTrue();
26+
}
27+
28+
[Fact]
29+
public void InvocationContext_with_linked_cancellation_token_can_cancel_by_passed_token()
30+
{
31+
using CancellationTokenSource cts1 = new();
32+
using CancellationTokenSource cts2 = new();
33+
var parseResult = new CommandLineBuilder(new RootCommand())
34+
.Build()
35+
.Parse("");
36+
using InvocationContext context = new(parseResult, cancellationToken: cts1.Token);
37+
context.LinkToken(cts2.Token);
38+
39+
var token = context.GetCancellationToken();
40+
41+
token.IsCancellationRequested.Should().BeFalse();
42+
cts1.Cancel();
43+
token.IsCancellationRequested.Should().BeTrue();
44+
}
45+
46+
[Fact]
47+
public void InvocationContext_with_linked_cancellation_token_can_cancel_by_linked_token()
48+
{
49+
using CancellationTokenSource cts1 = new();
50+
using CancellationTokenSource cts2 = new();
51+
var parseResult = new CommandLineBuilder(new RootCommand())
52+
.Build()
53+
.Parse("");
54+
using InvocationContext context = new(parseResult, cancellationToken: cts1.Token);
55+
context.LinkToken(cts2.Token);
56+
57+
var token = context.GetCancellationToken();
58+
59+
token.IsCancellationRequested.Should().BeFalse();
60+
cts2.Cancel();
61+
token.IsCancellationRequested.Should().BeTrue();
62+
}
63+
}
64+
}

src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.CommandLine.Invocation;
45
using System.CommandLine.IO;
6+
using System.Threading;
57
using System.Threading.Tasks;
68
using FluentAssertions;
79
using Xunit;
@@ -149,5 +151,28 @@ public void RootCommand_Invoke_can_set_custom_result_code()
149151

150152
resultCode.Should().Be(123);
151153
}
154+
155+
[Fact]
156+
public async Task Command_InvokeAsync_with_cancelation_token_invokes_command_handler()
157+
{
158+
CancellationTokenSource cts = new();
159+
var command = new Command("test");
160+
command.SetHandler((InvocationContext context) =>
161+
{
162+
CancellationToken cancellationToken = context.GetCancellationToken();
163+
Assert.True(cancellationToken.CanBeCanceled);
164+
if (cancellationToken.IsCancellationRequested)
165+
{
166+
return Task.FromResult(42);
167+
}
168+
169+
return Task.FromResult(0);
170+
});
171+
172+
cts.Cancel();
173+
int rv = await command.InvokeAsync("test", cancellationToken: cts.Token);
174+
175+
rv.Should().Be(42);
176+
}
152177
}
153178
}

src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.CommandLine.Help;
5+
using System.CommandLine.Invocation;
56
using System.CommandLine.IO;
67
using System.CommandLine.Parsing;
78
using System.Linq;
9+
using System.Threading;
810
using System.Threading.Tasks;
911
using FluentAssertions;
1012
using Xunit;
@@ -44,7 +46,7 @@ public async Task InvokeAsync_chooses_the_appropriate_command()
4446

4547
var parser = new CommandLineBuilder(new RootCommand
4648
{
47-
first,
49+
first,
4850
second
4951
})
5052
.Build();
@@ -327,5 +329,39 @@ public async Task When_help_builder_factory_is_specified_it_is_used_to_create_th
327329
handlerWasCalled.Should().BeTrue();
328330
factoryWasCalled.Should().BeTrue();
329331
}
332+
333+
[Fact]
334+
public async Task Command_InvokeAsync_can_cancel_from_middleware()
335+
{
336+
var handlerWasCalled = false;
337+
var isCancelRequested = false;
338+
339+
var command = new Command("the-command");
340+
command.SetHandler((InvocationContext context) =>
341+
{
342+
handlerWasCalled = true;
343+
isCancelRequested = context.GetCancellationToken().IsCancellationRequested;
344+
return Task.FromResult(0);
345+
});
346+
347+
348+
using CancellationTokenSource cts = new();
349+
var parser = new CommandLineBuilder(new RootCommand
350+
{
351+
command
352+
})
353+
.AddMiddleware(async (context, next) =>
354+
{
355+
context.LinkToken(cts.Token);
356+
cts.Cancel();
357+
await next(context);
358+
})
359+
.Build();
360+
361+
await parser.InvokeAsync("the-command");
362+
363+
handlerWasCalled.Should().BeTrue();
364+
isCancelRequested.Should().BeTrue();
365+
}
330366
}
331367
}

src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ public void When_parsing_a_string_array_input_then_a_full_path_to_an_executable_
4242

4343
command.Parse(Split("inner -x hello")).Errors.Should().BeEmpty();
4444

45-
command.Parse(Split($"{RootCommand.ExecutablePath} inner -x hello"))
46-
.Errors
47-
.Should()
48-
.ContainSingle(e => e.Message == $"{LocalizationResources.Instance.UnrecognizedCommandOrArgument(RootCommand.ExecutablePath)}");
45+
var parserResult = command.Parse(Split($"\"{RootCommand.ExecutablePath}\" inner -x hello"));
46+
parserResult
47+
.Errors
48+
.Should()
49+
.ContainSingle(e => e.Message == LocalizationResources.Instance.UnrecognizedCommandOrArgument(RootCommand.ExecutablePath));
4950
}
5051

5152
[Fact]
@@ -76,7 +77,7 @@ public void When_parsing_an_unsplit_string_then_input_a_full_path_to_an_executab
7677
}
7778
};
7879

79-
var result2 = command.Parse($"{RootCommand.ExecutablePath} inner -x hello");
80+
var result2 = command.Parse($"\"{RootCommand.ExecutablePath}\" inner -x hello");
8081

8182
result2.RootCommandResult.Token.Value.Should().Be(RootCommand.ExecutablePath);
8283
}

src/System.CommandLine/Binding/BindingContext.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Linq;
88

9-
#nullable enable
10-
119
namespace System.CommandLine.Binding
1210
{
1311
/// <summary>

0 commit comments

Comments
 (0)