C#/.NET プログラミングの話。
とある要件で、ASP.NET Core Web アプリケーションを起動し、HTTP リッスン状態になるまでを待機して、それからその ASP.NET Core Web アプリに HTTP 要求を送信して応答を得るという、そのようなコンソールアプリケーションを作成する需要が発生した。
もちろん、.NET の標準ライブラリにある System.Diagnostics.Process クラスを使えば実装できる。Process クラスに対し、起動したい実行可能ファイルパスやコマンドライン引数を指定するのに加えて、標準出力や標準エラー出力のリダイレクトを指定し、および Process クラスから発火される、標準出力や標準エラー出力への書き込み発生のイベントをハンドルすればよい。
ASP.NET Core Web アプリケーションであれば、HTTP リッスン状態になると「Now listening on: https://localhost:7241」といったテキストが標準出力への書き込まれるので (下図例)、これを検知すればよい。
![_d0079457_14065210.png]()
具体的な実装例は以下のとおり。
using System.Diagnostics;
// ASP.NET Core Web アプリプロジェクトを "dotnet run"
// コマンドで起動する Process オブジェクトを準備。
// その際、標準出力のリダイレクトを指定。
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "run",
WorkingDirectory = @"...",
RedirectStandardOutput = true
}
};
// "Now listening on..." の出力を待ち合わせるための
// タスク完了ソースを用意
var tcs = new TaskCompletionSource();
// 標準出力発生のイベントをハンドルし、"Now listening on..." が出力
// されたら、タスクを完了させる
process.OutputDataReceived += (_, e) =>
{
var output = e.Data?.Trim() ?? string.Empty;
if (output.StartsWith("Now listening on: http://localhost:"))
{
tcs.TrySetResult();
}
};
// プロセスを開始 & 標準出力読み取りを開始
process.Start();
process.BeginOutputReadLine();
// "Now listening on..." の出力が検出されるまで非同期で待機
await tcs.Task;
// ここまで来たら、起動した ASP.NET Core Web アプリは、
// HTTP リッスン開始している
Console.WriteLine("DETECT START LISTENING!");
ちゃんと実装するとなると大変!ただ、上記実装例は、もう既にずいぶんと冗長な感じがすることに加え、標準エラー出力については実装していない。また、正常に動作しているぶんにはよいが、起動した ASP.NET Core Web アプリケーションプロセスが何らかのエラーを発生していた場合に、コンソールには標準出力や標準エラー出力の内容は表示されないため (出力をリダイレクトしてイベントで拾うようになってしまっているので)、何かあったときのトラブルシューティングが困難極める。タイムアウトの仕組みもないので、"Now listening on:..." の出力がされないと永遠に await し続けてしまい、エラーの発生に気づくことすらできなさそうだ。その他にも、このコンソールアプリケーション自体が例外を発生した時に、起動した ASP.NET Core Web アプリケーションプロセスをどう始末するのか、といったことも考慮が必要だろう。
それら考慮点をすべて網羅していくと、結構なコード量に膨れ上がることになる。
Process クラスの強化版的なライブラリ世の中には、こういった需要に応えるような、Process クラスの強化版的なライブラリが、無償利用可能な NuGet パッケージとして公開されていたりする。それらライブラリのうち、自分の中での推しのひとつは、ProcessX というライブラリだ (下記リンク先)。
非同期 Stream で標準出力を扱えたり、文字列に対する await 構文で外部コマンドを手軽に実行、完了を待機できたりと、なかなかに便利で興味深い機能が提供されている。
ただ、この ProcessX をもってしても、「指定の出力が現れるまで待機」や「トラブル発生時に、それまでの標準出力・標準エラー出力を見たい」といった今回の要件に伴う需要に応えるには、もう一息、実装を積み重ねる必要があった。
結局自作したそこで、今回要件のような、エッジケースに特化した、新たな "Process クラスの強化版" ライブラリを、もうひとつ新たに作成して OSS として世に送り出すことにした。
そうして作成したのが "XProcess" である (下記リンク先、MIT license)。
この XProcess は、前述のとおり今回の自分の要件に特化してあるため、先に挙げた「ASP.NET Core Web アプリケーションを起動し、HTTP リッスン状態になるまで待機」のコードは、以下にまで端的に書けるようになる。
using Toolbelt.Diagnostics;
// 作業フォルダ指定して "dotnet run" で ASP.NET Core Web アプリ
// プロジェクトを起動
using var process = XProcess.Start("dotnet", "run", workingDirectory: @"...");
// 指定した Predection が true を返す出力があるまで非同期で待機、
// 但し、最大 5 秒のタイムアウトを指定する。
var found = await process.WaitForOutputAsync(output =>
{
output = output.Trim();
return output.StartsWith("Now listening on: http://localhost:");
}, millsecondsTimeout: 5000);
// true が返されたら、目的の出力があったということ
if (found) {
Console.WriteLine("DETECT START LISTENING!");
} else {
Console.WriteLine("SOME ERROR OCCURED!");
// Output プロパティを参照すれば、それまでの標準出力と標準エラー出力
// の内容を参照できる
Console.WriteLine(process.Output);
}
まとめこのように、特定のユースケースに特化された XProcess は、ProcessX ほどの柔軟性と高度な機能は備えていないものの、その利用シナリオに適合した場合は、より簡潔にそのやりたいことを実現可能だ。
XProcess は、もっぱら自分の要件を満たすために作成した、特化されたライブラリであり、とはいえどうせ作ったのなら OSS & 無償利用可能なパッケージとして世の中に公開したら、同じ需要を持つ誰かの便利に役立つかもしれない、というノリでリリースしている。なので、"打倒、ProcessX!" みたいなつもりはないので悪しからず。
なので、XProcess に関しては、機能追加要望やバグ修正の Issue を上げられても、あまり積極的には対応しないかもしれない。その代わり、自分の新たな需要に対して機能不足やバグがあった場合は、速やかに改善・修正されるかもしれない、そんな性格のゆるい感じのライブラリだ。
とある要件で、ASP.NET Core Web アプリケーションを起動し、HTTP リッスン状態になるまでを待機して、それからその ASP.NET Core Web アプリに HTTP 要求を送信して応答を得るという、そのようなコンソールアプリケーションを作成する需要が発生した。
もちろん、.NET の標準ライブラリにある System.Diagnostics.Process クラスを使えば実装できる。Process クラスに対し、起動したい実行可能ファイルパスやコマンドライン引数を指定するのに加えて、標準出力や標準エラー出力のリダイレクトを指定し、および Process クラスから発火される、標準出力や標準エラー出力への書き込み発生のイベントをハンドルすればよい。
ASP.NET Core Web アプリケーションであれば、HTTP リッスン状態になると「Now listening on: https://localhost:7241」といったテキストが標準出力への書き込まれるので (下図例)、これを検知すればよい。

具体的な実装例は以下のとおり。
using System.Diagnostics;
// ASP.NET Core Web アプリプロジェクトを "dotnet run"
// コマンドで起動する Process オブジェクトを準備。
// その際、標準出力のリダイレクトを指定。
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "run",
WorkingDirectory = @"...",
RedirectStandardOutput = true
}
};
// "Now listening on..." の出力を待ち合わせるための
// タスク完了ソースを用意
var tcs = new TaskCompletionSource();
// 標準出力発生のイベントをハンドルし、"Now listening on..." が出力
// されたら、タスクを完了させる
process.OutputDataReceived += (_, e) =>
{
var output = e.Data?.Trim() ?? string.Empty;
if (output.StartsWith("Now listening on: http://localhost:"))
{
tcs.TrySetResult();
}
};
// プロセスを開始 & 標準出力読み取りを開始
process.Start();
process.BeginOutputReadLine();
// "Now listening on..." の出力が検出されるまで非同期で待機
await tcs.Task;
// ここまで来たら、起動した ASP.NET Core Web アプリは、
// HTTP リッスン開始している
Console.WriteLine("DETECT START LISTENING!");
ちゃんと実装するとなると大変!ただ、上記実装例は、もう既にずいぶんと冗長な感じがすることに加え、標準エラー出力については実装していない。また、正常に動作しているぶんにはよいが、起動した ASP.NET Core Web アプリケーションプロセスが何らかのエラーを発生していた場合に、コンソールには標準出力や標準エラー出力の内容は表示されないため (出力をリダイレクトしてイベントで拾うようになってしまっているので)、何かあったときのトラブルシューティングが困難極める。タイムアウトの仕組みもないので、"Now listening on:..." の出力がされないと永遠に await し続けてしまい、エラーの発生に気づくことすらできなさそうだ。その他にも、このコンソールアプリケーション自体が例外を発生した時に、起動した ASP.NET Core Web アプリケーションプロセスをどう始末するのか、といったことも考慮が必要だろう。
それら考慮点をすべて網羅していくと、結構なコード量に膨れ上がることになる。
Process クラスの強化版的なライブラリ世の中には、こういった需要に応えるような、Process クラスの強化版的なライブラリが、無償利用可能な NuGet パッケージとして公開されていたりする。それらライブラリのうち、自分の中での推しのひとつは、ProcessX というライブラリだ (下記リンク先)。
非同期 Stream で標準出力を扱えたり、文字列に対する await 構文で外部コマンドを手軽に実行、完了を待機できたりと、なかなかに便利で興味深い機能が提供されている。
ただ、この ProcessX をもってしても、「指定の出力が現れるまで待機」や「トラブル発生時に、それまでの標準出力・標準エラー出力を見たい」といった今回の要件に伴う需要に応えるには、もう一息、実装を積み重ねる必要があった。
結局自作したそこで、今回要件のような、エッジケースに特化した、新たな "Process クラスの強化版" ライブラリを、もうひとつ新たに作成して OSS として世に送り出すことにした。
そうして作成したのが "XProcess" である (下記リンク先、MIT license)。
この XProcess は、前述のとおり今回の自分の要件に特化してあるため、先に挙げた「ASP.NET Core Web アプリケーションを起動し、HTTP リッスン状態になるまで待機」のコードは、以下にまで端的に書けるようになる。
using Toolbelt.Diagnostics;
// 作業フォルダ指定して "dotnet run" で ASP.NET Core Web アプリ
// プロジェクトを起動
using var process = XProcess.Start("dotnet", "run", workingDirectory: @"...");
// 指定した Predection が true を返す出力があるまで非同期で待機、
// 但し、最大 5 秒のタイムアウトを指定する。
var found = await process.WaitForOutputAsync(output =>
{
output = output.Trim();
return output.StartsWith("Now listening on: http://localhost:");
}, millsecondsTimeout: 5000);
// true が返されたら、目的の出力があったということ
if (found) {
Console.WriteLine("DETECT START LISTENING!");
} else {
Console.WriteLine("SOME ERROR OCCURED!");
// Output プロパティを参照すれば、それまでの標準出力と標準エラー出力
// の内容を参照できる
Console.WriteLine(process.Output);
}
まとめこのように、特定のユースケースに特化された XProcess は、ProcessX ほどの柔軟性と高度な機能は備えていないものの、その利用シナリオに適合した場合は、より簡潔にそのやりたいことを実現可能だ。
XProcess は、もっぱら自分の要件を満たすために作成した、特化されたライブラリであり、とはいえどうせ作ったのなら OSS & 無償利用可能なパッケージとして世の中に公開したら、同じ需要を持つ誰かの便利に役立つかもしれない、というノリでリリースしている。なので、"打倒、ProcessX!" みたいなつもりはないので悪しからず。
なので、XProcess に関しては、機能追加要望やバグ修正の Issue を上げられても、あまり積極的には対応しないかもしれない。その代わり、自分の新たな需要に対して機能不足やバグがあった場合は、速やかに改善・修正されるかもしれない、そんな性格のゆるい感じのライブラリだ。