C#からC++アプリを最小化・非表示で起動し安全に終了させる方法【PLC連携にも対応】

C# で作った管理アプリから、PLC と通信する C++ アプリをそっと立ち上げて、画面には出さずに監視・終了まで面倒を見たい──そんなニーズは現場でよくあります。本記事では、.NET の Process クラスを使って C++ 実行ファイルを「最小化/非表示」で起動し、安全に終了させるための実装パターンと運用の考え方を、現場目線で詳しく解説します。

目次

C# から C++ アプリを「サービスのように」扱うイメージ

PLC やセンサーとの通信処理を C++ アプリに任せ、上位側の C# アプリを「コントローラ(管理役)」として使う構成はよくあります。 このとき C++ アプリは、ユーザーが直接触る UI ではなく、裏方で常駐する「サービスっぽい存在」として動かしたくなります。

Windows サービスに作り直さなくても、C# 側から System.Diagnostics.Process を使えば、次のようなことが実現できます。

  • C++ の EXE を最小化または非表示で起動する
  • プロセスの生存状態を監視し、落ちたら検知・再起動する
  • 安全な終了手順を踏んだ上で、必要なら強制終了する
  • 二重起動を防止する

起動パターンの整理(最小化・非表示・サービス)

まず、C# から C++ アプリを起動するパターンを整理しておきます。

パターン表示状態主な用途ポイント
GUI アプリを最小化で起動タスクバーに最小化アイコンのみたまに画面を確認したいツールWindowStyle = Minimized を利用
コンソールアプリを非表示で起動コンソールウィンドウなし裏方専用の常駐プロセスCreateNoWindow = true + 標準入出力で制御
Windows サービスとして動作UI なし24 時間運転・自動再起動が必須安定性は高いが、導入・デバッグがやや重い

本記事では、既存の C++ EXE をそのまま活かす「最小化/非表示起動」と、その終了・監視まわりにフォーカスします。

GUI アプリを最小化状態で起動する

C++ 側が MFC や Win32 などの GUI アプリケーションで、ウィンドウを持っている場合は、C# から最小化状態で起動できます。 典型的なコード例は次のようになります。


using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
    FileName = @"C:\apps\YourCppApp.exe",
    Arguments = "--plc",                 // 必要な引数があれば指定
    WorkingDirectory = @"C:\apps",
    UseShellExecute = true,              // GUI アプリなら true が扱いやすい
    WindowStyle = ProcessWindowStyle.Minimized
};

Process? proc = Process.Start(startInfo);

ここで使っている主なプロパティの意味と注意点をまとめると次の通りです。

プロパティ役割ポイント
FileName起動する EXE のフルパス拡張子 .exe まで含める
Argumentsコマンドライン引数PLC 接続先 IP など設定を渡すと便利
WorkingDirectoryカレントディレクトリ設定ファイルやログの出力先が相対パスの場合に重要
UseShellExecuteシェル経由で起動するかどうかGUI アプリでは true のほうが直感的(ショートカット起動に近い挙動)
WindowStyle起動時のウィンドウ状態Minimized を指定すると最小化で起動

注意したいのは、ターゲットの C++ アプリ側で「起動直後に自前で最大化/通常サイズに戻す」ような処理をしていると、 こちらで最小化を指定してもすぐに変更されてしまう点です。その場合、C++ 側に「/min」などの起動引数を追加して ウィンドウ状態を制御する設計にしてもらうと確実です。

コンソールアプリを非表示で起動する

PLC 通信専用の C++ アプリがコンソールアプリなら、そもそも画面は不要なことが多いでしょう。 この場合は、コンソールウィンドウを出さずに完全に非表示で起動するのがスッキリです。


using System.Diagnostics;

var startInfo = new ProcessStartInfo
{
    FileName = @"C:\apps\YourCppApp.exe",
    Arguments = "--plc",
    WorkingDirectory = @"C:\apps",
    UseShellExecute = false,             // CreateNoWindow を使うため false
    CreateNoWindow = true,               // コンソールウィンドウを出さない
    RedirectStandardOutput = true,       // 任意:標準出力をログとして取得
    RedirectStandardError = true         // 任意:エラー出力も取得
};

Process? proc = Process.Start(startInfo);

if (proc != null)
{
    proc.EnableRaisingEvents = true;
    proc.Exited += (_, __) =>
    {
        // TODO: ログに書く、アラートを上げる、自動再起動する…などの処理
    };
}

ここでは UseShellExecute = false にしている点が重要です。 CreateNoWindow は UseShellExecute = false のときにだけ有効になるため、この 2 つはセットで覚えておくと良いでしょう。

標準出力・標準エラーをログとして活用する

PLC 通信の状態やエラーを C++ 側で標準出力/標準エラーに書き出しておけば、C# 側からそれを拾ってログに保存できます。 たとえば、非同期でログを垂れ流す例は次のようになります。


if (proc != null)
{
    proc.OutputDataReceived += (_, e) =>
    {
        if (!string.IsNullOrEmpty(e.Data))
        {
            // TODO: ログファイルや監視システムに記録
            Console.WriteLine("[CPP OUT] " + e.Data);
        }
    };

    proc.ErrorDataReceived += (_, e) =>
    {
        if (!string.IsNullOrEmpty(e.Data))
        {
            // TODO: エラーログとして扱う
            Console.WriteLine("[CPP ERR] " + e.Data);
        }
    };

    proc.BeginOutputReadLine();
    proc.BeginErrorReadLine();
}

このようにしておくと、C++ 側に大きな変更を加えなくても、ログ基盤は C# 側に集約できます。

プロセスの死活監視と自動再起動の基本

PLC 通信プロセスは長時間動き続けるため、例外や外部要因で落ちる可能性を考慮しておく必要があります。 先ほどのように proc.EnableRaisingEvents = true; を設定しておけば、Exited イベントで異常終了を検知できるので、 そこで自動再起動処理を組むのが定番です。


void StartPlcAgent()
{
    // すでに動いていれば何もしない
    if (Process.GetProcessesByName("YourCppApp").Length > 0)
    {
        return;
    }

    var startInfo = new ProcessStartInfo
    {
        FileName = @"C:\apps\YourCppApp.exe",
        Arguments = "--plc",
        WorkingDirectory = @"C:\apps",
        UseShellExecute = false,
        CreateNoWindow = true
    };

    var proc = Process.Start(startInfo);
    if (proc == null) return;

    proc.EnableRaisingEvents = true;
    proc.Exited += (_, __) =>
    {
        // TODO: 再起動回数の上限チェックなどを入れる
        StartPlcAgent();
    };
}

実システムでは、無限に再起動を繰り返すと逆に危険なので、 「一定時間内の再起動回数に制限をかける」「人間への通知をはさんでから再開する」などのガードを入れておくと安心です。

安全に終了させるための考え方

PLC などの実機とつながっているプロセスを終了するとき、いきなり Kill() で強制終了するのは危険です。 「モータ停止」「バルブ閉」「トランザクション確定」などを行う猶予を C++ 側に与えたうえで終了させるのが基本です。

手順内容備考
1. C++ 側に安全停止コマンドを送るIPC や引数で「shutdown」「stop」などを通知PLC の出力を安全状態に戻す処理を C++ 内で実行
2. 正常終了を一定時間待つWaitForExit() などで待機タイムアウトを決めておく
3. まだ生きていれば「穏やかな終了」CloseMainWindow() 等でウィンドウに閉じる指示GUI アプリ向け
4. 最後の手段として強制終了Kill(entireProcessTree: true)すべての手順を踏んでも終わらない場合のみ

穏やかに終了させ、必要なら Kill するコード例

もっともシンプルな実装例は次のようになります。


void StopPlcAgent(Process? proc)
{
    if (proc is not { HasExited: false })
    {
        return; // すでに終了している
    }

    // ここで本来は IPC などで「安全停止コマンド」を送り、
    // そのうえで終了を待つのが理想。
    // SendShutdownCommandToCpp();

    // ① UI を持つアプリ向け:メインウィンドウに「閉じる」を要求
    if (proc.CloseMainWindow())
    {
        if (!proc.WaitForExit(5000)) // 5 秒程度待つ(要件次第で調整)
        {
            // ② まだ終了しなければ強制終了
            proc.Kill(entireProcessTree: true);
        }
    }
    else
    {
        // メインウィンドウを持たない(コンソール、サービスライク)場合
        proc.Kill(entireProcessTree: true);
    }
}

PLC 連携では、「C++ 側で安全停止 → 正常終了」 という流れを前提に設計し、 Kill() はあくまで最終手段として扱うのがポイントです。

既に動いている同名プロセスを探して終了する

C# アプリが起動したときに、すでに C++ の常駐プロセスが動いているか確認したいケースもよくあります。 このときは Process.GetProcessesByName を使いますが、引数の指定に注意が必要です。


foreach (var p in Process.GetProcessesByName("YourCppApp")) // .exe を付けない
{
    try
    {
        if (!p.CloseMainWindow() || !p.WaitForExit(3000))
        {
            p.Kill(entireProcessTree: true);
        }
    }
    catch (Exception ex)
    {
        // TODO: 例外内容をログに記録(アクセス権不足、すでに終了している等)
        Console.WriteLine(ex);
    }
}

よくあるミスと注意点をまとめると次の通りです。

ポイント内容
拡張子は不要GetProcessesByName("YourCppApp") のように .exe を含めない
権限エラー他ユーザーのプロセスは Kill できないことがある(管理者権限が必要)
二重起動防止起動前に Length == 0 を確認してから新規起動すると安全

二重起動を防ぐシンプルなチェック

C++ アプリを多重起動すると PLC 通信が二重に走り、想定外の制御信号が出るリスクがあります。 最低限、起動前の存在チェックだけでも入れておきましょう。


if (Process.GetProcessesByName("YourCppApp").Length == 0)
{
    // ここで起動
}
else
{
    // TODO: ログに「すでに起動済み」と書く、ユーザーに通知する等
}

PLC 連携ならではの設計ポイント

PLC や実機と連携する場合、通常のデスクトップツールとは違う視点で設計しておくとトラブルを防げます。

  • 安全停止のプロトコルを決めておく
    C++ 側に「すべての出力を安全側へ」「PLC 通信を切断」「ログを残して終了」の流れを実装し、 C# からは shutdown コマンドを送るだけでよい形にしておく。
  • タイムアウトとリトライのポリシー
    PLC 側の応答が遅い場合でもいつまでも待ち続けないよう、タイムアウト時間とリトライ回数を仕様として決める。
  • ログの一元管理
    C++ 側の標準出力/標準エラーを C# アプリが吸い上げ、テキストファイルや監視システムにまとめて保存する。
  • フェイルセーフ設計
    C++ プロセスが異常終了した場合に PLC の出力はどうなるか、ハード側の仕様も含めて確認し、 最悪ケースでも人や設備に危険が及ばないようにしておく。

Windows サービスとして動かす選択肢

「常に起動していてほしい」「OS 起動と同時に立ち上がってほしい」「ユーザーがログインしていなくても動いていてほしい」 といった要件がある場合は、C++/C# いずれか、もしくは両方を Windows サービス化する選択肢も検討に値します。

  • サービスにしておくと、サービスマネージャーから状態確認・再起動がしやすい
  • リモートから運用担当が扱うのに向いている
  • 一方で、デバッグや GUI 表示が難しくなるデメリットもある

既存 EXE をそのままサービス化するユーティリティ(sc.exe や NSSM など)を使う方法もありますが、 最終的には「開発・運用コスト」と「安全性・安定性」のバランスを見て選ぶのが現実的です。

C# のラッパークラスでプロセス管理をカプセル化する

C# のあちこちの画面から C++ アプリを触るようになると、ProcessStartInfo や Kill() の呼び出しが散らばって保守が面倒になります。 そこで、C++ アプリを専用オブジェクトとしてラップしてしまうのがおすすめです。


public sealed class PlcAgentProcessManager : IDisposable
{
    private Process? _process;
    private readonly string _exePath;
    private readonly string _workingDir;
    private bool _disposed;

    public bool IsRunning => _process is { HasExited: false };

    public PlcAgentProcessManager(string exePath, string workingDir)
    {
        _exePath = exePath;
        _workingDir = workingDir;
    }

    public void StartIfNeeded()
    {
        if (IsRunning) return;

        var startInfo = new ProcessStartInfo
        {
            FileName = _exePath,
            WorkingDirectory = _workingDir,
            Arguments = "--plc",
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true
        };

        _process = Process.Start(startInfo);
        if (_process == null) return;

        _process.EnableRaisingEvents = true;
        _process.Exited += (_, __) =>
        {
            // TODO: ログ/再起動戦略など
        };
    }

    public void Stop()
    {
        if (!IsRunning) return;

        // TODO: IPC 経由で安全停止コマンドを送る
        // SendShutdownCommand();

        if (_process!.CloseMainWindow())
        {
            if (!_process.WaitForExit(5000))
            {
                _process.Kill(entireProcessTree: true);
            }
        }
        else
        {
            _process.Kill(entireProcessTree: true);
        }
    }

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;

        try
        {
            Stop();
        }
        catch
        {
            // ここでは握りつぶしてもよい(終了時のベストエフォート)
        }
    }
}

アプリケーション側からは、このクラスを DI コンテナなどで共有し、 「起動したい側は StartIfNeeded() を呼ぶ」「アプリ終了時に Dispose() で止める」 というシンプルな API だけ意識すればよくなります。

まとめ

C# の Process API を使えば、既存の C++ アプリを「サービスのように」最小化/非表示で起動し、 死活監視や安全な終了まで一通りコントロールすることができます。

  • GUI アプリは WindowStyle = Minimized で最小化起動
  • コンソールアプリは UseShellExecute = false + CreateNoWindow = true で非表示起動
  • 終了は「安全停止コマンド → 正常終了待ち → CloseMainWindow → Kill」の順で段階的に行う
  • 二重起動防止や死活監視、自動再起動も Process とイベントで実装できる

PLC など実機とつながるシステムでは、特に「どう止めるか」 の設計が重要です。 C++ 側と C# 側の役割を整理し、プロセス管理をラッパークラスに抽象化することで、 安全で保守しやすい構成にしていきましょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次