C#定石 - 多重起動抑止

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

### 変更履歴
- 2025/01/14　GUID参照を三項演算子から、Qiitaコメントで指摘頂いたシンプルな記法に変更


### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

RDP評価は、Windows Server 2025 評価版を利用しました。
Windows Server では「管理用リモートデスクトップ」として同時２セッションまでのリモートデスクトップ接続が許可されています。
上記は、RDS (Remote Desktop Services) を構成しなくても利用できます。

### 参考情報

下記情報を参考にさせて頂きました。

- [［C#］AssemblyInfo アセンブリ情報やバージョンを取得する（.NET Core比較版）](https://note.dokeep.jp/post/csharp-assembly-info-get-core/)


## 多重起動抑止

共用リソースの同時アクセスで、競合やデータ破損を避けることなどを目的として、多重起動抑止を組み込むことが良くありました。
多重起動抑止といえば、Mutex - WaitOne ですね。

https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.mutex


今回は、まず、サンプルコード作成のポイントを記載します。

### Mutex 名称

多重起動抑止のキーが、Mutex 名称です。
Mutex 名称は、他会社、他製品と重複しない、対象モジュールとしてのユニークな名称を付与する必要があります。

単純ですが、プロセス名、製品名、会社名などを連結すればユニークな名称となりますね。
AssemblyInfo をきちんと設定しているという前提で、汎用的なコードとすると下記のようになります。

```CSharp
var assembly = Assembly.GetExecutingAssembly();
var mutexName = string.Format("{0}-{1}-{2}",
                  Process.GetCurrentProcess().ProcessName,
                  assembly.GetCustomAttribute<AssemblyProductAttribute>().Product,
                  assembly.GetCustomAttribute<AssemblyCompanyAttribute>().Company);
```

:::note
AssemblyInfo 設定情報に、Mutex 名称に相応しくない文字を含む可能性がある場合は、下記のように除外、もしくは、置換をしてください。

```CSharp
var info = assembly.GetCustomAttribute<AssemblyProductAttribute>().Product;
info = info.Replace(@"\", "");
```
:::

AssemblyInfo には GUID もあるので、プロセス名、GUID をキーとして利用という手もあります。
こちらであれば、Mutex 名称に相応しくない文字が存在するかを気にすることは不要です。

```CSharp
var assembly = Assembly.GetExecutingAssembly();
var guidAttr = assembly.GetCustomAttribute<GuidAttribute>();
var mutexName = string.Format("{0}-{1}",
                  Process.GetCurrentProcess().ProcessName,
                  guidAttr?.Value ?? "00000000-1111-2222-3333-444444444444");
```

:::note
.NET では、AssemblyInfo はプロジェクト設定に組み込まれていますが、GUID はプロジェクト設定に存在していません。
GUID の設定は、C# ソースとして下記のように記載する必要があります。

```CSharp:AssemblyInfo.cs
using System.Reflection;

// プロジェクト設定に組み込まれてない項目
// AssemblyInfo.cs 以外のソースでも可
[assembly: Guid("00000000-1111-2222-3333-444444444444")]
```

上記記述を実施していない場合、前述ソースで guidAttr が null となってしまうため、null条件演算子、null合体演算子で、null 時のデフォルト値を仮設定しています。
null条件演算子、null合体演算子で記載した部分は、下記、三項演算子と等価です。
```CSharp
guidAttr != null ? guidAttr.Value : "00000000-1111-2222-3333-444444444444"
```
:::


### 多重起動抑止の対象範囲

多重起動抑止の対象範囲は、下記２パターンがあります。

- ローカル
  - サインインしたユーザー内、マルチプロセスでの多重起動抑止です。
- グローバル
  - １台の PC 内で、同時利用ユーザーも対象とする、マルチユーザー/マルチプロセスでの多重起動抑止です。
  - RDPで複数ユーザーが同時利用する環境で、インストーラ（もしくは、インストーラのランチャ）、パッケージに対する設定アプリなどで、この形態にすることがありました。

後述、サンプルコードは、ローカルを対象としたコードを記載します。
グローバルを対象とする場合には、Mutex 名称の先頭に `Global\` を付与します。

```CSharp
var assembly = Assembly.GetExecutingAssembly();
var guidAttr = assembly.GetCustomAttribute<GuidAttribute>();
var mutexName = string.Format("{0}-{1}",
                  Process.GetCurrentProcess().ProcessName,
                  guidAttr?.Value ?? "00000000-1111-2222-3333-444444444444");
mutexName = @"Global\" + mutexName;
```

それに加えて、他ユーザーでもアクセス可能とするために、アクセス権付与も行います。
アクセス権付与は .NET Framework と .NET で手法が異なります。
後述コードは双方記載してあるので、どちらかをコメントアウトしてください。




```CSharp
using System.Security.AccessControl;
using System.Security.Principal;
```

```CSharp
// アクセス権付与情報
var mutexSecurity = new MutexSecurity();
mutexSecurity.AddAccessRule(
  new MutexAccessRule(
    new SecurityIdentifier(WellKnownSidType.WorldSid, null),
    MutexRights.FullControl,
    AccessControlType.Allow
  )
);

// .NET Framework - Mutex 生成時にアクセス権付与情報も指定
bool created;
var hMutex = new Mutex(false, mutexName, out created, mutexSecurity);

// .NET - SetAccessControl でアクセス権付与
// var hMutex = new Mutex(false, mutexName);
// ThreadingAclExtensions.SetAccessControl(hMutex, mutexSecurity);
```

### 単一所有権取得

https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.waithandle.waitone

生成した Mutex に対して WaitOne で単一所有権取得を行います。
多重起動でなければ（戻り値が true）、所有権取得状態となり、Mutex を解放するまで、所有権が維持されます。


```CSharp
bool bOwnerShip = false;
try
{
  bOwnerShip = hMutex.WaitOne(0);
}
catch(AbandonedMutexException)
{
  // 正しく開放されずに破棄されていたという通知なので無視
  bOwnerShip = true;
}
catch { /* NOP */ }
```

[AbandonedMutexException](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.abandonedmutexexception) は、対象名称の Mutex が、正しく開放されずに破棄状態となっていたことを検知するためのもので、AbandonedMutexException となっても所有権取得はできています。



### 多重起動された場合

多重起動された場合の処理として、下記などが考えられます。

- 多重起動である旨をダイアログ表示する。
- 何もせずに終了する。
- 起動済みモジュールをアクティブ化する。（対象がローカルの場合限定）

:::note
二昔前のショボイPCで、待ち切れずに連打してしまい、ダイアログだらけというトラウマがあるので、対象がローカルの場合に、ダイアログ表示という選択は避けていた気がします。
対象がグローバルの場合は、何も表示しないとわからないので、ダイアログ表示が基本ですね。
:::

起動済みモジュールのアクティブ化は、下記のような記述で可能です。

```CSharp
using System.Diagnostics;
using System.Runtime.InteropServices;
```

```CSharp
// 起動中プロセスのアクティブ化
private static void ActivateRunningProcess()
{
  var progname = Process.GetCurrentProcess().ProcessName;
  var processes = Process.GetProcessesByName(progname);
  if (processes != null)
  {
    foreach(var proc in processes)
    {
      // プロセスのメインウィンドウハンドルを取得
      var hWnd = proc.MainWindowHandle;
      if (hWnd != IntPtr.Zero)
      { 
        // ウィンドウを元のサイズに戻す（最小化解除）
        ShowWindow(hWnd, SW_RESTORE); 
        // ウィンドウをアクティブ化
        SetForegroundWindow(hWnd);
      }
    }
  }
}

[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);

[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

private const int SW_RESTORE = 9; // ウィンドウを元のサイズに戻す定数
```



## サンプルコード

スタートアップルーチンに多重起動抑止を組み込みます。
サンプルコードは、多重起動抑止の対象範囲をローカルとしたものです。
対象範囲をグローバルとする場合は、「多重起動抑止の対象範囲」に記載されている「Mutex名称」「アクセス権付与」の対処が必要です。



Windows Forms / WPF、.NET Framework / .NET ともに基本的に同一記述ですが、記述箇所とその周辺などに差異があるので、それぞれについて以降で記載します。

### Windows Forms - .NET Framework 4.8

```CSharp:Program.cs
internal static class Program
{
  [STAThread]
  static void Main()
  {
    // 多重起動抑止 - 前処理
    var assembly = Assembly.GetExecutingAssembly();
    var guidAttr = assembly.GetCustomAttribute<GuidAttribute>();
    var mutexName = string.Format("{0}-{1}",
                      Process.GetCurrentProcess().ProcessName,
                      guidAttr?.Value ?? "00000000-1111-2222-3333-444444444444");
    bool bOwnerShip = false;
    var hMutex = new Mutex(bOwnerShip, mutexName);
    try
    {
      bOwnerShip = hMutex.WaitOne(0);
    }
    catch (AbandonedMutexException)
    {
      // 正しく開放されずに破棄されていたという通知なので無視
      bOwnerShip = true;
    }
    catch { /* NOP */ }

    // 多重起動抑止 - 判定
    if (bOwnerShip)
    {
      // メインフォーム
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      Application.Run(new Form1());
    }
    else
    {
      // TODO
      //   多重起動である旨をダイアログ表示
      //   何もせずに終了
      //   起動済みモジュールをアクティブ化
    }

    // 多重起動抑止 - 後処理
    if (hMutex != null)
    {
      if (bOwnerShip)
      {
        // 所有権解放
        hMutex.ReleaseMutex();
      }
      hMutex.Close();
    }
}
```

### Windows Forms - .NET 8

```CSharp:Program.cs
internal static class Program
{
  [STAThread]
  static void Main()
  {
    // 多重起動抑止 - 前処理
    var assembly = Assembly.GetExecutingAssembly();
    var guidAttr = assembly.GetCustomAttribute<GuidAttribute>();
    var mutexName = string.Format("{0}-{1}",
                      Process.GetCurrentProcess().ProcessName,
                      guidAttr?.Value ?? "00000000-1111-2222-3333-444444444444");
    bool bOwnerShip = false;
    var hMutex = new Mutex(bOwnerShip, mutexName);
    try
    {
      bOwnerShip = hMutex.WaitOne(0);
    }
    catch (AbandonedMutexException)
    {
      // 正しく開放されずに破棄されていたという通知なので無視
      bOwnerShip = true;
    }
    catch { /* NOP */ }

    // 多重起動抑止 - 判定
    if (bOwnerShip)
    {
      // メインフォーム
      ApplicationConfiguration.Initialize();
      Application.Run(new Form1());
    }
    else
    {
      // TODO
      //   多重起動である旨をダイアログ表示
      //   何もせずに終了
      //   起動済みモジュールをアクティブ化
    }

    // 多重起動抑止 - 後処理
    if (hMutex != null)
    {
      if (bOwnerShip)
      {
        // 所有権解放
        hMutex.ReleaseMutex();
      }
      hMutex.Close();
    }
  }
}
```

### WPF - .NET Framework 4.8

[WPF定石 - スタートアップルーチン](https://qiita.com/chai0917/items/1d1ca54ca82ab2145094) 記載内容を実施済みのコードをベースとします。

```CSharp:App.xaml.cs
public partial class App : Application
{
  // 内部変数
  private bool bOwnerShip = false;
  private Mutex hMutex = null;

  private void App_Startup(object sender, StartupEventArgs e)
  {
    // 多重起動抑止 - 前処理
    var assembly = Assembly.GetExecutingAssembly();
    var guidAttr = assembly.GetCustomAttribute<GuidAttribute>();
    var mutexName = string.Format("{0}-{1}",
                      Process.GetCurrentProcess().ProcessName,
                      guidAttr?.Value ?? "00000000-1111-2222-3333-444444444444");
    hMutex = new Mutex(bOwnerShip, mutexName);
    try
    {
      bOwnerShip = hMutex.WaitOne(0);
    }
    catch (AbandonedMutexException)
    {
      // 正しく開放されずに破棄されていたという通知なので無視
      bOwnerShip = true;
    }
    catch { /* NOP */ }

    // 多重起動抑止 - 判定
    if (bOwnerShip)
    {
      // メインウィンドウ
      var mainWindow = new MainWindow();
      mainWindow.Show();
    }
    else
    {
      // TODO
      //   多重起動である旨をダイアログ表示
      //   何もせずに終了
      //   起動済みモジュールをアクティブ化
    }
  }

  private void App_Exit(object sender, ExitEventArgs e)
  {
    // 多重起動抑止 - 後処理
    if (hMutex != null)
    {
      if (bOwnerShip)
      {
        // 所有権解放
        hMutex.ReleaseMutex();
      }
      hMutex.Close();
    }
  }
}
```

### WPF - .NET 8

WPF - .NET Framework 4.8 とほぼ同一コードです。
冒頭の内部変数 hMutex を下記のように null許容参照型に変更するだけです。


```CSharp:App.xaml.cs
public partial class App : Application
{
  // 内部変数
  private bool bOwnerShip = false;
  private Mutex? hMutex = null;      // ここを null許容参照型とする
```