[smuxi] Common(-Tests): implemented SingleApplicationInstance



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]