🚫

C#定石 - 多重起動抑止

2025/01/07に公開

はじめに

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

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

参考情報

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

多重起動抑止

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

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

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

Mutex 名称

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

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

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

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

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

多重起動抑止の対象範囲

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

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

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

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

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

using System.Security.AccessControl;
using System.Security.Principal;
// アクセス権付与情報
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 を解放するまで、所有権が維持されます。

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

AbandonedMutexException は、対象名称の Mutex が、正しく開放されずに破棄状態となっていたことを検知するためのもので、AbandonedMutexException となっても所有権取得はできています。

多重起動された場合

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

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

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

using System.Diagnostics;
using System.Runtime.InteropServices;
// 起動中プロセスのアクティブ化
private static void ActivateRunningProcess()
{
  string progname = Process.GetCurrentProcess().ProcessName;
  Process[] processes = Process.GetProcessesByName(progname);
  if (processes != null)
  {
    foreach(Process proc in processes)
    {
      // プロセスのメインウィンドウハンドルを取得
      IntPtr 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

Program.cs
internal static class Program
{
  [STAThread]
  static void Main()
  {
    // 多重起動抑止 - 前処理
    var assembly = Assembly.GetExecutingAssembly();
    var guidAttr = assembly.GetCustomAttribute<GuidAttribute>();
    string 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

Program.cs
internal static class Program
{
  [STAThread]
  static void Main()
  {
    // 多重起動抑止 - 前処理
    var assembly = Assembly.GetExecutingAssembly();
    var guidAttr = assembly.GetCustomAttribute<GuidAttribute>();
    string 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定石 - スタートアップルーチン 記載内容を実施済みのコードをベースとします。

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>();
    string 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許容参照型に変更するだけです。

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

出典

本記事は、2025/01/06 Qiita 投稿記事の転載です。

C#定石 - 多重起動抑止

Discussion