Mutex と NamedPipe で アプリの二重起動を禁止しながらコマンドライン引数を渡す

コンピュータ

メモ帳はなぜタブで開くのか

エクスプローラーでテキストファイルをダブルクリックすると、メモ帳が起動し、そのファイルが開かれます。
その状態のまま、エクスプローラーで別のテキストファイルをダブルクリックすると、新しいメモ帳は起動せず、既に起動しているメモ帳のタブとしてファイルが追加されます(Windows 11)。

一見すると、これは単純な「多重起動の禁止処理」のように見えます。

しかし、ここで重要なのは次の点です。

  • 新しいメモ帳プロセスは起動していない

  • それでも 別のファイルが開かれている

  • つまり、エクスプローラーから渡された ファイルパスは失われていない

この挙動から分かるのは、

多重起動は抑止されているが、
起動要求(コマンドライン引数)は既存プロセスに渡されている

ということです。

二重起動禁止は「Mutex」という技術が定番です。

ファイルパスを渡す方法は、「Named Pipe」を試してみます。

サンプルコード

ファイル名:SingleInstanceIpcSample.csproj


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:App.xaml.cs


using System.Linq;
using System.Threading;
using System.Windows;

namespace SingleInstanceIpcSample;

public partial class App : Application
{
    private static Mutex? _mutex;
    private const string MutexName = "SingleInstanceIpcSample_Mutex";

    protected override void OnStartup(StartupEventArgs e)
    {
        bool createdNew;
        _mutex = new Mutex(true, MutexName, out createdNew);

        if (createdNew)
        {
            // ワーカーとして起動
            IpcServer.Start();
            base.OnStartup(e);

            var window = new MainWindow();
            window.Show();
            if (e.Args.Length > 0)
            {
                window.ShowMessage(e.Args[0]);
            }
        }
        else
        {
            // クライアントとして起動
            string message = e.Args.FirstOrDefault() ?? "(no argument)";
            IpcClient.Send(message);

            Shutdown();
        }
    }
}

ファイル名:IpcClient.cs


using System.IO;
using System.IO.Pipes;

namespace SingleInstanceIpcSample;

public static class IpcClient
{
    private const string PipeName = "SingleInstanceIpcSample_Pipe";

    public static void Send(string message)
    {
        try
        {
            using var client = new NamedPipeClientStream(
                ".",
                PipeName,
                PipeDirection.Out);

            client.Connect(500);

            using var writer = new StreamWriter(client)
            {
                AutoFlush = true
            };
            writer.WriteLine(message);
        }
        catch
        {
            // ワーカーがいなければ何もしない
        }
    }
}

ファイル名:IpcServer.cs


using System.IO;
using System.IO.Pipes;
using System.Threading.Tasks;

namespace SingleInstanceIpcSample;

public static class IpcServer
{
    private const string PipeName = "SingleInstanceIpcSample_Pipe";

    public static event Action<string>? MessageReceived;

    public static void Start()
    {
        Task.Run(async () =>
        {
            while (true)
            {
                using var server = new NamedPipeServerStream(
                    PipeName,
                    PipeDirection.In,
                    1,
                    PipeTransmissionMode.Message,
                    PipeOptions.Asynchronous);

                await server.WaitForConnectionAsync();

                using var reader = new StreamReader(server);
                string? message = await reader.ReadLineAsync();

                if (message != null)
                {
                    MessageReceived?.Invoke(message);
                }
            }
        });
    }
}

ファイル名:MainWindow.xaml.cs


using System.Windows;

namespace SingleInstanceIpcSample;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        IpcServer.MessageReceived += ShowMessage;
    }

    public void ShowMessage(string message)
    {
        Dispatcher.Invoke(() =>
        {
            MessageText.Text = message;
        });
    }
}

ファイル名:App.xaml


<Application x:Class="SingleInstanceIpcSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
</Application>

ファイル名:MainWindow.xaml


<Window x:Class="SingleInstanceIpcSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="IPC Sample"
        Width="600" Height="200"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <TextBlock 
            x:Name="MessageText"
            FontSize="16"
            TextWrapping="Wrap"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Text="(waiting...)"/>
    </Grid>
</Window>

実行例

・初回実行 … 引数に”C:\sample1.txt”

ウィンドウが起動し、”C:\sample1.txt”と表示される。

・2回目実行 … 引数に”C:\sample2.txt”

既存のウィンドウに、”C:\sample2.txt”と表示される。

何気ない処理ですが、
2回目実行ではMutexで起動しようとしているプロセスを強制終了しています。
終了する前に、既存プロセスにNamed Pipeで引数を通知することで、機能を実現しています。

コメント