Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Async pre-actions are not covered by process-termination handling: Ctrl+C hard-kills the process instead of cancelling the token

Đang mở
#2,836 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
74/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Ít trao đổi
Công nghệ
csharp
Lĩnh vực
cli

Hướng nghiên cứu

Bắt đầu tại src/System.CommandLine/Invocation/InvocationPipeline.cs, ở InvokeAsync, và so sánh vòng lặp trước tác vụ với đường dẫn tác vụ của lệnh chính. Đọc ProcessTerminationHandler.cs và chạy bản tái hiện net10.0 được cung cấp bằng Ctrl+C trong khi thực hiện trước tác vụ. Hoàn tất khi token trước tác vụ bị hủy, tín hiệu bị chặn và tác vụ có thể unwind trong khoảng thời gian ân hạn đã cấu hình.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Summary

InvocationPipeline.InvokeAsync installs the ProcessTerminationHandler only around the
main command action, never around pre-actions. As a result, when the user presses
Ctrl+C (SIGINT/SIGTERM) while an async pre-action is running:

  • the CancellationToken the pre-action received is never cancelled, and
  • the OS default signal handling is not suppressed, so the process is hard-terminated
    immediately
    — no OperationCanceledException, no unwind, no cleanup.

The same handler running as the command action cancels gracefully. So whether Ctrl+C is
cooperative or fatal depends purely on whether the async work runs in a pre-action or the
command action, which is surprising and undocumented.

Repro

Minimal console app (net10.0) referencing System.CommandLine:

using System.CommandLine;
using System.CommandLine.Invocation;

var slow = new Option<string>("--slow");
slow.Action = new SlowPreAction();          // non-terminating async action => runs as a PreAction

var root = new RootCommand("repro") { slow };
root.SetAction(async (parseResult, ct) => { // async command action
	Console.WriteLine("command action started");
	try {
		await Task.Delay(TimeSpan.FromSeconds(30), ct);
		Console.WriteLine("command action finished");
	} catch (OperationCanceledException) {
		// this catch works as expected
		Console.WriteLine("command action cancelled");
	}
	return 0;
});

return await root.Parse(args).InvokeAsync();

sealed class SlowPreAction : AsynchronousCommandLineAction {
	public override bool Terminating => false;
	public override async Task<int> InvokeAsync(ParseResult parseResult, CancellationToken ct) {
		Console.WriteLine("pre-action started");
		try {
			await Task.Delay(TimeSpan.FromSeconds(30), ct);
			Console.WriteLine("pre-action finished");
		} catch (OperationCanceledException) {
			// this catch will not work
			Console.WriteLine("pre-action cancelled");
		}
		return 0;
	}
}
Case A — cancel during the command action (works as expected)
> repro
command action started
^Ccommand action cancelled

Process finished with exit code 0.

The token is cancelled, Task.Delay throws OperationCanceledException, the process exits
cleanly.

Case B — cancel during the pre-action (the bug)
> repro --slow x
pre-action started
^C

The process exits immediately as if killed — the token is never cancelled, no exception is
observed, and pre-action finished never prints. It behaves as though there were no Ctrl+C
handling installed at all.

Expected behavior

Ctrl+C during an async pre-action should behave the same as during the command action: the
CancellationToken handed to the pre-action is cancelled, the OS default kill is suppressed,
and the pre-action is given the ProcessTerminationTimeout grace period to unwind.

Actual behavior

Pre-actions run with no ProcessTerminationHandler. The token is inert and the process is
hard-terminated by the default signal.

Root cause

In src/System.CommandLine/Invocation/InvocationPipeline.cs, InvokeAsync:

  • Pre-actions are awaited in the loop with no termination handler:

    case AsynchronousCommandLineAction asyncAction:
        result = await asyncAction.InvokeAsync(parseResult, cts.Token);   // no ProcessTerminationHandler
        break;
    
  • The ProcessTerminationHandler — which registers the SIGINT/SIGTERM handler
    (ProcessTerminationHandler.cs, PosixSignalRegistration.Create(...)), sets
    context.Cancel = true to suppress the default kill, and cancels the linked cts — is
    created only for the main command action:

    var timeout = parseResult.InvocationConfiguration.ProcessTerminationTimeout;
    if (timeout.HasValue) terminationHandler = new(cts, timeout.Value);
    var startedInvocation = asyncAction.InvokeAsync(parseResult, cts.Token);
    ...
    

Because no handler is installed during the pre-action phase, nothing ever cancels cts there,
and SIGINT/SIGTERM fall through to the runtime default (terminate the process).

Note: even setting aside the hard-kill, a perfectly cooperative pre-action could never observe
cancellation, since cts is not cancellable during that phase.

Suggested fix

Install the process-termination handling around the entire async invocation (pre-actions +
command action), not just the command action — e.g. create the ProcessTerminationHandler
before the pre-action loop so SIGINT is intercepted and cts is cancellable throughout. At
minimum, pre-actions should receive a token that is actually cancelled on Ctrl+C and should not
be hard-killed mid-run.

Environment

  • System.CommandLine 3.0.0-preview.5.26302.115 (also confirmed present on main)
  • .NET SDK 10.0.100
  • Reproduced on macOS (darwin); the code path is platform-independent (both the
    PosixSignalRegistration and Console.CancelKeyPress branches are gated inside the
    handler that pre-actions never construct).

▎ Drafted with AI assistance; I reproduced the behavior on 3.0.0-preview.5, confirmed the same code path on main, and verified the root-cause references myself.

Ngôn ngữ chính
C#
Star
3.7k
Fork
434
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của dotnet/command-line-api

Tất cả issue của dotnet/command-line-api

Issue tương tự

Thêm issue về C#

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.