5.6 多重起動の禁止
5.6.1 プロセスの調査
Windows アプリケーションは、ひとつの実行単位がプロセスとして管理されています。Windows は複数のプロセスを同時に起動させ、同時に実行することができます。Office を起動しながら、Internet Explorer でインターネットのハイパーテキストを参照し、バックグラウンドで音楽を再生するということができるのは、複数のプロセスを同時に実行することができるためです。もちろん、すべてのプロセスの実行には物理的なメモリが必要になりますし、処理をしているときは CPU を占有することになります。
特別な仕掛けを施さなければ、同じ exe ファイルからアプリケーションをいくつも起動することができます。小さなツールアプリケーションの場合は、むしろ多重起動できた方が便利な場合が多いでしょう。しかし、コンピュータに高い負荷をかけるゲームの場合、多重起動を禁止したい場合があります。特に、ゲームはビジネスツールとは性質が異なるため、多重起動されることが逆に不都合につながります。同じ実行ファイルが複数起動されているという状態は、例えばディスク上のファイルに自動保存するデータで整合性が失われる危険性もあります。
こうした問題を避けるために、多重起動することに大きなメリットを持たない複雑な構造のアプリケーションでは多重起動を禁止するべきです。多重起動を禁止する方法はいくつかありますが、システムで実行されているプロセスを調べる方法がもっとも単純でしょう。
プロセスの情報は System.Diagnostics.Process クラスから取得することができます。
System.Object System.MarshalByRefObject System.ComponentModel.Component System.Diagnostics.Process
public class Process : Component
まず、多重起動禁止の仕組みを構築する前に、Process クラスの基本的な仕組みについて解説します。新しいプロセスを構築する場合はコンストラクタから新しい Process インスタンスを生成しますが、現在実行中のプロセスを得るには GetCurrentProcess() メソッドを呼び出します。
public static Process GetCurrentProcess()
このメソッドは static なので、直接呼び出すことができます。このメソッドは、メソッドを呼び出した実行中のアプリケーションに関連付けられた Process オブジェクトを返します。
Process オブジェクトを取得することができれば、実行中のアプリケーションに関する情報を取得することができます。プロセスの名前は ProcessName プロパティから取得できます。
public string ProcessName { get; }
このプロパティは、プロセスに関連している実行ファイルの拡張子 exe とパスを除いたファイル名となります。システムで常に一意となることが保障される名前ではありません。同じ名前の exe ファイルがあった場合、異なるアプリケーションでも同じプロセス名を持つことがあるので注意してください。
プロセスが GUI 対応のアプリケーションであれば、メインウィンドウのテキストを MainWindowTitle プロパティから取得することができます。
public string MainWindowTitle { get; }
プロセスにメインウィンドウがない場合、このプロパティの結果は空の文字列 "" となります。
using System.Drawing; using System.Diagnostics; using System.Windows.Forms; public class Test : Form { private Process process; protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (process == null) process = Process.GetCurrentProcess(); Font font = new Font(Font.Name, 20); Brush brush = new SolidBrush(Color.Black); e.Graphics.DrawString( "Name=" + process.ProcessName, font, brush, 0, 0 ); e.Graphics.DrawString( "Title=" + process.MainWindowTitle, font, brush, 0, font.Height ); } static void Main() { Form form = new Test(); form.Text = "Kitty on your lap"; Application.Run(form); } }
コード1は、フォームのクライアント領域に自分自身のプロセスの名前とメインウィンドウのタイトルを表示するプログラムです。Process オブジェクトを Process.GetCurrentProcess() で取得して、名前を ProcessName プロパティから、ウィンドウタイトルを MainWindowTitle から取得しています。MainWindowTitle の文字列はフォームに設定している Text プロパティと同じであることが分かります。
ローカルシステムで起動しているすべてのプロセスの情報を調べることができれば、名前を比較してすでに自分自身のアプリケーションが起動しているかどうかを調べられそうです。システムで起動しているプロセスすべてを得るには GetProcesses() メソッドを使います。
public static Process[] GetProcesses()
GetProcesses() メソッドは、ローカルコンピュータ上のすべてのプロセスに対応した Process オブジェクトの配列を返します。システムで実行されているすべてのプロセスを取得できれば、今 Windows がどのようなアプリケーションを実行しているのかを調べることができます。
しかし、多重起動を防止することが目的であれば、単純に自分自身と同じ名前のプロセスが存在するかどうかを調べればよいだけです。この場合 GetProcessesByName() メソッドを使った方が効率的です。
public static Process[] GetProcessesByName(string processName)
processName パラメータにはプロセスの名前を指定します。このメソッドは指定したプロセス名と同じローカルコンピュータ上のすべてのプロセスの配列を返します。検索したいプロセスの名前が既知である場合はこのメソッドを使うべきです。
using System.Drawing; using System.Diagnostics; using System.Windows.Forms; public class Test : Form { private Process[] processes; protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (processes == null) processes = Process.GetProcesses(); Brush brush = new SolidBrush(Color.Black); for(int i = 0 ; i < processes.Length ; i++) e.Graphics.DrawString( processes[i].ProcessName, Font, brush, 0, Font.Height * i ); } private const string TITLE = "ぴぴるぴるぴるぴぴるぴ~"; static void Main() { string name = Process.GetCurrentProcess().ProcessName; Process[] processes = Process.GetProcessesByName(name); for(int i = 0 ; i < processes.Length ; i++) { Process proc = processes[i]; if (proc.ProcessName == name && proc.MainWindowTitle == TITLE) { MessageBox.Show( null, "このアプリケーションはすでに起動しています", TITLE, MessageBoxButtons.OK, MessageBoxIcon.Error ); return; } } Form form = new Test(); form.Text = TITLE; Application.Run(form); } }
コード2は、システムが現在実行しているプロセスを列挙するプログラムですが、起動時に多重起動をチェックし、すでに自分自身と同じアプリケーションがプロセスにある場合はエラーダイアログを表示して終了するというものです。プロセス名が同じであれば多重起動していると判定してもかまいませんが、別のアプリケーションがまったく同じ名前の exe ファイルであった場合は不具合となってしまいます。このプログラムでは、プロセス名が同じであり、かつメインウィンドウのタイトルが同じである場合に多重起動と判定しています。
5.6.2 ミューテックスを使う
システムが現在起動しているプロセスを調べる方法よりも確実で、より一般的な多重起動の防止にミューテックスを応用する方法があります。ミューテックスとは、本来ならば複数のプロセス間で同期を行うためのオブジェクトです。同期とは、つまり特定の処理に対して連携しているアプリケーションが共有しているリソースにアクセスするとき、お互いがデータの更新を競合しないように通信しあうことです。
複数のプロセスが実行している状態で、あるプロセスが共有しているリソースにアクセスする場合、最初にミューテックスオブジェクトを確保して所有権を取得します。特定のプロセスがミューテックスの所有権を保持している間、他のプロセスが同じミューテックスの所有権を得ることはできなくなり、先行するプロセスがミューテックスの所有権を解放するまで待機しなければならなくなります。
ミューテックスは Windows システムが管理しているオブジェクトなので、異なるアプリケーション間で同じミューテックスを共有できます。この機能を応用して、アプリケーション起動時にミューテックスの所有権を取得し、終了時に所有権を解放すれば同じアプリケーションが多重起動しないように設定できます。
ミューテックスを利用するには System.Threading.Mutex クラスを使います。
System.Object System.MarshalByRefObject System.Threading.WaitHandle System.Threading.Mutex
public sealed class Mutex : WaitHandle
新しいミューテックスオブジェクトを生成するには、コンストラクタからインスタンスを生成します。生成されるインスタンスはアプリケーション固有のものですが、インスタンス内部で保有しているネイティブシステムと関連付けられているミューテックスオブジェクトはシステムで共有されるため、他のアプリケーションと通信することができます。
public Mutex(bool initiallyOwned, string name)
public Mutex(bool initiallyOwned, string name, out bool createdNew)
initiallyOwned パラメータには、このコンストラクタを呼び出したアプリケーションにミューテックスの所有権を与えるかどうかを指定します。インスタンスを生成した時点で所有権を即座に取得する場合は true を、そうではない場合は false を指定します。複数のアプリケーション間で同期を行うために使う場合、すでに他のアプリケーションが所有権を所持している可能性もあるので通常は false に設定します。
name パラメータにはミューテックスオブジェクトの名前を指定します。この名前はアプリケーション空間固有のものではなく、システムで共有される名前です。指定した名前のミューテックスオブジェクトがシステム内に存在しない場合、ネイティブシステムは指定した名前のミューテックスを新しく生成します。すでに、他のプロセスによって指定した名前のミューテックスが生成されている場合は、生成済みのミューテックスオブジェクトが返されます。つまり、異なるプロセス間でも同じ名前のミューテックスを要求すれば、同一のミューテックスオブジェクトを共有できるという仕組みです。
createdNew パラメータは、コンストラクタの結果が返されたときに所有権を得ることができれば true、所有権を取得しなければ false が格納されます。このパラメータは out キーワードが指定されているので、結果を得るための専用の変数を渡します。そのため、上書きをしても良い bool 型の変数を指定する必要があります。
アプリケーションの多重起動を確認するのであれば、常に同じ名前をミューテックスに指定して、initiallyOwned を true に設定してインスタンスを生成します。その後、アプリケーションが終了するまでミューテックスの所有権を解放しなければ、同じプロセスが多重起動したときに、2つ目のプロセスからはミューテックスの所有権を得られなくなります。createdNew パラメータの結果を調べて、結果が false であれば多重起動であることが確認できます。
アプリケーションを終了するときにミューテックスの所有権を解放するには ReleaseMutex() メソッドを呼びだします。
public void ReleaseMutex()
最初に起動したプロセスが、プロセス終了まで所有権を持ち続け、終了直前に ReleaseMutex() を呼び出して所有権を解放します。
using System.Threading; using System.Windows.Forms; public class Test : Form { private const string APPNAME = "HereWeGo!!"; private static Mutex mutex; static void Main() { bool ownership; mutex = new Mutex(true, APPNAME, out ownership); if (!ownership) { MessageBox.Show( null, "このアプリケーションはすでに起動しています", APPNAME, MessageBoxButtons.OK, MessageBoxIcon.Error ); return; } Application.Run(new Test()); mutex.ReleaseMutex(); } }
コード3は、Main() メソッドの先頭で Mutex インスタンスを生成しています。このとき指定する名前はフィールドで定義されている定数 APPNAME です。多重起動したとき、同じプログラムであれば異なるプロセスであっても同じ名前のミューテックスを指定することになるためミューテックスを共有することになります。
もし、現在のプロセスが最初の起動であれば所有権をそのまま得ることができるため ownership 変数は true となります。その後、ミューテックスの所有権を維持したままプログラムが起動し、Main() メソッドの末尾でアプリケーションが終了する直前に ReleaseMutex() メソッドで所有権を返上しています。
現在プロセスが2回目以降の起動であれば、ミューテックスはすでに所有権が最初に起動したプロセスに移っているため、Mutex() コンストラクタの結果 ownership 変数は false となり、所有権の取得に失敗します。所有権が取得できなければ多重起動であると判断して、エラーメッセージを表示した後アプリケーションを終了させます。