C# Process ExitCodeの取得方法|WaitForExitの使い方とエラー対処を解説

はじめに

C#で外部プログラムやコマンドを実行するとき、処理が成功したのか、失敗したのかを判断するために重要になるのがProcess.ExitCodeです。

たとえば、C#からexeファイル、バッチファイル、コマンドプロンプト、PowerShell、Git、Python、ffmpegなどの外部プロセスを実行した場合、実行結果をExitCodeで確認できます。

ただし、ExitCodeはプロセスが終了してからでないと取得できません。終了前に取得しようとするとInvalidOperationExceptionが発生することがあります。そのため、基本的にはWaitForExitでプロセスの終了を待ってからExitCodeを読む必要があります。

この記事では、C#のProcessクラスを使ってExitCodeを取得する方法、WaitForExitの正しい使い方、よくあるエラー、標準出力・標準エラー出力の取得方法、非同期処理での実装例まで解説します。

1. C#のProcess ExitCodeとは

Process.ExitCodeとは、C#のSystem.Diagnostics.Processクラスで起動した外部プロセスの終了コードを取得するためのプロパティです。

終了コードは、実行したプログラムが終了時にOSへ返す数値です。C#側では、その数値をExitCodeプロパティから取得できます。

C#
int exitCode = process.ExitCode;

ただし、ExitCodeを取得できるのは、対象のプロセスが終了した後です。まだ実行中のプロセスに対してExitCodeを読み取ると例外が発生します。

1-1. ExitCodeで取得できる値の意味

ExitCodeで取得できる値は、外部プロセスが終了時に返した整数値です。

一般的には、次のように扱われます。

0     : 正常終了
0以外 : 異常終了、警告、エラーなど

たとえば、C#から外部ツールを実行してExitCode0なら成功、1なら一般的なエラー、2なら引数エラーのように判断するケースがあります。

ただし、終了コードの意味は実行するプログラムごとに異なります。必ずしも1が同じ意味になるとは限りません。

1-2. 終了コード0と0以外の違い

多くのコマンドラインツールでは、終了コード0を正常終了として扱います。

ExitCode = 0

これは「処理が成功した」ことを示すのが一般的です。

一方で、0以外の終了コードは、何らかの問題が発生したことを示す場合が多いです。

ExitCode = 1
ExitCode = 2
ExitCode = -1

ただし、アプリケーションによっては、0以外でも警告扱いだったり、特定の状態を表していたりすることがあります。そのため、ExitCodeだけでなく、標準出力や標準エラー出力もあわせて確認することが重要です。

1-3. 外部プロセス実行時にExitCodeが必要になるケース

C#でProcess.ExitCodeが必要になる主なケースは次のとおりです。

外部exeを実行して成功・失敗を判定したい場合、バッチファイルやPowerShellスクリプトの実行結果を確認したい場合、CI/CDや自動化ツールでコマンドの成否を判定したい場合、ffmpegやGitなどのコマンドラインツールの結果を確認したい場合などです。

たとえば、C#からファイル変換ツールを実行する場合、変換が成功したかどうかをExitCodeで判断できます。

C#
if (process.ExitCode == 0)
{
Console.WriteLine("変換に成功しました。");
}
else
{
Console.WriteLine("変換に失敗しました。");
}

1-4. Processクラスでできること

Processクラスを使うと、C#から外部プロセスを起動したり、終了を待機したり、実行結果を取得したりできます。

代表的な機能は次のとおりです。

C#
Process.Start();        // プロセスを起動
process.WaitForExit(); // プロセス終了まで待機
process.ExitCode; // 終了コードを取得
process.HasExited; // 終了済みか確認
process.Kill(); // プロセスを強制終了

また、ProcessStartInfoを使うことで、標準出力や標準エラー出力を取得したり、作業ディレクトリを指定したり、シェル経由で実行するかどうかを制御したりできます。

2. C#でProcessのExitCodeを取得する基本手順

C#でProcess.ExitCodeを取得する基本的な流れは、次の3ステップです。

1. Process.Startで外部プログラムを起動する
2. WaitForExitでプロセスの終了を待つ
3. ExitCodeプロパティから終了コードを取得する

重要なのは、ExitCodeを取得する前に必ずプロセスの終了を待つことです。

2-1. Process.Startで外部プログラムを起動する

外部プログラムを起動するには、Process.Startを使います。

C#
using System.Diagnostics;

Process? process = Process.Start("notepad.exe");

この例では、メモ帳を起動しています。

ただし、Process.Startは条件によってnullを返す可能性があります。そのため、実務では戻り値のnullチェックを行うのが安全です。

C#
Process? process = Process.Start("notepad.exe");

if (process == null)
{
Console.WriteLine("プロセスを開始できませんでした。");
return;
}

2-2. WaitForExitでプロセス終了を待つ

ExitCodeは、プロセスが終了してから取得します。

そのため、WaitForExitを呼び出して、外部プロセスが終了するまで待機します。

C#
process.WaitForExit();

WaitForExit()は、対象プロセスが終了するまで現在のスレッドをブロックします。

コンソールアプリやバッチ処理では使いやすいですが、GUIアプリやWebアプリで使う場合は、処理が止まらないようにタイムアウトや非同期処理を検討する必要があります。

2-3. ExitCodeプロパティから終了コードを取得する

プロセスが終了した後に、ExitCodeプロパティを読み取ります。

C#
int exitCode = process.ExitCode;
Console.WriteLine($"ExitCode: {exitCode}");

ExitCode0であれば正常終了、0以外であればエラーや警告として扱うのが一般的です。

C#
if (exitCode == 0)
{
Console.WriteLine("正常終了しました。");
}
else
{
Console.WriteLine("異常終了しました。");
}

2-4. 基本コード例

以下は、C#で外部プロセスを起動し、WaitForExitで終了を待ってからExitCodeを取得する基本コードです。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
using Process? process = Process.Start("cmd.exe", "/c exit 0");

if (process == null)
{
Console.WriteLine("プロセスを開始できませんでした。");
return;
}

process.WaitForExit();

int exitCode = process.ExitCode;

Console.WriteLine($"ExitCode: {exitCode}");

if (exitCode == 0)
{
Console.WriteLine("正常終了しました。");
}
else
{
Console.WriteLine("異常終了しました。");
}
}
}

この例では、cmd.exe /c exit 0を実行しているため、終了コードは0になります。

/cは、指定したコマンドを実行した後にコマンドプロンプトを終了するオプションです。

3. WaitForExitの正しい使い方

Process.ExitCodeを正しく取得するには、WaitForExitの使い方を理解しておく必要があります。

WaitForExitには、主に次の2種類があります。

C#
process.WaitForExit();
process.WaitForExit(milliseconds);

それぞれ動作が異なるため、用途に応じて使い分けます。

3-1. WaitForExit()とWaitForExit(int milliseconds)の違い

WaitForExit()は、プロセスが終了するまで無期限に待機します。

C#
process.WaitForExit();

外部プロセスがすぐに終わる場合は問題ありませんが、プロセスが応答しなくなった場合、呼び出し元の処理も止まり続けます。

一方、WaitForExit(int milliseconds)は、指定したミリ秒だけ終了を待機します。

C#
bool exited = process.WaitForExit(5000);

この例では、最大5秒間だけ待機します。

戻り値はboolです。

true  : 指定時間内にプロセスが終了した
false : 指定時間内にプロセスが終了しなかった

タイムアウトを設定したい場合は、WaitForExit(int milliseconds)を使います。

3-2. タイムアウトを設定して待機する方法

外部プロセスが長時間終了しない可能性がある場合は、タイムアウトを設定するのが安全です。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
using Process? process = Process.Start("cmd.exe", "/c timeout /t 10");

if (process == null)
{
Console.WriteLine("プロセスを開始できませんでした。");
return;
}

bool exited = process.WaitForExit(3000);

if (exited)
{
Console.WriteLine($"ExitCode: {process.ExitCode}");
}
else
{
Console.WriteLine("タイムアウトしました。");
process.Kill();
}
}
}

この例では、プロセスの終了を3秒だけ待ちます。3秒以内に終了しなければタイムアウトとして扱い、Killで強制終了しています。

3-3. HasExitedで終了状態を確認する方法

HasExitedを使うと、プロセスが終了済みかどうかを確認できます。

C#
if (process.HasExited)
{
int exitCode = process.ExitCode;
}
else
{
Console.WriteLine("まだプロセスは実行中です。");
}

ExitCodeを取得する前にHasExitedを確認することで、終了前にExitCodeを読んで例外が発生することを防げます。

ただし、通常はWaitForExitで終了を待ってからExitCodeを取得する方がシンプルです。

3-4. WaitForExitを使わずにExitCodeを読むとどうなるか

プロセスが終了していない状態でExitCodeを取得しようとすると、InvalidOperationExceptionが発生する可能性があります。

C#
using Process? process = Process.Start("cmd.exe", "/c timeout /t 5");

if (process == null)
{
return;
}

// 終了を待たずにExitCodeを取得しようとしている
int exitCode = process.ExitCode;

このコードでは、外部プロセスがまだ実行中の場合、次のような例外が発生します。

System.InvalidOperationException

ExitCodeは「終了したプロセスの結果」を表す値です。そのため、終了前に取得できないのは自然な動作です。

正しくは、次のようにします。

C#
process.WaitForExit();
int exitCode = process.ExitCode;

または、HasExitedで確認してから取得します。

C#
if (process.HasExited)
{
int exitCode = process.ExitCode;
}

4. ExitCode取得時によくあるエラーと対処法

Process.ExitCodeを扱うときによく発生するエラーには、いくつかのパターンがあります。

特に多いのは、プロセスが終了していない状態でExitCodeを取得するケースです。

4-1. InvalidOperationExceptionが発生する原因

InvalidOperationExceptionが発生する主な原因は、対象のプロセスがまだ終了していないことです。

C#
int exitCode = process.ExitCode;

この時点でプロセスが実行中だと、ExitCodeを取得できません。

対処法は、WaitForExitで終了を待つことです。

C#
process.WaitForExit();
int exitCode = process.ExitCode;

また、タイムアウトを使う場合は、戻り値を確認してからExitCodeを取得します。

C#
bool exited = process.WaitForExit(5000);

if (exited)
{
int exitCode = process.ExitCode;
}
else
{
Console.WriteLine("プロセスが終了していません。");
}

4-2. プロセスが終了していない場合の対処

プロセスが終了していない場合、無理にExitCodeを取得してはいけません。

まず、HasExitedで状態を確認します。

C#
if (!process.HasExited)
{
Console.WriteLine("プロセスはまだ実行中です。");
return;
}

int exitCode = process.ExitCode;

タイムアウト後に強制終了したい場合は、Killを使います。

C#
if (!process.WaitForExit(10000))
{
process.Kill();
process.WaitForExit();
Console.WriteLine("プロセスを強制終了しました。");
}

Killを呼び出した後も、完全に終了するまで少し時間がかかる場合があります。そのため、Kill後に再度WaitForExitを呼び出すと安全です。

4-3. Process.Startがnullを返す場合の注意点

Process.Startは、起動方法によってはnullを返す可能性があります。

そのため、次のようにnullチェックを行います。

C#
Process? process = Process.Start("cmd.exe", "/c exit 0");

if (process == null)
{
Console.WriteLine("Process.Startがnullを返しました。");
return;
}

Process.Startnullの場合、process.WaitForExit()process.ExitCodeを呼ぶとNullReferenceExceptionになります。

特に、関連付けられたアプリをシェル経由で開く場合や、プロセス情報を取得できない起動方法では注意が必要です。

4-4. ファイルパスや実行権限のエラー対処

指定したファイルが存在しない場合や、実行権限がない場合、Process.Startで例外が発生することがあります。

たとえば、存在しないexeを指定するとエラーになります。

C#
Process.Start("not-exists.exe");

このような場合に備えて、try-catchで例外を捕捉します。

C#
try
{
using Process? process = Process.Start("not-exists.exe");

if (process == null)
{
Console.WriteLine("プロセスを開始できませんでした。");
return;
}

process.WaitForExit();
Console.WriteLine($"ExitCode: {process.ExitCode}");
}
catch (Exception ex)
{
Console.WriteLine($"プロセス実行時にエラーが発生しました: {ex.Message}");
}

また、ファイルパスにスペースが含まれる場合は、ProcessStartInfoを使ってFileNameArgumentsを分けて指定するのがおすすめです。

C#
var startInfo = new ProcessStartInfo
{
FileName = @"C:\Tools\My App\tool.exe",
Arguments = "--version"
};

4-5. try-catchで安全にExitCodeを取得する方法

実務では、外部プロセスの実行には失敗がつきものです。

ファイルが存在しない、権限がない、プロセスが終了しない、引数が間違っているなど、さまざまな原因で失敗します。

そのため、次のようにtry-catchを使って安全に実装します。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
try
{
using Process? process = Process.Start("cmd.exe", "/c exit 1");

if (process == null)
{
Console.WriteLine("プロセスを開始できませんでした。");
return;
}

process.WaitForExit();

int exitCode = process.ExitCode;
Console.WriteLine($"ExitCode: {exitCode}");

if (exitCode == 0)
{
Console.WriteLine("正常終了しました。");
}
else
{
Console.WriteLine("異常終了しました。");
}
}
catch (Exception ex)
{
Console.WriteLine($"例外が発生しました: {ex.Message}");
}
}
}

この例では、ExitCode1になるため、異常終了として処理されます。

5. 標準出力・標準エラー出力も取得する方法

ExitCodeだけでは、失敗理由がわからないことがあります。

そのため、外部プロセスの標準出力と標準エラー出力を取得すると、原因調査がしやすくなります。

標準出力は通常の実行結果、標準エラー出力はエラーメッセージや警告メッセージを出力するために使われます。

5-1. ProcessStartInfoの基本設定

標準出力や標準エラー出力を取得するには、ProcessStartInfoを使います。

C#
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c echo Hello",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

重要なのは、RedirectStandardOutputRedirectStandardErrorを使う場合、UseShellExecutefalseにする必要があることです。

5-2. RedirectStandardOutputで標準出力を取得する

標準出力を取得するには、RedirectStandardOutputtrueにします。

C#
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c echo Hello",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};

using Process process = new Process();
process.StartInfo = startInfo;
process.Start();

string output = process.StandardOutput.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);
Console.WriteLine($"ExitCode: {process.ExitCode}");

この例では、echo Helloの結果をC#側で文字列として取得できます。

5-3. RedirectStandardErrorでエラー内容を取得する

標準エラー出力を取得するには、RedirectStandardErrortrueにします。

C#
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dir not-exists-folder",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using Process process = new Process();
process.StartInfo = startInfo;
process.Start();

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine($"Output: {output}");
Console.WriteLine($"Error: {error}");
Console.WriteLine($"ExitCode: {process.ExitCode}");

存在しないフォルダを指定しているため、エラー内容が標準エラー出力に出力されます。

5-4. UseShellExecute=falseが必要な理由

RedirectStandardOutputRedirectStandardErrorを使う場合は、UseShellExecutefalseにする必要があります。

C#
UseShellExecute = false

UseShellExecutetrueの場合、OSのシェルを使ってプロセスを起動します。この場合、標準出力や標準エラー出力をC#側で直接リダイレクトできません。

そのため、出力を取得したい場合は、必ず次のように設定します。

C#
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c echo Hello",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
};

5-5. 出力取得時のデッドロックを防ぐ方法

標準出力と標準エラー出力を両方取得する場合、読み取り方法によってはデッドロックが発生することがあります。

特に、出力量が多いプロセスで、片方のストリームだけを先にReadToEndすると、もう片方のバッファが詰まってプロセスが停止することがあります。

安全な方法のひとつは、非同期で読み取ることです。

C#
using System;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c echo output && echo error 1>&2",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using Process process = new Process();
process.StartInfo = startInfo;

process.Start();

Task<string> outputTask = process.StandardOutput.ReadToEndAsync();
Task<string> errorTask = process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

string output = await outputTask;
string error = await errorTask;

Console.WriteLine($"Output: {output}");
Console.WriteLine($"Error: {error}");
Console.WriteLine($"ExitCode: {process.ExitCode}");
}
}

標準出力と標準エラー出力を同時に読み取ることで、バッファ詰まりによるデッドロックを防ぎやすくなります。

6. 実践的なコード例

ここからは、C#でProcess.ExitCodeを取得する実践的なコード例を紹介します。

exeファイル、コマンドプロンプト、PowerShell、タイムアウト、終了コードによる分岐など、実務で使いやすいパターンを確認していきます。

6-1. exeファイルを実行してExitCodeを取得する例

外部exeを実行してExitCodeを取得する基本例です。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
var startInfo = new ProcessStartInfo
{
FileName = @"C:\Tools\sample.exe",
Arguments = "--mode test",
UseShellExecute = false,
CreateNoWindow = true
};

try
{
using Process process = new Process();
process.StartInfo = startInfo;

process.Start();
process.WaitForExit();

int exitCode = process.ExitCode;

Console.WriteLine($"ExitCode: {exitCode}");
}
catch (Exception ex)
{
Console.WriteLine($"実行エラー: {ex.Message}");
}
}
}

実行ファイルのパスや引数は、FileNameArgumentsに分けて指定すると扱いやすくなります。

6-2. コマンドプロンプトのコマンドを実行する例

dircopyなど、コマンドプロンプトのコマンドを実行する場合は、cmd.exe /cを使います。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dir C:\\",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using Process process = new Process();
process.StartInfo = startInfo;

process.Start();

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

if (!string.IsNullOrWhiteSpace(error))
{
Console.WriteLine($"Error: {error}");
}

Console.WriteLine($"ExitCode: {process.ExitCode}");
}
}

cmd.exe /cは、指定したコマンドを実行した後に終了します。

終了後、cmd.exeの終了コードをExitCodeとして取得できます。

6-3. PowerShellコマンドを実行する例

PowerShellコマンドをC#から実行する場合は、powershell.exeまたはpwshを使います。

Windows PowerShellを使う例は次のとおりです。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
var startInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -ExecutionPolicy Bypass -Command \"Write-Output 'Hello PowerShell'; exit 0\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using Process process = new Process();
process.StartInfo = startInfo;

process.Start();

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine($"Output: {output}");
Console.WriteLine($"Error: {error}");
Console.WriteLine($"ExitCode: {process.ExitCode}");
}
}

PowerShell側でexit 0exit 1を指定すると、その値をC#側のProcess.ExitCodeで取得できます。

PowerShell 7以降を使う場合は、環境に応じてFileNamepwshに変更します。

C#
FileName = "pwsh"

6-4. タイムアウト時にKillする例

外部プロセスが終了しない場合に備えて、タイムアウト処理を入れる例です。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c timeout /t 30",
UseShellExecute = false,
CreateNoWindow = true
};

using Process process = new Process();
process.StartInfo = startInfo;

process.Start();

bool exited = process.WaitForExit(5000);

if (exited)
{
Console.WriteLine($"ExitCode: {process.ExitCode}");
}
else
{
Console.WriteLine("タイムアウトしたためプロセスを終了します。");

process.Kill();
process.WaitForExit();

Console.WriteLine("プロセスを強制終了しました。");
}
}
}

WaitForExit(5000)により、最大5秒だけ待機します。

指定時間内に終了しない場合はfalseが返るため、Killで強制終了しています。

6-5. ExitCodeに応じて処理を分岐する例

ExitCodeによって後続処理を分岐する例です。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
using Process? process = Process.Start("cmd.exe", "/c exit 2");

if (process == null)
{
Console.WriteLine("プロセスを開始できませんでした。");
return;
}

process.WaitForExit();

switch (process.ExitCode)
{
case 0:
Console.WriteLine("正常終了しました。");
break;

case 1:
Console.WriteLine("一般的なエラーが発生しました。");
break;

case 2:
Console.WriteLine("引数または入力値に問題があります。");
break;

default:
Console.WriteLine($"未定義の終了コードです: {process.ExitCode}");
break;
}
}
}

このように、外部プログラム側で定義されている終了コードに応じて、C#側の処理を分けることができます。

7. 非同期でProcessのExitCodeを取得する方法

C#では、WaitForExitを使った同期的な待機だけでなく、WaitForExitAsyncを使った非同期待機もできます。

GUIアプリ、Webアプリ、非同期処理が中心のアプリケーションでは、WaitForExitAsyncを使うとスレッドをブロックせずに外部プロセスの終了を待てます。

7-1. WaitForExitAsyncを使う方法

WaitForExitAsyncは、プロセスが終了するまで非同期に待機するメソッドです。

C#
await process.WaitForExitAsync();

WaitForExit()のように現在のスレッドをブロックせず、awaitで自然に待機できます。

ただし、WaitForExitAsyncは主に.NET 5以降で利用できます。.NET Frameworkでは基本的に使えないため、イベントやTaskCompletionSourceなどを使って代替実装する必要があります。

7-2. async/awaitで外部プロセスを待機する例

以下は、WaitForExitAsyncを使って非同期でExitCodeを取得する例です。

C#
using System;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c echo Hello && exit 0",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using Process process = new Process();
process.StartInfo = startInfo;

process.Start();

Task<string> outputTask = process.StandardOutput.ReadToEndAsync();
Task<string> errorTask = process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

string output = await outputTask;
string error = await errorTask;

Console.WriteLine($"Output: {output}");
Console.WriteLine($"Error: {error}");
Console.WriteLine($"ExitCode: {process.ExitCode}");
}
}

このコードでは、標準出力と標準エラー出力を非同期で読み取りつつ、プロセスの終了も非同期で待機しています。

7-3. CancellationTokenでキャンセル可能にする方法

WaitForExitAsyncにはCancellationTokenを渡すこともできます。

C#
await process.WaitForExitAsync(cancellationToken);

キャンセル可能にすることで、一定時間を超えた場合に処理を中断できます。

C#
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c timeout /t 30",
UseShellExecute = false,
CreateNoWindow = true
};

using Process process = new Process();
process.StartInfo = startInfo;

process.Start();

try
{
await process.WaitForExitAsync(cts.Token);
Console.WriteLine($"ExitCode: {process.ExitCode}");
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。プロセスを終了します。");

if (!process.HasExited)
{
process.Kill();
await process.WaitForExitAsync();
}
}
}
}

CancellationTokenで待機をキャンセルしても、外部プロセス自体が自動で終了するとは限りません。そのため、必要に応じてKillを呼び出します。

7-4. 同期処理と非同期処理の使い分け

WaitForExitWaitForExitAsyncは、アプリケーションの種類によって使い分けます。

コンソールアプリや単純なバッチ処理では、WaitForExitでも問題ないことが多いです。

C#
process.WaitForExit();

一方で、GUIアプリやWebアプリでは、同期的に待機すると画面が固まったり、リクエスト処理のスレッドを長時間占有したりする可能性があります。

そのような場合は、WaitForExitAsyncを使う方が適しています。

C#
await process.WaitForExitAsync();

また、標準出力や標準エラー出力を取得する場合も、非同期読み取りを組み合わせるとデッドロック対策になります。

8. ExitCodeを扱う際の注意点

Process.ExitCodeは便利ですが、扱い方を間違えると、誤判定や例外、デッドロックにつながることがあります。

ここでは、C#で外部プロセスの終了コードを扱う際の注意点を整理します。

8-1. 終了コードの意味は実行するプログラムごとに異なる

終了コード0は正常終了、0以外は異常終了として扱われることが多いですが、厳密な意味はプログラムごとに異なります。

たとえば、あるツールでは1が一般的なエラーを表し、別のツールでは「差分あり」や「警告あり」を表すことがあります。

そのため、外部ツールを使う場合は、そのツールのドキュメントで終了コードの意味を確認することが重要です。

8-2. ExitCodeだけで失敗原因を判断しない

ExitCodeだけでは、失敗の原因まではわかりません。

C#
ExitCode: 1

この情報だけでは、ファイルが存在しないのか、権限がないのか、引数が間違っているのか判断できません。

そのため、実務ではExitCodeとあわせて、標準出力や標準エラー出力をログに残すのがおすすめです。

C#
Console.WriteLine($"ExitCode: {process.ExitCode}");
Console.WriteLine($"Error: {error}");

8-3. 標準エラー出力もあわせて確認する

外部プロセスが失敗した場合、多くのコマンドラインツールは標準エラー出力に詳細なエラーメッセージを出力します。

そのため、ExitCode0以外の場合は、標準エラー出力も確認しましょう。

C#
if (process.ExitCode != 0)
{
Console.WriteLine("外部プロセスが失敗しました。");
Console.WriteLine(error);
}

ログ出力を行う場合も、終了コードだけでなく、実行したコマンド、引数、標準出力、標準エラー出力を残しておくと調査しやすくなります。

8-4. リソース解放のためにusingを使う

ProcessはOSリソースを扱うクラスです。

使い終わったらDisposeしてリソースを解放する必要があります。

C#では、usingを使うと安全に解放できます。

C#
using Process process = new Process();
process.StartInfo = startInfo;

process.Start();
process.WaitForExit();

Console.WriteLine(process.ExitCode);

また、Process.Startの戻り値を使う場合も、次のようにusingを使えます。

C#
using Process? process = Process.Start("cmd.exe", "/c exit 0");

if (process == null)
{
return;
}

process.WaitForExit();
Console.WriteLine(process.ExitCode);

8-5. クロスプラットフォーム実行時の注意点

C#はWindowsだけでなく、LinuxやmacOSでも動作します。

ただし、実行するコマンドはOSによって異なります。

Windowsでは次のようにcmd.exeを使います。

C#
FileName = "cmd.exe";
Arguments = "/c echo Hello";

LinuxやmacOSでは、一般的に/bin/bash/bin/shを使います。

C#
FileName = "/bin/bash";
Arguments = "-c \"echo Hello\"";

また、パス区切り文字、実行権限、シェルの種類、文字コードなども環境によって異なります。

クロスプラットフォーム対応が必要な場合は、OSごとに実行コマンドを分けることを検討しましょう。

C#
using System.Runtime.InteropServices;

string fileName;
string arguments;

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
fileName = "cmd.exe";
arguments = "/c echo Hello";
}
else
{
fileName = "/bin/bash";
arguments = "-c \"echo Hello\"";
}

9. よくある質問

9-1. ExitCodeが取得できないのはなぜ?

ExitCodeが取得できない主な理由は、プロセスがまだ終了していないためです。

Process.ExitCodeは、プロセス終了後に取得できる値です。終了前に読み取るとInvalidOperationExceptionが発生することがあります。

対処法は、WaitForExitで終了を待ってから取得することです。

C#
process.WaitForExit();
int exitCode = process.ExitCode;

タイムアウトを使う場合は、WaitForExitの戻り値を確認します。

C#
if (process.WaitForExit(5000))
{
int exitCode = process.ExitCode;
}
else
{
Console.WriteLine("まだ終了していません。");
}

9-2. WaitForExitは必ず必要?

ExitCodeを確実に取得するには、基本的にWaitForExitが必要です。

ただし、すでにプロセスが終了していることをHasExitedで確認できる場合は、WaitForExitを呼ばずにExitCodeを取得できます。

C#
if (process.HasExited)
{
int exitCode = process.ExitCode;
}

とはいえ、通常の実装では次のように書くのがわかりやすく安全です。

C#
process.WaitForExit();
int exitCode = process.ExitCode;

非同期処理では、WaitForExitAsyncを使います。

C#
await process.WaitForExitAsync();
int exitCode = process.ExitCode;

9-3. ExitCodeが0なのにエラーになることはある?

あります。

ExitCode0でも、アプリケーション側では期待した結果になっていない場合があります。

たとえば、標準エラー出力に警告が出ている、出力ファイルが作成されていない、コマンド自体は成功したが処理結果が空だった、というケースです。

そのため、重要な処理ではExitCodeだけでなく、出力ファイルの存在確認や標準出力、標準エラー出力も確認しましょう。

C#
if (process.ExitCode == 0 && File.Exists(outputFilePath))
{
Console.WriteLine("処理に成功しました。");
}
else
{
Console.WriteLine("処理に失敗した可能性があります。");
}

9-4. バッチファイルの終了コードも取得できる?

取得できます。

バッチファイルを実行した場合も、終了コードをProcess.ExitCodeで取得できます。

C#
using Process? process = Process.Start("cmd.exe", "/c sample.bat");

if (process == null)
{
return;
}

process.WaitForExit();

Console.WriteLine($"ExitCode: {process.ExitCode}");

バッチファイル側では、exit /bで終了コードを返せます。

bat
exit /b 1

この場合、C#側のprocess.ExitCode1になります。

9-5. .NET Frameworkと.NETの違いはある?

基本的なProcess.StartWaitForExitExitCodeの使い方は、.NET Frameworkと.NETで大きく変わりません。

C#
process.WaitForExit();
int exitCode = process.ExitCode;

ただし、WaitForExitAsyncは.NET Frameworkでは基本的に利用できません。

.NET Frameworkで非同期的にプロセス終了を待ちたい場合は、EnableRaisingEventsExitedイベント、またはTaskCompletionSourceなどを使って実装します。

一方、.NET 5以降ではWaitForExitAsyncを使えるため、非同期処理を簡潔に書けます。

C#
await process.WaitForExitAsync();

新しい.NETを使っている場合は、非同期処理ではWaitForExitAsyncを選ぶとよいでしょう。

まとめ

C#で外部プロセスの終了コードを取得するには、System.Diagnostics.ProcessクラスのExitCodeプロパティを使います。

基本的な流れは、Process.Startで外部プログラムを起動し、WaitForExitで終了を待ち、最後にExitCodeを取得するという手順です。

C#
using Process? process = Process.Start("cmd.exe", "/c exit 0");

if (process == null)
{
return;
}

process.WaitForExit();

int exitCode = process.ExitCode;

ExitCodeはプロセス終了後にしか取得できないため、終了前に読み取るとInvalidOperationExceptionが発生することがあります。

また、ExitCode0なら正常終了、0以外なら異常終了として扱うのが一般的ですが、終了コードの意味は実行するプログラムごとに異なります。

実務では、ExitCodeだけでなく、標準出力や標準エラー出力もあわせて取得するのがおすすめです。

C#
RedirectStandardOutput = true;
RedirectStandardError = true;
UseShellExecute = false;

外部プロセスが終了しない可能性がある場合は、WaitForExit(int milliseconds)でタイムアウトを設定し、必要に応じてKillで終了させます。

非同期処理では、.NET 5以降で利用できるWaitForExitAsyncを使うと、スレッドをブロックせずに外部プロセスの終了を待てます。

C#でProcess.ExitCodeを扱うときは、次のポイントを押さえておきましょう。

ExitCodeはプロセス終了後に取得する、WaitForExitまたはWaitForExitAsyncで終了を待つ、Process.Startnullチェックを行う、標準出力と標準エラー出力も確認する、usingでリソースを解放する、タイムアウトや例外処理を入れて安全に実装する。

これらを意識すれば、C#から外部プロセスを実行し、終了コードに応じた安定した処理を実装できます。