From 95da95cd8024e62b4d5259324089e720eb1f59a6 Mon Sep 17 00:00:00 2001 From: Manfred Wallner Date: Thu, 20 Apr 2017 09:47:41 +0200 Subject: [PATCH 1/3] GH-1258 - added global mutex a single global mutex (process-wide) on serialization and deserialization of the xml-configuration file ensures the file stays consistent --- src/chocolatey/chocolatey.csproj | 1 + .../guards/SingleGlobalMutex.cs | 63 ++++++ .../infrastructure/services/XmlService.cs | 186 +++++++++--------- 3 files changed, 160 insertions(+), 90 deletions(-) create mode 100644 src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index bad1e387a2..23ce8399d0 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -109,6 +109,7 @@ + diff --git a/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs b/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs new file mode 100644 index 0000000000..22f04a8cc7 --- /dev/null +++ b/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Runtime.InteropServices; +using System.Reflection; +using System.Security.AccessControl; +using System.Security.Principal; + +namespace chocolatey.infrastructure.guards +{ + // origin source: http://stackoverflow.com/a/7810107/2279385 + class SingleGlobalMutex : IDisposable + { + public bool hasHandle = false; + Mutex mutex; + + private void InitMutex() + { + string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString(); + string mutexId = string.Format("Global\\{{{0}}}", appGuid); + mutex = new Mutex(false, mutexId); + + var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow); + var securitySettings = new MutexSecurity(); + securitySettings.AddAccessRule(allowEveryoneRule); + mutex.SetAccessControl(securitySettings); + } + + public SingleGlobalMutex(int timeOut) + { + InitMutex(); + try + { + if (timeOut < 0) + hasHandle = mutex.WaitOne(Timeout.Infinite, false); + else + hasHandle = mutex.WaitOne(timeOut, false); + + if (hasHandle == false) + throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance"); + } + catch (AbandonedMutexException) + { + hasHandle = true; + } + } + + + public void Dispose() + { + if (mutex != null) + { + if (hasHandle) + mutex.ReleaseMutex(); + mutex.Dispose(); + } + } + } + + +} diff --git a/src/chocolatey/infrastructure/services/XmlService.cs b/src/chocolatey/infrastructure/services/XmlService.cs index 3be9ca0ff4..320a047a44 100644 --- a/src/chocolatey/infrastructure/services/XmlService.cs +++ b/src/chocolatey/infrastructure/services/XmlService.cs @@ -24,6 +24,7 @@ namespace chocolatey.infrastructure.services using cryptography; using filesystem; using tolerance; + using chocolatey.infrastructure.guards; /// /// XML interaction @@ -41,55 +42,58 @@ public XmlService(IFileSystem fileSystem, IHashProvider hashProvider) public XmlType deserialize(string xmlFilePath) { - return FaultTolerance.try_catch_with_logging_exception( - () => - { - var xmlSerializer = new XmlSerializer(typeof(XmlType)); - using (var fileStream = _fileSystem.open_file_readonly(xmlFilePath)) - using (var fileReader = new StreamReader(fileStream)) - using (var xmlReader = XmlReader.Create(fileReader)) - { - if (!xmlSerializer.CanDeserialize(xmlReader)) - { - this.Log().Warn("Cannot deserialize response of type {0}", typeof(XmlType)); - return default(XmlType); - } + using (new SingleGlobalMutex(1000)) + { + return FaultTolerance.try_catch_with_logging_exception( + () => + { + var xmlSerializer = new XmlSerializer(typeof(XmlType)); + using (var fileStream = _fileSystem.open_file_readonly(xmlFilePath)) + using (var fileReader = new StreamReader(fileStream)) + using (var xmlReader = XmlReader.Create(fileReader)) + { + if (!xmlSerializer.CanDeserialize(xmlReader)) + { + this.Log().Warn("Cannot deserialize response of type {0}", typeof(XmlType)); + return default(XmlType); + } - try - { - return (XmlType)xmlSerializer.Deserialize(xmlReader); - } - catch(InvalidOperationException ex) - { - // Check if its just a malformed document. - if (ex.Message.Contains("There is an error in XML document")) - { - // If so, check for a backup file and try an parse that. - if (_fileSystem.file_exists(xmlFilePath + ".backup")) - { - using (var backupStream = _fileSystem.open_file_readonly(xmlFilePath + ".backup")) - using (var backupReader = new StreamReader(backupStream)) - using (var backupXmlReader = XmlReader.Create(backupReader)) - { - var validConfig = (XmlType)xmlSerializer.Deserialize(backupXmlReader); + try + { + return (XmlType)xmlSerializer.Deserialize(xmlReader); + } + catch (InvalidOperationException ex) + { + // Check if its just a malformed document. + if (ex.Message.Contains("There is an error in XML document")) + { + // If so, check for a backup file and try an parse that. + if (_fileSystem.file_exists(xmlFilePath + ".backup")) + { + using (var backupStream = _fileSystem.open_file_readonly(xmlFilePath + ".backup")) + using (var backupReader = new StreamReader(backupStream)) + using (var backupXmlReader = XmlReader.Create(backupReader)) + { + var validConfig = (XmlType)xmlSerializer.Deserialize(backupXmlReader); - // If there's no errors and it's valid, go ahead and replace the bad file with the backup. - if(validConfig != null) - { - _fileSystem.copy_file(xmlFilePath + ".backup", xmlFilePath, overwriteExisting: true); - } + // If there's no errors and it's valid, go ahead and replace the bad file with the backup. + if (validConfig != null) + { + _fileSystem.copy_file(xmlFilePath + ".backup", xmlFilePath, overwriteExisting: true); + } - return validConfig; - } - } - } - - throw; - } - } - }, - "Error deserializing response of type {0}".format_with(typeof(XmlType)), - throwError: true); + return validConfig; + } + } + } + + throw; + } + } + }, + "Error deserializing response of type {0}".format_with(typeof(XmlType)), + throwError: true); + } } public void serialize(XmlType xmlType, string xmlFilePath) @@ -97,54 +101,56 @@ public void serialize(XmlType xmlType, string xmlFilePath) serialize(xmlType,xmlFilePath, isSilent: false); } - public void serialize(XmlType xmlType, string xmlFilePath, bool isSilent) - { - _fileSystem.create_directory_if_not_exists(_fileSystem.get_directory_name(xmlFilePath)); + public void serialize(XmlType xmlType, string xmlFilePath, bool isSilent) + { + _fileSystem.create_directory_if_not_exists(_fileSystem.get_directory_name(xmlFilePath)); + using (new SingleGlobalMutex(1000)) + { + FaultTolerance.try_catch_with_logging_exception( + () => + { + var xmlSerializer = new XmlSerializer(typeof(XmlType)); - FaultTolerance.try_catch_with_logging_exception( - () => - { - var xmlSerializer = new XmlSerializer(typeof(XmlType)); + // Write the updated file to memory + using (var memoryStream = new MemoryStream()) + using (var streamWriter = new StreamWriter(memoryStream, encoding: new UTF8Encoding(encoderShouldEmitUTF8Identifier: true))) + { + xmlSerializer.Serialize(streamWriter, xmlType); + streamWriter.Flush(); - // Write the updated file to memory - using(var memoryStream = new MemoryStream()) - using(var streamWriter = new StreamWriter(memoryStream, encoding: new UTF8Encoding(encoderShouldEmitUTF8Identifier: true))) - { - xmlSerializer.Serialize(streamWriter, xmlType); - streamWriter.Flush(); + memoryStream.Position = 0; - memoryStream.Position = 0; - - // Grab the hash of both files and compare them. - var originalFileHash = _hashProvider.hash_file(xmlFilePath); - if (!originalFileHash.is_equal_to(_hashProvider.hash_stream(memoryStream))) - { - // If there wasn't a file there in the first place, just write the new one out directly. - if(string.IsNullOrEmpty(originalFileHash)) - { - using(var updateFileStream = _fileSystem.create_file(xmlFilePath)) - { - memoryStream.Position = 0; - memoryStream.CopyTo(updateFileStream); + // Grab the hash of both files and compare them. + var originalFileHash = _hashProvider.hash_file(xmlFilePath); + if (!originalFileHash.is_equal_to(_hashProvider.hash_stream(memoryStream))) + { + // If there wasn't a file there in the first place, just write the new one out directly. + if (string.IsNullOrEmpty(originalFileHash)) + { + using (var updateFileStream = _fileSystem.create_file(xmlFilePath)) + { + memoryStream.Position = 0; + memoryStream.CopyTo(updateFileStream); - return; - } - } + return; + } + } - // Otherwise, create an update file, and resiliently move it into place. - var tempUpdateFile = xmlFilePath + ".update"; - using(var updateFileStream = _fileSystem.create_file(tempUpdateFile)) - { - memoryStream.Position = 0; - memoryStream.CopyTo(updateFileStream); - } - _fileSystem.replace_file(tempUpdateFile, xmlFilePath, xmlFilePath + ".backup"); - } - } - }, - "Error serializing type {0}".format_with(typeof(XmlType)), - throwError: true, - isSilent: isSilent); - } + // Otherwise, create an update file, and resiliently move it into place. + var tempUpdateFile = xmlFilePath + ".update"; + using (var updateFileStream = _fileSystem.create_file(tempUpdateFile)) + { + memoryStream.Position = 0; + memoryStream.CopyTo(updateFileStream); + } + _fileSystem.replace_file(tempUpdateFile, xmlFilePath, xmlFilePath + ".backup"); + } + } + }, + "Error serializing type {0}".format_with(typeof(XmlType)), + throwError: true, + isSilent: isSilent); + } + } } } From ff8c3f34720f3869a9a1b4eda03fd1d4ed2a417e Mon Sep 17 00:00:00 2001 From: Manfred Wallner Date: Wed, 26 Apr 2017 14:28:20 +0200 Subject: [PATCH 2/3] GH-1258 added license text --- .../guards/SingleGlobalMutex.cs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs b/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs index 22f04a8cc7..a34b46ce51 100644 --- a/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs +++ b/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs @@ -1,16 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Runtime.InteropServices; -using System.Reflection; -using System.Security.AccessControl; -using System.Security.Principal; +// Copyright © 2017 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. namespace chocolatey.infrastructure.guards { - // origin source: http://stackoverflow.com/a/7810107/2279385 + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Runtime.InteropServices; + using System.Reflection; + using System.Security.AccessControl; + using System.Security.Principal; + class SingleGlobalMutex : IDisposable { public bool hasHandle = false; From 42d6c4367c6d0e9585565c1b7f93ab56e80718da Mon Sep 17 00:00:00 2001 From: Manfred Wallner Date: Wed, 26 Apr 2017 23:07:34 +0200 Subject: [PATCH 3/3] GH-1264 fixes for review corrected bracketing, visibility modifiers and namespace --- src/chocolatey/chocolatey.csproj | 2 +- .../infrastructure/services/XmlService.cs | 6 ++--- .../GlobalMutex.cs} | 22 +++++++++++++++---- 3 files changed, 22 insertions(+), 8 deletions(-) rename src/chocolatey/infrastructure/{guards/SingleGlobalMutex.cs => synchronization/GlobalMutex.cs} (83%) diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 23ce8399d0..0d86c84d73 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -109,7 +109,7 @@ - + diff --git a/src/chocolatey/infrastructure/services/XmlService.cs b/src/chocolatey/infrastructure/services/XmlService.cs index 320a047a44..776d6ff5c0 100644 --- a/src/chocolatey/infrastructure/services/XmlService.cs +++ b/src/chocolatey/infrastructure/services/XmlService.cs @@ -24,7 +24,7 @@ namespace chocolatey.infrastructure.services using cryptography; using filesystem; using tolerance; - using chocolatey.infrastructure.guards; + using chocolatey.infrastructure.synchronization; /// /// XML interaction @@ -42,7 +42,7 @@ public XmlService(IFileSystem fileSystem, IHashProvider hashProvider) public XmlType deserialize(string xmlFilePath) { - using (new SingleGlobalMutex(1000)) + using (new GlobalMutex(1000)) { return FaultTolerance.try_catch_with_logging_exception( () => @@ -104,7 +104,7 @@ public void serialize(XmlType xmlType, string xmlFilePath) public void serialize(XmlType xmlType, string xmlFilePath, bool isSilent) { _fileSystem.create_directory_if_not_exists(_fileSystem.get_directory_name(xmlFilePath)); - using (new SingleGlobalMutex(1000)) + using (new GlobalMutex(1000)) { FaultTolerance.try_catch_with_logging_exception( () => diff --git a/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs b/src/chocolatey/infrastructure/synchronization/GlobalMutex.cs similarity index 83% rename from src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs rename to src/chocolatey/infrastructure/synchronization/GlobalMutex.cs index a34b46ce51..521c4d8e39 100644 --- a/src/chocolatey/infrastructure/guards/SingleGlobalMutex.cs +++ b/src/chocolatey/infrastructure/synchronization/GlobalMutex.cs @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace chocolatey.infrastructure.guards +namespace chocolatey.infrastructure.synchronization { using System; using System.Collections.Generic; @@ -26,10 +26,16 @@ namespace chocolatey.infrastructure.guards using System.Security.AccessControl; using System.Security.Principal; - class SingleGlobalMutex : IDisposable + /// + /// global mutex used for synchronizing multiple processes based on appguid + /// + /// + /// Based on http://stackoverflow.com/a/7810107/2279385 + /// + public class GlobalMutex : IDisposable { public bool hasHandle = false; - Mutex mutex; + private Mutex mutex; private void InitMutex() { @@ -43,18 +49,24 @@ private void InitMutex() mutex.SetAccessControl(securitySettings); } - public SingleGlobalMutex(int timeOut) + public GlobalMutex(int timeOut) { InitMutex(); try { if (timeOut < 0) + { hasHandle = mutex.WaitOne(Timeout.Infinite, false); + } else + { hasHandle = mutex.WaitOne(timeOut, false); + } if (hasHandle == false) + { throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance"); + } } catch (AbandonedMutexException) { @@ -68,7 +80,9 @@ public void Dispose() if (mutex != null) { if (hasHandle) + { mutex.ReleaseMutex(); + } mutex.Dispose(); } }