実に1年ぶりですね。あんまり書くネタないんだよな。調べれば大抵のことは他の人が書いてるし。
二重起動を禁止する、っていうエントリーは、いろんなところで見かけるんだけど、
二重起動を禁止して、尚かつ既に起動しているプロセスに対して何かする、っていうエントリーは、いまいち回答が見つかりにくい。
ならいっそ、共通処理化出来たら意外とニーズはあるんじゃないのか?という企み。
キーワードは、「Ipc」である。ここに書いてあることで理解できないことや、もっと発展的にやってみたい、という方は、「Ipc C#(またはVB)」で検索すれば、結構それなりに引っかかる。
まあ、ともあれ、まずはクラスのソースコードから。
まず肝となる「ExtendetMutex」クラスから。本来の「Mutex」が意味するところとは若干違う役割を持っているけど、適切なクラス名が思いつかなかったので、最初につけた名前でずるずるときてしまっている。ご容赦。
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Serialization.Formatters;
using System.Threading;
namespace ExMutex
{
using BinaryClient = BinaryClientFormatterSinkProvider;
using BinaryServer = BinaryServerFormatterSinkProvider;
/// <summary>同プロセスが呼び出されたことを通知するデリゲート</summary>
/// <typeparam name="TMessage">通知するメッセージの型</typeparam>
/// <param name="holder">通知するメッセージ</param>
public delegate void CadetStartedHandler<TMessage>(IMessageHolder<TMessage> holder);
/// <summary>プロセス間通信により、起動オプションなどのメッセージのやりとりをサポートするMutex</summary>
/// <typeparam name="TMessage">プロセス間でやりとりするメッセージの型</typeparam>
public sealed class ExtendedMutex<TMessage> : IMessageHolder<TMessage>, IDisposable
{
/// <summary>IPCで利用する共通メッセージクラス</summary>
private sealed class MutexMessage : MarshalByRefObject, IMessageHolder<TMessage>
{
#region ISyncHolder<T> メンバ
private bool mIsCadet = false;
public bool IsCadet
{
get
{
try { return mIsCadet; }
finally { mIsCadet = true; }
}
}
public TMessage Message { get; set; }
public event CadetStartedHandler<TMessage> CadetStarted;
#endregion
public void RaiseCadetStarted(TMessage e)
{
Message = e;
if (CadetStarted != null)
CadetStarted(this);
}
}
private IChannel mChannel;
private MutexMessage mMessage;
private ExtendedMutex(IChannel channel, MutexMessage message)
{
this.mChannel = channel;
this.mMessage = message;
this.IsCadet = message.IsCadet;
}
#region ISyncHolder<T> メンバ
/// <summary>起動したプロセスが、すでに他に存在するかどうかを取得します。</summary>
public bool IsCadet { get; private set; }
/// <summary>プロセス間通信オブジェクト。</summary>
public TMessage Message { get { return mMessage.Message; } }
/// <summary>他のプロセスが後から起動した場合に発生するイベント。</summary>
public event CadetStartedHandler<TMessage> CadetStarted
{
add
{
if (mMessage != null)
mMessage.CadetStarted += value;
}
remove
{
if (mMessage != null)
mMessage.CadetStarted -= value;
}
}
#endregion
/// <summary>プロセス開始通知</summary>
/// <param name="name">開始プロセスのユニークな名称文字列</param>
/// <param name="eventArg">先に開始していた同名のプロセスがあった場合、当該プロセスに送信するメッセージ</param>
public static ExtendedMutex<TMessage> Start(string name, TMessage eventArg)
{
// 開始プロセス自体をMutexでプロセス間同期
using (var mutex = new Mutex(true, typeof(MutexMessage).FullName + "[" + name + "]"))
{
bool isServer = false;
mutex.WaitOne();
try
{
#region サーバ
// IPCチャネルの作成と登録
var channel = new IpcServerChannel(name, "remote", new BinaryServer() { TypeFilterLevel = TypeFilterLevel.Full, });
ChannelServices.RegisterChannel(channel, true);
// サーバとして起動できる
isServer = true;
RemotingConfiguration.RegisterWellKnownServiceType(typeof(MutexMessage), name, WellKnownObjectMode.Singleton);
// メッセージキャプチャを開始
var message = new MutexMessage() { Message = eventArg, };
RemotingServices.Marshal(message, name, typeof(MutexMessage));
return new ExtendedMutex<TMessage>(channel, message);
#endregion
}
catch (RemotingException)
{
// サーバの起動に失敗
if (isServer)
throw;
#region クライアント
var channel = new IpcClientChannel(name, new BinaryClient());
ChannelServices.RegisterChannel(channel, true);
RemotingConfiguration.RegisterActivatedClientType(typeof(MutexMessage), "ipc://remote/" + name);
// メッセージを発行する
MutexMessage message = (MutexMessage)Activator.GetObject(typeof(MutexMessage), "ipc://remote/" + name);
message.RaiseCadetStarted(eventArg);
return new ExtendedMutex<TMessage>(channel, message);
#endregion
}
}
}
#region IDisposable メンバ
public void Dispose()
{
try { ChannelServices.UnregisterChannel(mChannel); }
catch { }
}
#endregion
}
}
中で利用している「IMessageHolder」クラス
namespace ExMutex
{
/// <summary><seealso cref="Kyoh.ExtendedMutex"/>で用いる、プロセス間メッセージのラッパー。</summary>
/// <typeparam name="T">メッセージに用いるオブジェクトの型</typeparam>
public interface IMessageHolder<T>
{
bool IsCadet { get; }
T Message { get; }
event CadetStartedHandler<T> CadetStarted;
}
}
と、まあ。
「T」やら「TMessage」っていう型アーギュメントがあるけれど、これはプロセス間でやりとりしたいオブジェクトの型を指定する。
型は特に制約を持たない。基本的にはclassであることが望ましい(相互にやりとりできるし)けれど、一方通行のナニであればstructでも問題は無いと思う。
また、単に通知したいだけなら、メッセージを飛ばす必要もないので、T(TMessage)にobjectを指定して、渡すメッセージはnull、でも何ら問題ない。
次に、どういう風に使うか、の例。
GUIの場合とCUIの場合両方で書いてみた。
GUIの方は単に、先に走っていたプロセスがあった場合にはそのプロセスの画面(MainForm)をアクティブにする(実際にはタスクバーで点滅が起きる)という処理。ただし、CUIとして動かすために全面的にコメントアウトされているので注意。実際にはちゃんとMainFormとして画面を作らなくては動かない。
CUIの方では、メッセージとして
Environment.CommandLine
を渡している。つまり、呼び出しコマンドをそのまま文字列として渡している。
using System;
namespace ExMutex
{
public class Program
{
/*
static MainForm mainForm;
[STAThread]
static void Main()
{
using(var mutex = ExtendedMutex<object>.Start("testProcessName", null))
{
if (!mutex.IsCadet)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Program.mainForm = new MainForm();
mutex.CadetStarted += new CadetStartedHandler<object>(CadetStarted);
Application.Run(Program.mainForm);
}
}
}
static void CadetStarted(IMessageHolder<object> holder)
{
try { Program.mainForm.Activate(); }
catch { }
}
/*/
static void Main(string[] args)
{
// メッセージを文字列型で受け取るMutexに
// コマンドラインをそのまま渡す
using (var mutex = ExtendedMutex<string>.Start("testProcessName", Environment.CommandLine))
{
if (mutex.IsCadet)
return;
// 後から起動されたプロセスを処理
mutex.CadetStarted += new CadetStartedHandler<string>(CadetStarted);
// ポーリング
string input;
do { input = Console.ReadLine().Trim(); } while (string.IsNullOrEmpty(input));
}
}
/// <summary>後から起動されたプロセスのメッセージを表示する</summary>
/// <param name="holder">表示するメッセージ</param>
static void CadetStarted(IMessageHolder<string> holder)
{
Console.WriteLine(holder.Message);
}
//*/
}
}
詳しい話はそのうち気が向いたら(=十中八九気は向かない)書くけど、
要するに「先に起動した方がサーバー」「あとに起動した方はクライアント」として動作する、クライアント・サーバーモデルなあん畜生で通信してるんですよ。
もしかしたら、うまくいかないケースもあるかもしれない。