【WPF】アプリケーションのメインプロセスが終了しないケースを深堀する記事


***いいね、ストック、Badボタン、クレーム、編集リクエストで言いがかりをつけるなどが励みになります。***


自前のアプリ制作でなぜかメインプロセスが残る問題(いわゆるゾンビプロセス　Zombie Process)が解決できずに難儀していたので、掘り下げてみることにした。

ファイルの読み書き等処理等は自動でGCされるわけではないので明示的なDisposeかUsingディレクティブで囲む必要がある。

***これをしないとアプリケーションを```Close```してもメモリ上にメインプロセスの.exeが残り続ける場合がある***(つまり実際にはアプリケーションが終了しない)

→この場合、```Application.ShutDown()```など他の終了方法も通用しない


今回はこのことを実証しつつ、幾つか```Close```してもプロセスが終了しないケースを挙げてみます。

# 参考URL

ゾンビプロセスを回避する方法（対象はC#のアプリケーション）

https://qiita.com/yusuke-sasaki/items/6693795670c914accb0d


# git
Visualstudio 2022 .net9 
それほど難しいコードは使用していないため、比較的古いTargetFrameWotkでも動作します（たぶん）。

```e-mail
git close https://github.com/Sheephuman/ZombiProcessTest.git
```


https://qiita.com/yusuke-sasaki/items/6693795670c914accb0d

まずは比較的単純なケースから再現してみることにする。

# TimerクラスがNewされ、適切にDisposeされなかった場合




```Cs

  private System.Timers.Timer _timer = null!;
  private Thread _backgroundThread = null!;
  private bool _isRunning = true;
  
   private void StartBackgroundWork()
   {
       // バックグラウンドスレッドを開始
       _backgroundThread = new Thread(() =>
       {
           while (_isRunning)
           {
               Console.WriteLine("Background thread running...");
               Thread.Sleep(1000);
           }
       });
       _backgroundThread.IsBackground = false; // 意図的にフォアグラウンドスレッドにする
       _backgroundThread.Start();

       // タイマーを開始
       _timer = new System.Timers.Timer(500);
       _timer.Elapsed += TimerElapsed;
       _timer.AutoReset = true;
       _timer.Start();
   }

   private void TimerElapsed(object sender, ElapsedEventArgs e)
   {
       // タイマーイベントで何か処理（例: ログ出力）
       Console.WriteLine("Timer ticked...");
   }

```

Test実行ボタン
```cs
 private async void testButton_Click(object sender, RoutedEventArgs e)
 {
      private void testButton_Click(object sender, RoutedEventArgs e)
  {
      try
      {
          StartBackgroundWork();
      }
      catch (Exception ex)
      {
          MessageBox.Show(ex.Message);
      }
  }



```

終了時の処理をコメントアウト
```cs
  private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
  {
      // 意図的にリソース解放をしない
      // _isRunning = false;
      // _timer?.Stop();
      // _timer?.Dispose();
      //_backgroundThread?.Join();
  }
```

# 実行結果
明示的な開放をしないので、アプリケーションが終了後もメインプロセスがゾンビとして残る

<iframe width="560" height="315" src="https://www.youtube.com/embed/XyoibXFKfAo?si=KsI4j4uFVOnKvicu" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


# ffmpegのような外部プロセスを呼び出すケースにおいて、適切な終了処理あるいは実装を行わなかった場合


$\color{Gray}{\tiny \textsf{※再現にやたらと苦労させられた}}$




・***非同期メソッド内で同期的な処理がある場合***
　→　このケースのみ、MainWindowがCloseされてもアプリケーションのメインプロセスが閉じない
・```ShutdownMode.OnMainWindowClose```以外が指定されていた場合
→　```OnExplicitShutdown```は```Application.Current.Shutdown();```が明示的に呼ばれないと終了しないモード
　→このモードでさえ、非同期処理内で同期処理があったり、キャンセルトークンが通知されていないと利かないケースがあるのが判明しています（多分に推測交じり）


・WaitForExitAsync()やキャンセルトークンなどを実行しなかった場合
　



参考

https://qiita.com/skitoy4321/items/10c47eea93e5c6145d48

# 実装

非同期で実装した場合は高確率で起こる（UIスレッドがブロックされるので基本的にしない。というか出力が表示されない）


```cs:コンストラクタ・フィールド変数
 Thread th1 = null!;
Process process = null!; // Processオブジェクトをフィールドとして定義

  public MainWindow()
  {
      InitializeComponent();


           // Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
            // MainWindowを閉じたときにアプリケーション全体を終了するように設定 ゾンビプロセス化しないために必要
            //
  }

```

```cs:testButton
 private void testButton_Click(object sender, RoutedEventArgs e)
 {
     th1 = new Thread(() => RunFfmpegAsync());
     th1.IsBackground = true;
     //フォアグラウンドで動作。ゾンビプロセスの主な原因

     
     th1.Start();
 }

```

### RunFfmpeg(同期バージョン：非実用的)
メインプロセスが終了しなくなる。

```cs:RunFfmpegAsync(非同期バージョン：非実用的)
  
        private void RunFfmpegAsync()
        {

            var startInfo = new ProcessStartInfo
            {
                FileName = "ffmpeg.exe",
                Arguments = "-i test.mp4 -y output.mp4",
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardError = true,
                CreateNoWindow = true
            };

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

                // 標準出力とエラー出力のコールバックを設定



                process.ErrorDataReceived += async (s, ev) =>
                {
                    if (ev.Data != null)
                    {

                        // バッファに追加（スレッドセーフでない）
                        await Dispatcher.InvokeAsync(() =>
                           {

                               OutputTextBox.AppendText(ev.Data + Environment.NewLine);
                               OutputTextBox.ScrollToEnd(); // テキストボックスをスクロールして最新の出力を表示
                           });

                        await Task.Delay(100); // 適切な遅延を入れることでUIの更新をスムーズにする
                    }
                };

                // Exitedイベント（問題を引き起こす可能性のある実装）
                process.Exited += async (s, ev) =>
                {
                    // WaitForExitを呼ばない（バッファが残る可能性）

                    await Dispatcher.InvokeAsync(() => MessageBox.Show("ffmpeg process exited."));
                };

                // プロセス開始
                process.Start();

                process.BeginErrorReadLine();



                // 終了を待つ（キャンセルトークンを使用）
                try
                {


                    //process.WaitForExitAsync();
                }
                catch
                {
                    // 例外を無視（問題を悪化させる）
                }
            }
            // usingブロックを抜けた後、プロセスが完全に解放されない可能性
        }


```

### RunFfmpegAsync(非同期バージョン)
実用的。
```ShutdownMode.OnExplicitShutdown```などの条件次第でゾンビプロセス化する。
***非同期メソッド内で同期的な処理があるとゾンビプロセス化する***（コメント参照）

```cs:RunFfmpegAsync(非同期バージョン)
  public MainWindow()
  {
      InitializeComponent();

      //  Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
      // MainWindowを閉じたときにアプリケーション全体を終了するように設定 
      //defalutなので指定は不要
      
      
      Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
      //ShutDownメソッドを呼ばないとゾンビプロセス化する
      }



    private async Task RunFfmpegAsync()
    {

        var startInfo = new ProcessStartInfo
        {
            FileName = "ffmpeg.exe",
            Arguments = "-i test.mp4 -y output.mp4",
            UseShellExecute = false,
            RedirectStandardInput = true,
            RedirectStandardError = true,
            CreateNoWindow = true
        };

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

            // 標準出力とエラー出力のコールバックを設定


         ///実験結果　ErrorDataReceived内でasync・awaitを不使用にするとゾンビプロセス化する
         ///警告も特に発生しない
             process.ErrorDataReceived += (s, ev) =>
  {
      if (ev.Data != null)
      {
         ////本来はasync・awaitを入れる
          Dispatcher.InvokeAsync(() =>
         {

             OutputTextBox.AppendText(ev.Data + Environment.NewLine);
             OutputTextBox.ScrollToEnd(); // テキストボックスをスクロールして最新の出力を表示
         });

          Task.Delay(100); // 適切な遅延を入れることでUIの更新をスムーズにする　
        //本来はawaitを入れる
      }
  };
                    await Task.Delay(100); // 適切な遅延を入れることでUIの更新をスムーズにする
                }
            };

            // Exitedイベント（問題を引き起こす可能性のある実装）
            process.Exited += async (s, ev) =>
            {
                // WaitForExitを呼ばない（バッファが残る可能性）

                await Dispatcher.InvokeAsync(() => MessageBox.Show("ffmpeg process exited."));
            };

            // プロセス開始
            process.Start();

            process.BeginErrorReadLine();



            // 終了を待つ（キャンセルトークンを使用）
            try
            {


                await process.WaitForExitAsync();
            }
            catch
            {
                // 例外を無視（問題を悪化させる）
            }
        }
    }


```

### StopButton
qキーの送信
恐らく破棄(Dispose)した方が良い
```cs:StopButton
  private void StopButton_Click(object sender, RoutedEventArgs e)
  {
      try
      {
          StreamWriter inputWriter = process.StandardInput;


          inputWriter.WriteLine("q");
      }

      catch (Exception ex)
      {
          MessageBox.Show($"Error stopping ffmpeg: {ex.Message}");
      }
      //inputWriter.Dispose();
      //解放させない
  }
```

# 実行結果


https://x.com/sheephuman/status/1934309981685416001


# あとがき
Uploadeめんどい。
いいねがついた時点で更新。