Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

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

未关闭
#2,836 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
3/5
预计耗时
1-2 天
新手友好度
74/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
冷清
技术栈
csharp
领域
cli

调研方向

从 src/System.CommandLine/Invocation/InvocationPipeline.cs 中的 InvokeAsync 开始,将操作前循环与主命令操作路径进行比较。阅读 ProcessTerminationHandler.cs,并在操作前按下 Ctrl+C 运行提供的 net10.0 重现。完成的标准是:操作前 token 被取消、信号被抑制,并且操作可以在配置的宽限期内展开完成。

由索引模型根据 Issue 内容生成。

描述

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.

主要语言
C#
星标
3.7k
派生
434
平均合并
4 小时 46 分钟
30 天内合并 PR
1

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

dotnet/command-line-api 的其他 Issue

查看 dotnet/command-line-api 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。