C# Processの使い方完全ガイド|外部アプリ起動・標準出力取得・待機・終了処理まで解説

はじめに

C#で外部アプリや外部コマンドを実行したいときに使う代表的なクラスが、System.Diagnostics.Processです。たとえば、C#アプリからメモ帳を起動する、pingコマンドを実行して結果を取得する、ffmpegのような外部CLIを呼び出す、バッチファイルを自動実行する、といった処理を実装できます。

一方で、C# Processは単にProcess.Start()を書けばよい場面だけではありません。標準出力を取得したい場合、エラー内容を取得したい場合、プロセス終了まで待機したい場合、タイムアウトや強制終了を入れたい場合には、ProcessStartInfoWaitForExitRedirectStandardOutputKillなどを正しく組み合わせる必要があります。

この記事では、C# Processの基本から、外部アプリ起動、コマンド実行、標準出力取得、標準エラー取得、終了待機、非同期実行、強制終了、よくあるエラー、安全な使い方までを実用例付きで解説します。

1. C#のProcessとは?外部アプリ・外部コマンドを実行する基本

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

Processクラスは、OS上で動作するプロセスをC#から扱うためのクラスです。C# Processを使うと、外部プログラムの起動、起動したプロセスの終了待機、標準出力や標準エラーの取得、終了コードの確認、プロセスの終了・強制終了などができます。

代表的には、次のような操作が可能です。

  • exeファイルを起動する

  • URLやファイルを既定アプリで開く

  • コマンドプロンプトやPowerShellコマンドを実行する

  • 外部CLIツールをC#から呼び出す

  • 標準出力、標準エラー出力を取得する

  • プロセス終了まで待機する

  • 終了コードを取得する

  • プロセスを終了・強制終了する

  • Exitedイベントで終了を検知する

Processには、プロセス終了時に発生するExitedイベント、標準出力を非同期で受け取るOutputDataReceivedイベント、標準エラーを受け取るErrorDataReceivedイベントなどが用意されています。Microsoft Learn

1-2. Processを使う主な用途

C# Processは、アプリケーション内部だけでは完結しない処理を外部プログラムに任せるときによく使われます。

たとえば、次のような場面です。

C#
using System.Diagnostics;

Process.Start("notepad.exe");

このコードはメモ帳を起動する最もシンプルな例です。実務では、次のような用途で使われます。

  • 社内ツールから外部exeを起動する

  • C#アプリからgitdotnetnpmffmpegなどのCLIを実行する

  • バッチファイルやスクリプトを自動実行する

  • コマンドの実行結果をログとして保存する

  • 外部処理の成功・失敗を終了コードで判定する

  • GUIアプリからレポートファイルやPDFを既定アプリで開く

C# Processを使うと、既存の外部ツールをC#アプリケーションに組み込めるため、自動化や業務アプリ開発で非常に便利です。

1-3. Processを使うために必要な名前空間

C#でProcessを使うには、基本的に次の名前空間を追加します。

C#
using System.Diagnostics;

標準出力の文字コードを指定する場合は、次も使います。

C#
using System.Text;

例外処理でWin32Exceptionを扱う場合は、次を使うこともあります。

C#
using System.ComponentModel;

最小構成は次のようになります。

C#
using System.Diagnostics;

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

1-4. Process.StartとProcessStartInfoの違い

Process.Start()は、外部アプリや外部コマンドを起動するためのメソッドです。単純に起動するだけなら、次のように短く書けます。

C#
Process.Start("notepad.exe");

一方、ProcessStartInfoは、起動方法を細かく制御するための設定クラスです。FileNameArgumentsWorkingDirectoryUseShellExecuteRedirectStandardOutputCreateNoWindowなどを指定できます。ProcessStartInfoでは、少なくともFileNameを設定する必要があります。Microsoft Learn

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe",
Arguments = @"C:\Temp\memo.txt"
};

Process.Start(startInfo);

使い分けの目安は、単純に起動するだけならProcess.Start()、引数・作業ディレクトリ・標準出力・ウィンドウ表示・環境変数などを制御したいならProcessStartInfoです。

2. C#で外部アプリを起動する基本的な使い方

2-1. Process.Startでexeファイルを起動する

exeファイルを起動するだけなら、Process.Startに実行ファイル名を渡します。

C#
using System.Diagnostics;

Process.Start("notepad.exe");

フルパスで指定することもできます。

C#
using System.Diagnostics;

Process.Start(@"C:\Windows\System32\notepad.exe");

パスにスペースが含まれる場合でも、ProcessStartInfo.FileNameにフルパスをそのまま指定すれば問題ありません。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = @"C:\Program Files\MyApp\MyTool.exe"
};

Process.Start(startInfo);

2-2. メモ帳やブラウザなど既定アプリを起動する

URLを既定のブラウザで開く場合は、UseShellExecute = trueを指定します。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "https://example.com",
UseShellExecute = true
};

Process.Start(startInfo);

.txt.pdfなどのファイルを既定アプリで開く場合も、UseShellExecute = trueが基本です。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = @"C:\Temp\sample.pdf",
UseShellExecute = true
};

Process.Start(startInfo);

UseShellExecuteは、OSシェルを使ってプロセスを開始するかどうかを指定するプロパティです。.NET Frameworkでは既定値がtrue、.NET Coreでは既定値がfalseです。URLや関連付けアプリでファイルを開く場合は、この違いに注意してください。Microsoft Learn

2-3. ファイルパスを指定してアプリを開く

特定のアプリでファイルを開きたい場合は、FileNameにアプリ、ArgumentsまたはArgumentListにファイルパスを指定します。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe",
Arguments = @"C:\Temp\memo.txt"
};

Process.Start(startInfo);

ただし、ファイルパスにスペースが含まれる場合は、Argumentsではクォートが必要です。

C#
using System.Diagnostics;

var filePath = @"C:\Temp\my memo.txt";

var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe",
Arguments = $"\"{filePath}\""
};

Process.Start(startInfo);

より安全で扱いやすい方法は、ArgumentListを使うことです。ArgumentListに追加した文字列は、事前にエスケープする必要がありません。Microsoft Learn

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe"
};

startInfo.ArgumentList.Add(@"C:\Temp\my memo.txt");

Process.Start(startInfo);

2-4. 作業ディレクトリを指定して起動する

外部プログラムによっては、相対パスを使ってファイルを読み込むため、作業ディレクトリの指定が必要です。その場合はWorkingDirectoryを設定します。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = @"C:\Tools\MyTool.exe",
WorkingDirectory = @"C:\Tools",
Arguments = "input.txt"
};

Process.Start(startInfo);

WorkingDirectoryを指定しておくと、外部プログラム側でinput.txtのような相対パスを扱うときに、どのフォルダを基準にするかを明確にできます。実務では、実行ファイルの場所と作業対象ファイルの場所を混同しないように、フルパスとWorkingDirectoryを意識して使い分けることが重要です。

2-5. 起動できないときによくある原因

C# Processで外部アプリを起動できない場合、よくある原因は次のとおりです。

  • FileNameのパスが間違っている

  • exeファイルが存在しない

  • PATH環境変数に登録されていないコマンドを名前だけで実行している

  • ファイルパスにスペースがあるのにクォートしていない

  • UseShellExecuteの設定が用途に合っていない

  • 実行権限がない

  • 32bit/64bitのパス差異に引っかかっている

  • カレントディレクトリが想定と違う

  • URLやドキュメントを開くのにUseShellExecute = trueを指定していない

特に.NET Core以降ではUseShellExecuteの既定値がfalseなので、URLやPDFなどを既定アプリで開くコードを.NET Frameworkから移行した場合に動作が変わることがあります。Microsoft Learn

3. ProcessStartInfoで起動方法を細かく制御する

3-1. FileNameとArgumentsの指定方法

ProcessStartInfoで最も重要なのが、FileNameArgumentsです。

FileNameには、起動する実行ファイル名、コマンド名、またはシェル実行するファイルやURLを指定します。Argumentsには、コマンドライン引数を文字列で指定します。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "--info",
UseShellExecute = false
};

Process.Start(startInfo);

複数の引数を指定する場合、Argumentsでは1つの文字列として組み立てます。

C#
var startInfo = new ProcessStartInfo
{
FileName = "mytool.exe",
Arguments = "--input \"C:\\Temp\\input file.txt\" --mode test"
};

ただし、引数の数が増えたり、スペースを含むパスを扱ったりする場合は、ArgumentListの方が安全です。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "mytool.exe",
UseShellExecute = false
};

startInfo.ArgumentList.Add("--input");
startInfo.ArgumentList.Add(@"C:\Temp\input file.txt");
startInfo.ArgumentList.Add("--mode");
startInfo.ArgumentList.Add("test");

Process.Start(startInfo);

3-2. 引数にスペースや日本語を含む場合の注意点

引数にスペースや日本語を含む場合、Argumentsを手動で組み立てるとクォート漏れやエスケープミスが起きやすくなります。

悪い例です。

C#
var path = @"C:\Temp\日本語 ファイル.txt";

var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe",
Arguments = path
};

この場合、スペースを境に別々の引数として解釈される可能性があります。Argumentsを使うなら、次のようにクォートします。

C#
var path = @"C:\Temp\日本語 ファイル.txt";

var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe",
Arguments = $"\"{path}\""
};

よりおすすめなのはArgumentListです。

C#
var path = @"C:\Temp\日本語 ファイル.txt";

var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe"
};

startInfo.ArgumentList.Add(path);

Process.Start(startInfo);

ArgumentListを使うと、引数を1つずつ明確に渡せるため、スペースや日本語を含む値でも扱いやすくなります。

3-3. UseShellExecuteの役割とtrue/falseの違い

UseShellExecuteは、C# Processで非常に重要なプロパティです。

trueの場合は、OSのシェルを使って起動します。URLをブラウザで開く、PDFを既定アプリで開く、拡張子に関連付けられたアプリでファイルを開く、といった処理に向いています。

C#
Process.Start(new ProcessStartInfo
{
FileName = "https://example.com",
UseShellExecute = true
});

falseの場合は、実行ファイルを直接起動します。標準出力や標準エラーを取得したい場合、環境変数を変更したい場合、コンソール画面を非表示にしたい場合などに使います。

C#
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true
};

標準出力を読み取るには、UseShellExecute = falseかつRedirectStandardOutput = trueが必要です。そうでない場合、StandardOutputの読み取りで例外が発生します。Microsoft Learn

3-4. CreateNoWindowでコンソール画面を非表示にする

コンソールアプリやコマンドを実行するとき、黒いコンソール画面を表示したくない場合はCreateNoWindow = trueを指定します。

C#
using System.Diagnostics;

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

Process.Start(startInfo);

標準出力を取得する場合は、次のように組み合わせることが多いです。

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

GUIアプリから外部コマンドを実行する場合、CreateNoWindow = trueを指定しておくと、ユーザーに不要なコンソール画面を見せずに処理できます。

3-5. WindowStyleでウィンドウ表示を制御する

WindowStyleを使うと、起動するプロセスのウィンドウ表示を指定できます。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe",
WindowStyle = ProcessWindowStyle.Minimized
};

Process.Start(startInfo);

指定できる代表的な値は次のとおりです。

  • ProcessWindowStyle.Normal

  • ProcessWindowStyle.Hidden

  • ProcessWindowStyle.Minimized

  • ProcessWindowStyle.Maximized

ただし、対象アプリの種類や起動方法によっては、期待どおりに反映されないことがあります。コンソール画面を表示したくない場合は、UseShellExecute = falseCreateNoWindow = trueの組み合わせも検討しましょう。

3-6. 環境変数を指定してプロセスを起動する

C# Processでは、子プロセスに渡す環境変数を指定できます。EnvironmentVariablesを変更して起動する場合は、UseShellExecute = falseが必要です。UseShellExecutetrueの場合、環境変数を変更して起動しようとすると例外が発生します。Microsoft Learn

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "mytool.exe",
UseShellExecute = false
};

startInfo.EnvironmentVariables["APP_MODE"] = "Production";
startInfo.EnvironmentVariables["LOG_LEVEL"] = "Debug";

Process.Start(startInfo);

.NETの新しい環境では、Environmentプロパティを使う書き方もできます。

C#
var startInfo = new ProcessStartInfo
{
FileName = "mytool.exe",
UseShellExecute = false
};

startInfo.Environment["APP_MODE"] = "Production";

Process.Start(startInfo);

環境変数は、外部ツールの設定切り替え、APIキーの受け渡し、実行モードの制御などに使えます。ただし、機密情報を環境変数に渡す場合は、ログに出力しない、エラー画面に表示しないなどの注意が必要です。

4. C# Processでコマンドを実行する方法

4-1. コマンドプロンプトからコマンドを実行する

Windowsでdircopyなどのコマンドプロンプト組み込みコマンドを実行する場合は、cmd.exeを起動して/cでコマンドを渡します。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dir C:\\Temp",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(startInfo);

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

process.WaitForExit();

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.WriteLine(error);
}

dirはexeファイルではなくcmd.exeの内部コマンドなので、FileName = "dir"ではなく、cmd.exe /c dirのように実行します。

4-2. PowerShellコマンドを実行する

PowerShellコマンドを実行する場合は、Windows PowerShellならpowershell.exe、PowerShell 7以降ならpwshを使います。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

startInfo.ArgumentList.Add("-NoProfile");
startInfo.ArgumentList.Add("-Command");
startInfo.ArgumentList.Add("Get-ChildItem C:\\Temp | Select-Object Name");

using var process = Process.Start(startInfo);

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

process.WaitForExit();

Console.WriteLine(output);

PowerShellではパイプやオブジェクト操作が使えるため、Windows環境の管理タスクや自動化と相性がよいです。ただし、ユーザー入力をそのまま-Commandに連結すると危険なので、信頼できない値を扱う場合は十分に検証してください。

4-3. cmd /cとcmd /kの違い

cmd.exeでコマンドを実行するときによく使うのが/c/kです。

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

C#
Arguments = "/c dir";

/kは、指定したコマンドを実行した後もコマンドプロンプトを開いたままにします。

C#
Arguments = "/k dir";

C# Processから自動実行して結果を取得する場合は、通常/cを使います。/kを使うとコマンドプロンプトが終了しないため、WaitForExitで待機している処理が終わらない原因になります。

4-4. バッチファイルを実行する

バッチファイルを実行する場合も、cmd.exe /cで呼び出す方法がわかりやすいです。

C#
using System.Diagnostics;

var batchPath = @"C:\Scripts\backup.bat";

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

startInfo.ArgumentList.Add("/c");
startInfo.ArgumentList.Add(batchPath);

using var process = Process.Start(startInfo);

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

process.WaitForExit();

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.Error.WriteLine(error);
}

バッチファイルのパスにスペースが含まれる場合でも、ArgumentListを使えば扱いやすくなります。

4-5. 外部ツールやCLIをC#から呼び出す

gitdotnetnpmffmpegなどの外部CLIをC#から呼び出す場合は、基本的にFileNameにコマンド名、ArgumentListに引数を追加します。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "git",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

startInfo.ArgumentList.Add("status");
startInfo.ArgumentList.Add("--short");

using var process = Process.Start(startInfo);

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

process.WaitForExit();

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.Error.WriteLine(error);
}

CLIツールをC# Processから呼び出す場合は、次の点を確認しましょう。

  • コマンドがPATHに登録されているか

  • フルパス指定が必要か

  • 作業ディレクトリが正しいか

  • 標準出力と標準エラーのどちらに結果が出るか

  • 終了コードの仕様はどうなっているか

  • 引数にスペースを含むパスがないか

5. 標準出力を取得する方法

5-1. RedirectStandardOutputの基本

C# Processで外部コマンドの実行結果を取得するには、RedirectStandardOutput = trueを指定します。このとき、UseShellExecute = falseも必要です。StandardOutputを使用するには、UseShellExecutefalseRedirectStandardOutputtrueに設定する必要があります。Microsoft Learn

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};

using var process = Process.Start(startInfo);

string output = process!.StandardOutput.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

この例では、dotnet --versionの標準出力を文字列として取得しています。

5-2. StandardOutput.ReadToEndで結果を取得する

標準出力全体をまとめて取得するには、StandardOutput.ReadToEnd()を使います。

C#
using System.Diagnostics;

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

using var process = Process.Start(startInfo);

string output = process!.StandardOutput.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

標準出力だけを取得する単純なケースでは、この書き方で十分です。ただし、標準出力と標準エラーの両方を大量に出すプロセスでは、同期的な読み取り順序によってデッドロックが起きることがあります。大量出力を扱う場合は、後述する非同期読み取りを検討してください。

5-3. StandardOutputを1行ずつ読み取る方法

出力を1行ずつ処理したい場合は、ReadLine()を使います。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dir C:\\Temp",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};

using var process = Process.Start(startInfo);

string? line;
while ((line = process!.StandardOutput.ReadLine()) != null)
{
Console.WriteLine($"出力: {line}");
}

process.WaitForExit();

ログを加工しながら表示する、特定のキーワードを検出する、進捗を画面に表示する、といった用途では1行ずつ読み取る方法が便利です。

5-4. 標準出力が取得できないときのチェックポイント

StandardOutputが空になる場合は、次を確認してください。

  • UseShellExecute = falseになっているか

  • RedirectStandardOutput = trueになっているか

  • 外部コマンドが本当に標準出力に書いているか

  • 実は標準エラーに出力していないか

  • GUIアプリを起動していないか

  • コマンドがすぐに失敗していないか

  • WaitForExit前後の読み取り順序が適切か

  • 文字コードの問題で読めていないように見えていないか

特にCLIツールの中には、通常ログや進捗を標準エラーに出力するものがあります。たとえばffmpegの進捗や診断ログは標準エラー側に出ることが多いため、標準出力だけを見ていると空に見えることがあります。

5-5. 文字化けを防ぐStandardOutputEncodingの指定

日本語を含む標準出力を取得すると、文字化けすることがあります。その場合はStandardOutputEncodingを指定します。

UTF-8の出力を読む例です。

C#
using System.Diagnostics;
using System.Text;

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c echo こんにちは",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};

using var process = Process.Start(startInfo);

string output = process!.StandardOutput.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

Windowsの古いコマンドや環境によっては、Shift_JIS相当のエンコーディングが必要になることもあります。

C#
using System.Diagnostics;
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dir",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.GetEncoding("shift_jis")
};

文字化け対策では、C#側だけでなく、外部コマンド側がどの文字コードで出力しているかを確認することが重要です。

6. 標準エラー出力と終了コードを取得する方法

6-1. RedirectStandardErrorでエラー内容を取得する

外部コマンドのエラー内容を取得するには、RedirectStandardError = trueを指定します。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dir C:\\NoSuchFolder",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(startInfo);

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

process.WaitForExit();

Console.WriteLine("標準出力:");
Console.WriteLine(output);

Console.WriteLine("標準エラー:");
Console.WriteLine(error);

標準出力は正常系の結果、標準エラーはエラーや警告、診断情報に使われることが多いです。ただし、どちらをどう使うかは外部プログラムの仕様によって異なります。

6-2. StandardError.ReadToEndの使い方

標準エラー全体を文字列で取得するには、StandardError.ReadToEnd()を使います。

C#
string error = process.StandardError.ReadToEnd();

標準出力と同じように、標準エラーにも文字コードの問題があります。日本語のエラーが文字化けする場合は、StandardErrorEncodingを指定します。

C#
using System.Text;

var startInfo = new ProcessStartInfo
{
FileName = "mytool.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};

6-3. ExitCodeで処理結果を判定する

外部プロセスの成功・失敗を判定するには、ExitCodeを確認します。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c exit 1",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(startInfo);

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

process.WaitForExit();

if (process.ExitCode == 0)
{
Console.WriteLine("成功");
}
else
{
Console.WriteLine($"失敗: ExitCode={process.ExitCode}");
Console.WriteLine(error);
}

一般的には0が成功、0以外が失敗を表しますが、終了コードの意味は外部プログラムごとに異なります。実務では、対象ツールのドキュメントや仕様に合わせて判定しましょう。

6-4. 標準出力と標準エラー出力を同時に扱う方法

標準出力と標準エラーの両方を大量に読み取る場合は、片方をReadToEnd()で読み終わるまで待ってからもう片方を読むと、バッファが詰まってデッドロックする可能性があります。Microsoftのドキュメントでも、標準出力と標準エラーを順番に同期読み取りすると、子プロセスが十分な量を書き込んだ場合にデッドロックが発生し得ると説明されています。Microsoft Learn

安全な方法の一つは、ReadToEndAsync()を使って両方を同時に読み取ることです。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dir C:\\Temp",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = new 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);

if (process.ExitCode != 0)
{
Console.Error.WriteLine(error);
}

この書き方なら、標準出力と標準エラーを並行して読み取れるため、大量出力時の詰まりを避けやすくなります。

6-5. エラー時のログ出力実装例

外部コマンドの実行結果をログに残す場合は、標準出力、標準エラー、終了コードをセットで記録すると原因調査がしやすくなります。

C#
using System.Diagnostics;

static async Task<int> RunCommandAsync(string fileName, params string[] args)
{
var startInfo = new ProcessStartInfo
{
FileName = fileName,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

foreach (var arg in args)
{
startInfo.ArgumentList.Add(arg);
}

using var process = new Process
{
StartInfo = startInfo
};

process.Start();

var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

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

Console.WriteLine("=== STDOUT ===");
Console.WriteLine(output);

Console.WriteLine("=== STDERR ===");
Console.WriteLine(error);

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

return process.ExitCode;
}

呼び出し例です。

C#
int exitCode = await RunCommandAsync("dotnet", "--info");

if (exitCode != 0)
{
Console.WriteLine("外部コマンドが失敗しました。");
}

7. プロセス終了まで待機する方法

7-1. WaitForExitの基本的な使い方

起動したプロセスが終了するまで待つには、WaitForExit()を使います。

C#
using System.Diagnostics;

using var process = Process.Start("notepad.exe");

process!.WaitForExit();

Console.WriteLine("メモ帳が終了しました。");

WaitForExit()は、プロセスが終了するまで現在のスレッドをブロックします。コンソールアプリでは使いやすいですが、GUIアプリのUIスレッドで使うと画面が固まる原因になります。

7-2. タイムアウト付きで待機する方法

無限に待ちたくない場合は、ミリ秒を指定するWaitForExit(int milliseconds)を使います。

C#
using System.Diagnostics;

using var process = Process.Start("notepad.exe");

bool exited = process!.WaitForExit(5000);

if (exited)
{
Console.WriteLine("5秒以内に終了しました。");
}
else
{
Console.WriteLine("5秒以内に終了しませんでした。");
}

WaitForExit(Int32)は、指定した時間内にプロセスが終了すればtrue、終了しなければfalseを返します。Microsoft Learn

タイムアウト後に強制終了する例です。

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

if (!process!.WaitForExit(5000))
{
process.Kill();
process.WaitForExit();
}

強制終了はデータ損失につながる可能性があるため、まず正常終了を依頼し、それでも終了しない場合の最終手段として使うのが基本です。

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

HasExitedを使うと、プロセスが既に終了しているかを確認できます。

C#
using System.Diagnostics;

using var process = Process.Start("notepad.exe");

while (!process!.HasExited)
{
Console.WriteLine("まだ実行中です...");
Thread.Sleep(1000);
}

Console.WriteLine("終了しました。");

HasExitedはポーリングで状態を確認したい場合に便利です。ただし、頻繁にループして確認すると無駄な処理が増えるため、実務ではWaitForExitWaitForExitAsyncExitedイベントと使い分けましょう。

7-4. Exitedイベントで終了を検知する

プロセス終了をイベントで検知したい場合は、Exitedイベントを使います。Exitedイベントを受け取るには、EnableRaisingEvents = trueを設定する必要があります。Microsoft Learn

C#
using System.Diagnostics;

var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "notepad.exe"
},
EnableRaisingEvents = true
};

process.Exited += (sender, e) =>
{
Console.WriteLine("プロセスが終了しました。");
process.Dispose();
};

process.Start();

Console.WriteLine("起動しました。");

Exitedイベントを使うと、メイン処理を止めずに終了を検知できます。GUIアプリや常駐アプリでは便利な方法です。

7-5. GUIアプリでWaitForExitを使うと固まる理由

WinFormsやWPFなどのGUIアプリで、ボタンクリックイベント内からWaitForExit()を直接呼ぶと、UIスレッドがブロックされます。その結果、画面の再描画やボタン操作が止まり、アプリが固まったように見えます。

避けるべき例です。

C#
private void Button_Click(object sender, EventArgs e)
{
using var process = Process.Start("notepad.exe");
process!.WaitForExit();

MessageBox.Show("終了しました");
}

GUIアプリでは、WaitForExitAsync()を使うか、バックグラウンドで実行する構成にしましょう。

C#
private async void Button_Click(object sender, EventArgs e)
{
using var process = Process.Start("notepad.exe");

if (process != null)
{
await process.WaitForExitAsync();
MessageBox.Show("終了しました");
}
}

8. 非同期でProcessを実行する方法

8-1. WaitForExitAsyncの使い方

.NET 5以降では、WaitForExitAsync()を使ってプロセス終了を非同期に待機できます。

C#
using System.Diagnostics;

using var process = Process.Start("notepad.exe");

await process!.WaitForExitAsync();

Console.WriteLine("終了しました。");

WaitForExitAsync()はUIスレッドやサーバー処理をブロックしにくいため、現代的なC#アプリでは積極的に使いたい方法です。

キャンセルトークンを使った例です。

C#
using System.Diagnostics;

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

using var process = Process.Start("notepad.exe");

try
{
await process!.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("タイムアウトしました。");

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

キャンセルは「待機をキャンセルする」ためのものであり、必ずしもプロセス自体を終了するわけではありません。タイムアウト時に終了させたい場合は、明示的にKillなどを呼び出します。

8-2. BeginOutputReadLineで標準出力を非同期取得する

標準出力をリアルタイムに受け取りたい場合は、BeginOutputReadLine()を使います。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "ping",
Arguments = "127.0.0.1",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};

using var process = new Process
{
StartInfo = startInfo,
EnableRaisingEvents = true
};

process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
Console.WriteLine($"OUT: {e.Data}");
}
};

process.Start();
process.BeginOutputReadLine();

await process.WaitForExitAsync();

BeginOutputReadLine()を使う場合は、OutputDataReceivedイベントで1行ずつ出力を受け取ります。Processには、標準出力に行が書き込まれるたびに発生するOutputDataReceivedイベントが用意されています。Microsoft Learn

8-3. OutputDataReceivedイベントの使い方

OutputDataReceivedイベントでは、e.Dataに出力された1行分の文字列が入ります。終了時などにはe.Datanullになることがあるため、nullチェックを入れます。

C#
process.OutputDataReceived += (sender, e) =>
{
if (e.Data == null)
{
return;
}

Console.WriteLine(e.Data);
};

ログ画面にリアルタイム表示したい場合は、イベント内でUIコンポーネントを直接更新しないように注意します。WinFormsやWPFでは、UIスレッドに切り替えて表示更新する必要があります。

8-4. 非同期で標準出力と標準エラーを取得する実装例

標準出力と標準エラーをリアルタイムに受け取りつつ、終了コードも取得する実装例です。

C#
using System.Diagnostics;
using System.Text;

static async Task<int> RunProcessWithRealtimeLogAsync(
string fileName,
params string[] args)
{
var output = new StringBuilder();
var error = new StringBuilder();

var startInfo = new ProcessStartInfo
{
FileName = fileName,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

foreach (var arg in args)
{
startInfo.ArgumentList.Add(arg);
}

using var process = new Process
{
StartInfo = startInfo,
EnableRaisingEvents = true
};

process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
output.AppendLine(e.Data);
Console.WriteLine($"OUT: {e.Data}");
}
};

process.ErrorDataReceived += (sender, e) =>
{
if (e.Data != null)
{
error.AppendLine(e.Data);
Console.Error.WriteLine($"ERR: {e.Data}");
}
};

process.Start();

process.BeginOutputReadLine();
process.BeginErrorReadLine();

await process.WaitForExitAsync();

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

return process.ExitCode;
}

呼び出し例です。

C#
int exitCode = await RunProcessWithRealtimeLogAsync("dotnet", "--info");

リアルタイムログが必要ない場合は、ReadToEndAsync()でまとめて取得する方がシンプルです。リアルタイム表示が必要な場合は、BeginOutputReadLine()とイベントを使います。

8-5. デッドロックを防ぐための注意点

C# Processで標準出力や標準エラーを扱うときに最も注意したいのがデッドロックです。

危険な例です。

C#
process.Start();

process.WaitForExit();

string output = process.StandardOutput.ReadToEnd();

子プロセスが大量に標準出力を書き込むと、バッファがいっぱいになり、子プロセスは親プロセスが読み取るのを待ちます。一方、親プロセスはWaitForExit()で子プロセスの終了を待っているため、双方が待ち合う状態になる可能性があります。Microsoftのドキュメントでも、WaitForExitReadToEndより前に呼ぶと、十分な量の出力でデッドロックが発生し得ると説明されています。Microsoft Learn

基本的な対策は次のとおりです。

  • 標準出力だけなら、ReadToEnd()してからWaitForExit()する

  • 標準出力と標準エラーを両方扱うなら、非同期で同時に読む

  • 大量出力があるなら、BeginOutputReadLine()ReadToEndAsync()を使う

  • WaitForExit()だけに頼らずタイムアウトを設定する

  • 外部コマンドが標準入力待ちになっていないか確認する

安全な例です。

C#
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;

9. 起動したプロセスを終了・強制終了する方法

9-1. CloseMainWindowで正常終了を依頼する

GUIアプリを終了させたい場合、まずCloseMainWindow()でメインウィンドウを閉じる要求を送る方法があります。

C#
using System.Diagnostics;

using var process = Process.Start("notepad.exe");

Thread.Sleep(3000);

if (process != null && !process.HasExited)
{
process.CloseMainWindow();
}

CloseMainWindow()は、アプリに対して「閉じてください」と依頼する処理です。未保存の確認ダイアログが出たり、アプリが終了を拒否したりする可能性があります。Killが強制終了であるのに対して、CloseMainWindowは終了要求です。Microsoft Learn

9-2. Killでプロセスを強制終了する

プロセスを強制終了するには、Kill()を使います。

C#
using System.Diagnostics;

using var process = Process.Start("notepad.exe");

Thread.Sleep(3000);

if (process != null && !process.HasExited)
{
process.Kill();
process.WaitForExit();
}

Kill()は強制終了なので、対象アプリが保存処理や終了処理を行えない場合があります。ファイル編集中のアプリやデータベース処理中のプロセスに対して使う場合は注意が必要です。

9-3. KillとCloseMainWindowの使い分け

基本的な使い分けは次のとおりです。

CloseMainWindow()は、GUIアプリに正常終了を依頼したいときに使います。ユーザー確認や終了処理を尊重できますが、必ず終了するとは限りません。

Kill()は、プロセスを強制終了したいときに使います。確実性は高いですが、データ損失や中途半端な状態を引き起こす可能性があります。

実務では、まずCloseMainWindow()を試し、一定時間待っても終了しなければKill()を使う流れが安全です。

C#
if (!process.HasExited)
{
process.CloseMainWindow();

if (!process.WaitForExit(5000))
{
process.Kill();
process.WaitForExit();
}
}

9-4. プロセスツリーごと終了する方法

外部コマンドがさらに子プロセスを起動する場合、親プロセスだけを終了しても子プロセスが残ることがあります。その場合は、Kill(entireProcessTree: true)を使います。

C#
using System.Diagnostics;

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

Kill(entireProcessTree: true)を使うとプロセスツリーの終了を試みますが、WaitForExitHasExitedが示すのは関連付けられたプロセスの状態であり、子孫プロセス全体の状態を完全に反映するわけではない点に注意が必要です。Microsoft Learn

9-5. 終了処理後にDisposeする理由

ProcessはOSリソースを扱うクラスです。使い終わったらDispose()でリソースを解放します。C#ではusingを使うのが簡単です。

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

process!.WaitForExit();

usingを使えない場合は、try-finallyで確実に破棄します。

C#
Process? process = null;

try
{
process = Process.Start("notepad.exe");
process?.WaitForExit();
}
finally
{
process?.Dispose();
}

外部プロセスを多数起動するアプリでは、Dispose()を忘れるとハンドルなどのリソースが残り、長時間動作時の不具合につながることがあります。

10. Process使用時によくあるエラーと対処法

10-1. ファイルが見つからないエラー

Process.Startでファイルが見つからない場合、まずFileNameを確認します。

C#
var startInfo = new ProcessStartInfo
{
FileName = @"C:\Tools\MyTool.exe"
};

確認ポイントです。

  • パスが正しいか

  • 拡張子が正しいか

  • 実行ファイルが存在するか

  • 相対パスではなくフルパスにすると動くか

  • PATH環境変数に登録されているか

  • 32bit/64bitで参照先が変わっていないか

  • WorkingDirectoryFileNameを混同していないか

安全に確認するなら、起動前にFile.Existsで存在チェックします。

C#
var exePath = @"C:\Tools\MyTool.exe";

if (!File.Exists(exePath))
{
throw new FileNotFoundException("実行ファイルが見つかりません。", exePath);
}

Process.Start(exePath);

10-2. アクセス権限エラー

アクセス権限エラーが出る場合は、対象ファイルやフォルダへの権限、実行権限、管理者権限の有無を確認します。

管理者として実行したい場合は、WindowsではVerb = "runas"を使う方法があります。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Verb = "runas",
UseShellExecute = true
};

Process.Start(startInfo);

Verb = "runas"を使う場合は、UACの確認画面が表示されることがあります。また、標準出力のリダイレクトとは相性が悪いため、管理者権限が必要な処理と出力取得を同時に行う場合は設計を分けることも検討しましょう。

10-3. 標準出力が空になる原因

標準出力が空になる場合、次の原因がよくあります。

  • RedirectStandardOutput = trueを指定していない

  • UseShellExecute = falseを指定していない

  • 対象プログラムが標準出力ではなく標準エラーに出している

  • コマンドが失敗してすぐ終了している

  • 引数が間違っている

  • 作業ディレクトリが違う

  • GUIアプリなので標準出力がない

  • 文字コードが合っていない

まずは、標準出力、標準エラー、終了コードをすべて出して確認しましょう。

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

10-4. WaitForExitで処理が止まる原因

WaitForExitで処理が止まる原因は、主に次のとおりです。

  • 外部プロセスが終了していない

  • cmd /kを使っている

  • 外部プロセスが入力待ちになっている

  • 標準出力や標準エラーのバッファが詰まっている

  • 子プロセスが残っている

  • GUIアプリをユーザーが閉じるまで待っている

特に、標準出力をリダイレクトしているのに読み取らずWaitForExit()している場合は、デッドロックの可能性があります。標準出力を読むなら、ReadToEnd()ReadToEndAsync()を適切な順序で呼び出しましょう。

10-5. UseShellExecute設定ミスによるエラー

UseShellExecuteは、C# Processのトラブル原因になりやすい設定です。

標準出力を取得したい場合は、次の設定が必要です。

C#
UseShellExecute = false;
RedirectStandardOutput = true;

URLやPDFを既定アプリで開きたい場合は、次の設定が必要です。

C#
UseShellExecute = true;

環境変数を変更して起動したい場合は、次の設定が必要です。

C#
UseShellExecute = false;

.NET Frameworkでは動いたのに.NET 6では動かないという場合、UseShellExecuteの既定値差異を疑いましょう。.NET Frameworkでは既定値がtrue、.NET Coreではfalseです。Microsoft Learn

10-6. 32bit/64bit環境差によるトラブル

Windows環境では、32bit/64bitの違いによって参照されるパスが変わることがあります。

特に注意したいのは次のパスです。

  • C:\Windows\System32

  • C:\Windows\SysWOW64

  • C:\Program Files

  • C:\Program Files (x86)

AnyCPUでビルドしたC#アプリが32bitプロセスとして動作している場合、ファイルシステムリダイレクトの影響を受けることがあります。外部exeを確実に起動したい場合は、対象プラットフォームを明確にする、フルパスを確認する、実行環境で実際に存在するパスをログに出す、といった対策が有効です。

11. 実用サンプルで学ぶC# Process活用例

11-1. 外部exeを起動して終了コードを取得する

外部exeを起動し、終了コードで成功・失敗を判定するサンプルです。

C#
using System.Diagnostics;

static async Task<int> RunExeAsync(string exePath, params string[] args)
{
var startInfo = new ProcessStartInfo
{
FileName = exePath,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

foreach (var arg in args)
{
startInfo.ArgumentList.Add(arg);
}

using var process = new Process
{
StartInfo = startInfo
};

process.Start();

var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

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

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.Error.WriteLine(error);
}

return process.ExitCode;
}

呼び出し例です。

C#
int exitCode = await RunExeAsync(@"C:\Tools\MyTool.exe", "--mode", "batch");

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

11-2. pingコマンドの結果を取得する

pingコマンドを実行して結果を取得する例です。

C#
using System.Diagnostics;
using System.Text;

var startInfo = new ProcessStartInfo
{
FileName = "ping",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};

startInfo.ArgumentList.Add("127.0.0.1");

using var process = new Process
{
StartInfo = startInfo
};

process.Start();

string output = await process.StandardOutput.ReadToEndAsync();
string error = await process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.Error.WriteLine(error);
}

Windowsの日本語環境で文字化けする場合は、Encoding.GetEncoding("shift_jis")を検討します。

11-3. ffmpegなど外部CLIを実行する

ffmpegで動画を変換する例です。

C#
using System.Diagnostics;

var input = @"C:\Videos\input.mp4";
var output = @"C:\Videos\output.mp3";

var startInfo = new ProcessStartInfo
{
FileName = "ffmpeg",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

startInfo.ArgumentList.Add("-y");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(input);
startInfo.ArgumentList.Add(output);

using var process = new Process
{
StartInfo = startInfo
};

process.Start();

string stdout = await process.StandardOutput.ReadToEndAsync();
string stderr = await process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

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

ffmpegのようなCLIは、進捗やログを標準エラーに出力することがあります。そのため、標準出力だけでなく標準エラーも取得するのが重要です。

11-4. バッチ処理を自動実行する

業務バッチをC#から実行する例です。

C#
using System.Diagnostics;

var batchPath = @"C:\Batch\DailyJob.bat";
var logPath = @"C:\Logs\dailyjob.log";

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = @"C:\Batch"
};

startInfo.ArgumentList.Add("/c");
startInfo.ArgumentList.Add(batchPath);

using var process = new Process
{
StartInfo = startInfo
};

process.Start();

var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

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

await File.WriteAllTextAsync(logPath, output + Environment.NewLine + error);

if (process.ExitCode != 0)
{
Console.WriteLine("バッチ処理が失敗しました。ログを確認してください。");
}

定期実行や自動化では、ログファイル、終了コード、タイムアウトを必ず組み合わせると運用しやすくなります。

11-5. ログをリアルタイム表示する

外部コマンドのログをリアルタイムに表示する例です。

C#
using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
FileName = "ping",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

startInfo.ArgumentList.Add("127.0.0.1");

using var process = new Process
{
StartInfo = startInfo,
EnableRaisingEvents = true
};

process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
Console.WriteLine($"[OUT] {e.Data}");
}
};

process.ErrorDataReceived += (sender, e) =>
{
if (e.Data != null)
{
Console.WriteLine($"[ERR] {e.Data}");
}
};

process.Start();

process.BeginOutputReadLine();
process.BeginErrorReadLine();

await process.WaitForExitAsync();

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

長時間実行される外部CLIや進捗を表示したい処理では、このイベント方式が便利です。

12. C# Processを安全に使うための注意点

12-1. ユーザー入力をコマンド引数に渡すリスク

C# Processで最も注意すべきなのは、ユーザー入力をコマンドラインに渡す処理です。特にcmd.exe /cやPowerShellの-Commandに文字列連結で入力を渡すと、意図しないコマンドが実行される危険があります。

危険な例です。

C#
var userInput = Console.ReadLine();

var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dir " + userInput,
UseShellExecute = false
};

ユーザーがC:\Temp & del C:\Important\file.txtのような値を入力すると、別のコマンドとして解釈される可能性があります。

12-2. コマンドインジェクション対策

コマンドインジェクションを防ぐ基本は、シェルを経由しないことです。可能な限りcmd.exe /cやPowerShellの文字列コマンドではなく、実行ファイルを直接呼び出し、ArgumentListで引数を分けて渡します。

推奨例です。

C#
var folder = @"C:\Temp";

var startInfo = new ProcessStartInfo
{
FileName = "mytool.exe",
UseShellExecute = false
};

startInfo.ArgumentList.Add("--folder");
startInfo.ArgumentList.Add(folder);

Process.Start(startInfo);

さらに、ユーザー入力を使う場合は次の対策を組み合わせます。

  • 許可する文字や形式を制限する

  • パスはPath.GetFullPathで正規化する

  • 想定ディレクトリ配下か確認する

  • コマンド文字列に直接連結しない

  • ArgumentListを使う

  • 実行できるコマンドをホワイトリスト化する

  • ログに機密情報を出さない

12-3. パス指定とクォート処理の注意点

パスにスペースが含まれる場合、Argumentsを使うならクォートが必要です。

C#
Arguments = "\"C:\\Temp\\my file.txt\"";

ただし、クォートやエスケープはミスが起きやすいため、できるだけArgumentListを使いましょう。

C#
startInfo.ArgumentList.Add(@"C:\Temp\my file.txt");

FileNameには実行ファイルそのものを指定し、引数はArgumentListに分けるのが基本です。

悪い例です。

C#
FileName = "mytool.exe --input C:\\Temp\\a.txt";

良い例です。

C#
FileName = "mytool.exe";
ArgumentList.Add("--input");
ArgumentList.Add(@"C:\Temp\a.txt");

12-4. 管理者権限が必要な処理への対応

管理者権限が必要な処理をC# Processで実行する場合、WindowsではVerb = "runas"を使えます。

C#
var startInfo = new ProcessStartInfo
{
FileName = "myadmintool.exe",
UseShellExecute = true,
Verb = "runas"
};

Process.Start(startInfo);

ただし、この方法ではUAC確認が表示されることがあります。また、UseShellExecute = trueが必要になるため、標準出力のリダイレクトとは同時に使いにくくなります。

管理者権限が必要な処理は、次のように設計を分けると安全です。

  • 通常アプリと管理者権限が必要な補助ツールを分ける

  • 管理者権限が必要な処理だけを最小化する

  • 実行前にユーザーへ明確に説明する

  • 標準出力取得が必要な処理とは分離する

  • ログ出力先の権限も確認する

12-5. クロスプラットフォーム環境での違い

.NETはWindows、Linux、macOSで動作しますが、C# Processで呼び出す外部コマンドはOSによって異なります。

例です。

  • Windows: cmd.exe, powershell.exe, dir

  • Linux: /bin/bash, sh, ls

  • macOS: /bin/zsh, sh, ls

OSごとにコマンドを切り替える例です。

C#
using System.Diagnostics;
using System.Runtime.InteropServices;

var startInfo = new ProcessStartInfo
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
startInfo.FileName = "cmd.exe";
startInfo.ArgumentList.Add("/c");
startInfo.ArgumentList.Add("dir");
}
else
{
startInfo.FileName = "/bin/sh";
startInfo.ArgumentList.Add("-c");
startInfo.ArgumentList.Add("ls");
}

using var process = Process.Start(startInfo);

string output = process!.StandardOutput.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

クロスプラットフォーム対応では、パス区切り、文字コード、シェル、権限、改行コード、実行ファイルの拡張子なども考慮しましょう。

13. C# Processのベストプラクティス

13-1. usingでProcessを確実に破棄する

Processを使い終わったら、usingで確実に破棄しましょう。

C#
using var process = new Process
{
StartInfo = startInfo
};

process.Start();
await process.WaitForExitAsync();

Process.Start()の戻り値を使う場合も、nullチェックを入れると安全です。

C#
using var process = Process.Start(startInfo);

if (process == null)
{
throw new InvalidOperationException("プロセスを起動できませんでした。");
}

await process.WaitForExitAsync();

外部プロセスを大量に扱うアプリや長時間稼働するサービスでは、Dispose()漏れを防ぐことが安定性につながります。

13-2. 同期処理と非同期処理を使い分ける

コンソールアプリで単発の短い処理を実行するだけなら、同期処理でも問題ありません。

C#
using var process = Process.Start(startInfo);

string output = process!.StandardOutput.ReadToEnd();

process.WaitForExit();

一方、GUIアプリ、Webアプリ、長時間処理、大量出力を扱う処理では、非同期処理を使うのがおすすめです。

C#
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;

UIを固めたくない、スレッドを無駄にブロックしたくない、標準出力と標準エラーを安全に扱いたい場合は、非同期を選びましょう。

13-3. 標準出力・標準エラー・終了コードを必ず確認する

外部プロセスを実行するときは、標準出力だけでなく、標準エラーと終了コードも確認するのが基本です。

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

標準出力だけを見て「結果が空だから成功」と判断すると、実は標準エラーにエラー内容が出ていた、ということがあります。特に自動化処理では、次の3点を必ずログに残しましょう。

  • 実行したコマンド

  • 標準出力

  • 標準エラー

  • 終了コード

  • 実行時間

13-4. タイムアウトを設定してハングを防ぐ

外部プロセスは、入力待ち、ネットワーク待ち、ロック待ち、バグなどで終了しないことがあります。そのため、実務ではタイムアウトを設定しましょう。

C#
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
await process.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException)
{
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
}

throw;
}

タイムアウト時には、ログに次の情報を残すと調査しやすくなります。

  • 実行ファイル名

  • 引数

  • 作業ディレクトリ

  • 経過時間

  • 途中までの標準出力

  • 途中までの標準エラー

  • 強制終了したかどうか

13-5. ログ出力と例外処理を組み合わせる

C# Processを安全に運用するには、例外処理とログ出力を組み合わせます。

C#
try
{
int exitCode = await RunProcessWithRealtimeLogAsync("dotnet", "--info");

if (exitCode != 0)
{
Console.WriteLine($"コマンドが異常終了しました。ExitCode={exitCode}");
}
}
catch (Exception ex)
{
Console.WriteLine("プロセス実行中に例外が発生しました。");
Console.WriteLine(ex);
}

外部プロセスは、C#コードの外側で動作するため、ファイル不足、権限不足、PATH設定、外部ツールのバージョン差、OS差異など、多くの要因で失敗します。正常系だけでなく、失敗時に原因を追えるようにしておくことが重要です。

まとめ

C# Processを使うと、C#アプリケーションから外部アプリ、外部コマンド、CLIツール、バッチファイルなどを実行できます。単純な起動であればProcess.Start()だけで十分ですが、実務ではProcessStartInfoを使って、引数、作業ディレクトリ、標準出力、標準エラー、環境変数、ウィンドウ表示などを制御することが多くなります。

重要なポイントは、標準出力を取得するならUseShellExecute = falseRedirectStandardOutput = trueを指定すること、標準エラーと終了コードも必ず確認すること、大量出力ではデッドロックを避けるために非同期読み取りを使うこと、長時間処理ではタイムアウトを設定することです。

また、ユーザー入力をコマンドに渡す場合は、コマンドインジェクション対策が欠かせません。cmd.exe /cに文字列連結するのではなく、可能な限り外部exeを直接起動し、ArgumentListで引数を分けて渡しましょう。

C# Processは便利な反面、OSや外部プログラムに依存するため、エラー処理、ログ出力、タイムアウト、リソース解放まで含めて設計することが大切です。基本を押さえておけば、外部ツール連携や自動化処理を安全かつ柔軟に実装できます。