[smuxi] Common(-Tests): implemented SingleApplicationInstance
- From: Mirco M. M. Bauer <mmmbauer src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [smuxi] Common(-Tests): implemented SingleApplicationInstance
- Date: Wed, 21 Jan 2015 19:31:42 +0000 (UTC)
commit b6f538d92417e77445541886768bc50414faf281
Author: Mirco Bauer <meebey meebey net>
Date: Tue Jan 20 17:18:53 2015 +0100
Common(-Tests): implemented SingleApplicationInstance
This implementation covers MS .NET on Windows, Mono with SHM on Unix, Mono
without SHM on Unix. It uses named mutex + Remoting or file mutex + Remoting
depending on what is supported by the operating system and CLI runtime.
* MS .NET on Windows -> named mutex
* Mono on Windows -> named mutex
* Mono with SHM on Linux -> named mutex
* Mono without SHM on Linux -> file lock
For file mutex support you need to compile this class with -define:MONO_UNIX
and -r:Mono.Posix.dll otherwise it will throw a NotSupportedException when
named mutex is not available.
src/Common-Tests/Common-Tests.csproj | 1 +
src/Common-Tests/SingleApplicationInstanceTests.cs | 127 +++++++++
src/Common/Common.csproj | 6 +-
src/Common/Makefile.am | 6 +-
src/Common/SingleApplicationInstance.cs | 297 ++++++++++++++++++++
5 files changed, 433 insertions(+), 4 deletions(-)
---
diff --git a/src/Common-Tests/Common-Tests.csproj b/src/Common-Tests/Common-Tests.csproj
index 447906a..aad2a24 100644
--- a/src/Common-Tests/Common-Tests.csproj
+++ b/src/Common-Tests/Common-Tests.csproj
@@ -37,6 +37,7 @@
<Compile Include="PatternTests.cs" />
<Compile Include="Crc32Tests.cs" />
<Compile Include="RateLimiterTests.cs" />
+ <Compile Include="SingleApplicationInstanceTests.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="System" />
diff --git a/src/Common-Tests/SingleApplicationInstanceTests.cs
b/src/Common-Tests/SingleApplicationInstanceTests.cs
new file mode 100644
index 0000000..ffd9b83
--- /dev/null
+++ b/src/Common-Tests/SingleApplicationInstanceTests.cs
@@ -0,0 +1,127 @@
+// Smuxi - Smart MUltipleXed Irc
+//
+// Copyright (c) 2015 Mirco Bauer <meebey meebey net>
+//
+// Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using System;
+using NUnit.Framework;
+using System.Runtime.Remoting;
+using System.Runtime.Remoting.Channels;
+using System.Threading;
+using System.IO;
+
+namespace Smuxi.Common
+{
+ [TestFixture]
+ public class SingleApplicationInstanceTests
+ {
+ SingleApplicationInstance<TestApplication> FirstInstance { get; set; }
+
+ class TestApplication : MarshalByRefObject
+ {
+ public int InvokeCounter { get; private set; }
+
+ public void Invoke()
+ {
+ InvokeCounter++;
+ }
+
+ public override object InitializeLifetimeService()
+ {
+ // I want to live forever
+ return null;
+ }
+ }
+
+ [SetUp]
+ public void SetUp()
+ {
+ FirstInstance = new SingleApplicationInstance<TestApplication>("test");
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (FirstInstance != null) {
+ FirstInstance.Dispose();
+ }
+ try {
+ var mutex = Mutex.OpenExisting("test");
+ Assert.Fail();
+ } catch (WaitHandleCannotBeOpenedException) {
+ }
+ }
+
+ [Test]
+ public void IsFirstInstance()
+ {
+ var instance1 = FirstInstance;
+ Assert.IsTrue(instance1.IsFirstInstance);
+
+ using (var instance2 = new SingleApplicationInstance<TestApplication>("test")) {
+ Assert.IsFalse(instance2.IsFirstInstance);
+ }
+ }
+
+ [Test]
+ public void Dispose()
+ {
+ FirstInstance.Dispose();
+ FirstInstance = null;
+
+ try {
+ var mutex = Mutex.OpenExisting("test");
+ Assert.Fail();
+ } catch (WaitHandleCannotBeOpenedException) {
+ }
+ Assert.IsNull(ChannelServices.GetChannel("ipc"));
+
+ var appData = Environment.GetFolderPath(
+ Environment.SpecialFolder.LocalApplicationData
+ );
+ var lockDirectory = Path.Combine(appData, "SingleApplicationInstance");
+ var lockFile = Path.Combine(lockDirectory, "test");
+ //Assert.IsFalse(File.Exists(lockFile));
+
+ FirstInstance = new SingleApplicationInstance<TestApplication>("test");
+ Assert.IsTrue(FirstInstance.IsFirstInstance);
+ //Assert.IsTrue(File.Exists(lockFile));
+ }
+
+ [Test]
+ public void Invoke()
+ {
+ var instance1 = FirstInstance;
+ Assert.IsTrue(instance1.IsFirstInstance);
+
+ instance1.FirstInstance = new TestApplication();
+ Assert.IsNotNull(instance1.FirstInstance);
+ Assert.AreEqual(0, instance1.FirstInstance.InvokeCounter);
+ instance1.FirstInstance.Invoke();
+ Assert.AreEqual(1, instance1.FirstInstance.InvokeCounter);
+
+ using (var instance2 = new SingleApplicationInstance<TestApplication>("test")) {
+ Assert.IsFalse(instance2.IsFirstInstance);
+
+ Assert.IsNotNull(instance2.FirstInstance);
+ Assert.AreEqual(1, instance2.FirstInstance.InvokeCounter);
+ instance2.FirstInstance.Invoke();
+ Assert.AreEqual(2, instance2.FirstInstance.InvokeCounter);
+ }
+ }
+ }
+}
diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj
index ce9f98f..7f93ee9 100644
--- a/src/Common/Common.csproj
+++ b/src/Common/Common.csproj
@@ -17,7 +17,7 @@
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\bin\debug</OutputPath>
- <DefineConstants>DEBUG;TRACE;LOG4NET;NET_2_0;NDESK_OPTIONS</DefineConstants>
+ <DefineConstants>DEBUG;TRACE;LOG4NET;NET_2_0;NDESK_OPTIONS;MONO_UNIX</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
@@ -31,7 +31,7 @@
<DebugType>full</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\bin\release</OutputPath>
- <DefineConstants>TRACE;LOG4NET;NET_2_0;NDESK_OPTIONS</DefineConstants>
+ <DefineConstants>TRACE;LOG4NET;NET_2_0;NDESK_OPTIONS;MONO_UNIX</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Execution>
@@ -58,6 +58,7 @@
<Compile Include="AtomFeed.cs" />
<Compile Include="SpecialFolderPatternConverter.cs" />
<Compile Include="RateLimiter.cs" />
+ <Compile Include="SingleApplicationInstance.cs" />
</ItemGroup>
<ItemGroup>
<None Include="Defines.cs.in" />
@@ -70,6 +71,7 @@
<Reference Include="Mono.Posix" />
<Reference Include="System.Core" />
<Reference Include="System.Xml" />
+ <Reference Include="System.Runtime.Remoting" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
\ No newline at end of file
diff --git a/src/Common/Makefile.am b/src/Common/Makefile.am
index 045df52..9625fd9 100644
--- a/src/Common/Makefile.am
+++ b/src/Common/Makefile.am
@@ -5,7 +5,7 @@ EXTRA_DIST =
if ENABLE_RELEASE
ASSEMBLY_COMPILER_COMMAND = @MCS@
-ASSEMBLY_COMPILER_FLAGS = -noconfig -codepage:utf8 -warn:4 -optimize+ "-define:NET_2_0,NDESK_OPTIONS"
+ASSEMBLY_COMPILER_FLAGS = -noconfig -codepage:utf8 -warn:4 -optimize+
"-define:NET_2_0,NDESK_OPTIONS,MONO_UNIX"
ASSEMBLY = ../../bin/release/smuxi-common.dll
ASSEMBLY_MDB =
@@ -19,7 +19,7 @@ endif
if ENABLE_DEBUG
ASSEMBLY_COMPILER_COMMAND = @MCS@
-ASSEMBLY_COMPILER_FLAGS = -noconfig -codepage:utf8 -warn:4 -debug -optimize+
"-define:DEBUG,TRACE,LOG4NET,NET_2_0,NDESK_OPTIONS"
+ASSEMBLY_COMPILER_FLAGS = -noconfig -codepage:utf8 -warn:4 -debug -optimize+
"-define:DEBUG,TRACE,LOG4NET,NET_2_0,NDESK_OPTIONS,MONO_UNIX"
ASSEMBLY = ../../bin/debug/smuxi-common.dll
ASSEMBLY_MDB = $(ASSEMBLY).mdb
@@ -59,6 +59,7 @@ FILES = \
NDesk.Options.cs \
Defines.cs \
SpecialFolderPatternConverter.cs \
+ SingleApplicationInstance.cs \
TaskQueue.cs \
ThreadPoolQueue.cs \
Platform.cs \
@@ -75,6 +76,7 @@ EXTRAS = \
REFERENCES = \
System \
System.Core \
+ System.Runtime.Remoting \
System.Xml \
Mono.Posix
diff --git a/src/Common/SingleApplicationInstance.cs b/src/Common/SingleApplicationInstance.cs
new file mode 100644
index 0000000..9743f23
--- /dev/null
+++ b/src/Common/SingleApplicationInstance.cs
@@ -0,0 +1,297 @@
+// This file is part of Smuxi and is licensed under the terms of MIT/X11
+//
+// Copyright (c) 2015 Mirco Bauer <meebey meebey net>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+using System;
+using System.Threading;
+using System.Runtime.Remoting.Channels.Ipc;
+using System.Runtime.Remoting;
+using System.Runtime.Remoting.Channels;
+using System.IO;
+
+#if MONO_UNIX
+using Mono.Unix;
+using Mono.Unix.Native;
+#endif
+
+namespace Smuxi.Common
+{
+ public class SingleApplicationInstance<T> : IDisposable where T : MarshalByRefObject
+ {
+ public string Identifier { get; private set; }
+ public bool IsFirstInstance { get; private set; }
+ Mutex FirstInstanceMutex { get; set; }
+#if MONO_UNIX
+ UnixFileInfo FirstInstanceFileInfo { get; set; }
+ UnixStream FirstInstanceFileStream { get; set; }
+ Thread UnixSignalThread { get; set; }
+#endif
+ IChannel RemotingChannel { get; set; }
+ string RemotingObjectName { get; set; }
+
+ T f_FirstInstance;
+ public T FirstInstance {
+ get {
+ if (f_FirstInstance == default(T)) {
+ if (IsFirstInstance) {
+ throw new InvalidOperationException("FirstInstance must be initialized first.");
+ } else {
+ ConnectToFirstInstance();
+ }
+ }
+ return f_FirstInstance;
+ }
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ if (!IsFirstInstance) {
+ throw new InvalidOperationException("FirstInstance must be initialized by the first
instance.");
+ }
+
+ f_FirstInstance = value;
+ ExposeFirstInstance();
+ }
+ }
+
+ public SingleApplicationInstance()
+ {
+ Identifier = typeof(T).Assembly.Location;
+ // Mono's IPC does not like \ or / in the name
+ // On MS .NET Local\ as a special and valid prefix!
+ Identifier = Identifier.Replace("\\", "_").Replace("/", "_");
+ Init();
+ }
+
+ public SingleApplicationInstance(string instanceIdentifier)
+ {
+ if (instanceIdentifier == null) {
+ throw new ArgumentNullException("instanceIdentifier");
+ }
+ Identifier = instanceIdentifier;
+ Init();
+ }
+
+ ~SingleApplicationInstance()
+ {
+ Dispose(false);
+ }
+
+ void Init()
+ {
+ RemotingObjectName = "SingleApplicationInstance";
+
+ // MS .NET on Windows -> named mutex
+ // Mono on Windows -> named mutex
+ // Mono w/ SHM on Linux -> named mutex
+ // Mono w/o SHM on Linux -> file lock
+ var platform = Environment.OSVersion.Platform;
+ switch (platform) {
+ case PlatformID.Win32NT:
+ InitMutex();
+ break;
+ case PlatformID.Unix:
+ var has_shm = false;
+ if (IsRunningOnMono()) {
+ // we can only assume that named mutex are available if
+ // MONO_ENABLE_SHM is set and MONO_DISABLE_SHM is unset
+ var enable_shm = Environment.GetEnvironmentVariable("MONO_ENABLE_SHM");
+ var disalbe_shm = Environment.GetEnvironmentVariable("MONO_DISABLE_SHM");
+ has_shm = !String.IsNullOrEmpty(enable_shm) &&
+ String.IsNullOrEmpty(disalbe_shm);
+ }
+
+ if (has_shm) {
+ InitMutex();
+ } else {
+ InitFileLock();
+ }
+ break;
+ default:
+ throw new NotSupportedException(
+ String.Format(
+ "Unknown/unsupported operating system: {0}",
+ platform
+ )
+ );
+ }
+ }
+
+ void InitMutex()
+ {
+ bool isFirstInstance;
+ FirstInstanceMutex = new Mutex(true, Identifier, out isFirstInstance);
+ IsFirstInstance = isFirstInstance;
+ }
+
+#if MONO_UNIX
+ string GetFileLockDirectory()
+ {
+ string lockDirRoot = null;
+
+ var xdg_runtime_dir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
+ if (!String.IsNullOrEmpty(xdg_runtime_dir)) {
+ // XDG_RUNTIME_DIR (/run/user/$UID) is the preferred location as it
+ // gets purged every reboot. Thus stalled file locks would
+ // automatically go away after a reboot.
+ lockDirRoot = xdg_runtime_dir;
+ } else {
+ // XDG_CACHE_HOME or ~/.cache is a good fallback if XDG_RUNTIME_DIR
+ // is not available as other users can't write there.
+ var xdg_cache_home = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
+ if (String.IsNullOrEmpty(xdg_cache_home)) {
+ var home = Environment.GetEnvironmentVariable("HOME");
+ lockDirRoot = Path.Combine(home, ".cache");
+ } else {
+ lockDirRoot = xdg_cache_home;
+ }
+ }
+ // /tmp or /dev/shm? No, thanks! We can't use those as rendezvous point
+ // as other users could easy break it.
+
+ return Path.Combine(lockDirRoot, "SingleApplicationInstance");
+ }
+
+ void InitFileLock()
+ {
+ var lockDirectory = GetFileLockDirectory();
+ if (!Directory.Exists(lockDirectory)) {
+ Directory.CreateDirectory(lockDirectory);
+ }
+ var lockFile = Path.Combine(lockDirectory, Identifier);
+ FirstInstanceFileInfo = new UnixFileInfo(lockFile);
+ try {
+ FirstInstanceFileStream = FirstInstanceFileInfo.Open(
+ OpenFlags.O_CREAT | OpenFlags.O_EXCL,
+ FilePermissions.S_IRWXU
+ );
+ IsFirstInstance = true;
+ } catch (Exception) {
+ IsFirstInstance = false;
+ }
+
+ if (IsFirstInstance) {
+ // managed shutdown
+ AppDomain.CurrentDomain.ProcessExit += (sender, e) => {
+ ReleaseFileLock();
+ };
+
+ // signal handlers
+ UnixSignal[] shutdown_signals = {
+ new UnixSignal(Signum.SIGINT),
+ new UnixSignal(Signum.SIGTERM),
+ };
+ UnixSignalThread = new Thread(() => {
+ UnixSignal.WaitAny(shutdown_signals);
+ ReleaseFileLock();
+ });
+ UnixSignalThread.Start();
+ }
+ }
+
+ // this method is idempotent
+ void ReleaseFileLock()
+ {
+ var lockFileInfo = FirstInstanceFileInfo;
+ if (lockFileInfo == null) {
+ return;
+ }
+ if (!IsFirstInstance) {
+ return;
+ }
+
+ FirstInstanceFileInfo = null;
+ if (!lockFileInfo.Exists) {
+ return;
+ }
+ lockFileInfo.Delete();
+ }
+#else
+ void InitFileLock()
+ {
+ throw new NotSupportedException("SingleApplicationInstance was built without MONO_UNIX
support.");
+ }
+#endif
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected void Dispose(bool disposing)
+ {
+ var channel = RemotingChannel;
+ if (channel != null) {
+ RemotingChannel = null;
+ ChannelServices.UnregisterChannel(channel);
+ }
+
+ var mutex = FirstInstanceMutex;
+ if (mutex != null) {
+ FirstInstanceMutex = null;
+ if (IsFirstInstance && disposing) {
+ // HACK: we are not allowed to release the mutex from a
+ // thread that hasn't created it! Thus we only release
+ // if Dispose() was called and not from the finalizer.
+ mutex.ReleaseMutex();
+ }
+ mutex.Close();
+ mutex.Dispose();
+ }
+
+#if MONO_UNIX
+ var lockStream = FirstInstanceFileStream;
+ if (lockStream != null) {
+ FirstInstanceFileStream = null;
+ lockStream.Close();
+ }
+ ReleaseFileLock();
+ var signalThread = UnixSignalThread;
+ if (signalThread != null) {
+ UnixSignalThread = null;
+ try {
+ signalThread.Abort();
+ } catch {
+ }
+ }
+#endif
+ }
+
+ void ExposeFirstInstance()
+ {
+ RemotingServices.Marshal(FirstInstance, RemotingObjectName);
+ RemotingChannel = new IpcChannel(Identifier);
+ ChannelServices.RegisterChannel(RemotingChannel, false);
+ }
+
+ void ConnectToFirstInstance()
+ {
+ RemotingChannel = new IpcClientChannel();
+ ChannelServices.RegisterChannel(RemotingChannel, false);
+ f_FirstInstance = (T) Activator.GetObject(typeof(T), "ipc://" + Identifier + "/" +
RemotingObjectName);
+ }
+
+ static bool IsRunningOnMono()
+ {
+ return Type.GetType("Mono.Runtime") != null;
+ }
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]