ConsoleAppFrameworkでGraceful Shutdown

投稿日
2025年3月26日
ConsoleAppFrameworkでGraceful Shutdownの見出し画像
目次

はじめに

Cysharp(サイゲームスのグループ会社Cysharpが管理)のOSSパッケージConsoleAppFrameworkの使い勝手が 良さそうだったので、Asyncなタスクを実行させるコンソールアプリを試作してみました。

基本的な使い方はGitHubのREADMEに書かれているので、本記事では少し詰まったポイント+Graceful Shutdownについて書きます。

dotnet
9.0
csharp
13
ConsoleAppFramework
5.4.1

ConsoleApp.AsyncRun

タスクを実行させる場合には、ConsoleApp.AsyncRunを使います。

Graceful Shutdown

ConsoleApp.Runと書式はほぼ変わりませんが、第二引数のデリゲートの引数にCancellationTokenを追加できます。 これを追加すると、Ctrl+Cなどから送信されるSIGINT等の強制終了シグナルを自動的にフックして、CancellationTokenをキャンセル状態にしてくれます。

下記のように、CancellationTokenを引数に追加することでGraceful Shutdownを実現できます。

await ConsoleApp.RunAsync(args, async (int foo, CancellationToken cancellationToken) =>
{
// 10秒待ってHello Worldを表示
await Task.Delay(10000, cancellationToken);
Console.WriteLine("Hello World!");
});

OperationCanceledExceptionの例外処理を追加すれば、シグナル経由のシャットダウンであっても、キャンセル時の処理を簡単に実装できるようになります。

await ConsoleApp.RunAsync(args, async (CancellationToken cancellationToken) =>
{
try
{
await Task.Delay(10000, cancellationToken);
}
catch (OperationCanceledException)
{
Console.WriteLine("Delayがキャンセルされました");
}
Console.WriteLine("Hello World!");
});

Functionのデフォルト引数

CancellationTokenを渡しつつ、コマンドライン引数にデフォルト値を提供したい場合は下記のようにdefaultを渡してあげます。そうじゃないと位置引数の関係で文法エラーになってしまうので。

await ConsoleApp.RunAsync(args, async (int foo = 10, int bar = 20, CancellationToken cancellationToken = default) =>
{
// 10秒待ってHello Worldを表示
await Task.Delay(10000, cancellationToken);
Console.WriteLine("Hello World!");
});

他のキャンセルトークンと組み合わせたい場合は?

今回はCtrl+C等のシグナル以外にも、「Qキーが押されたらメインタスクを終了」という仕様で実装を考えていました。 その場合はCreateLinkedTokenSourceで複数のCancellationTokenを組み合わせることで実現できます。

備忘録として、下記仕様を満たすようなコードを載せておきます。

  • 1秒ごとにcount回、文字列messageを表示し続けるメインタスク
  • Qキーが押されたらキャンセル
  • Ctrl+Cでもキャンセル
  • どちらのキャンセルでもSuccessfully Canceledというメッセージを表示
internal class Program
{
static readonly CancellationTokenSource s_cts = new();
static async Task Main(string[] args)
{
await ConsoleApp.RunAsync(args, RunnerAsync);
}
static async Task RunnerAsync(string message = "Sample Message", int count = 10, CancellationToken cancellationToken = default)
{
// キャンセルトークンを組み合わせる
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(s_cts.Token, cancellationToken).Token;
var cancelTask = Task.Run(() =>
{
Console.WriteLine("Press Q to cancel.");
while (Console.ReadKey().Key != ConsoleKey.Q) ;
s_cts.Cancel();
}).WaitAsync(cancellationToken);
var showMessageTask = ShowMessageAsync(message, count, cancellationToken);
try
{
await Task.WhenAll(showMessageTask, cancelTask);
}
catch (OperationCanceledException)
{
Console.WriteLine("Successfully Canceled");
}
}
static async Task ShowMessageAsync(string message, int count, CancellationToken cancellationToken)
{
// 1秒ごとにメッセージを表示
for (int i = 0; i < count; i++)
{
Console.WriteLine(message);
await Task.Delay(1000, cancellationToken);
}
s_cts.Cancel();
}
}

おわりに

ConsoleAppFrameworkを試すきっかけとなったのは、下記の記事でした。
(記事末尾にConsoleAppFrameworkに関する言及があります)

「コンソールアプリを汎用ホストで作るなら、StopApplicationをどっかから叩いて終了する必要があるよな?」、なんて調べているうちに辿り着きました。 感謝です🙏

関連記事

見つかりませんでした😢