diff --git a/Application/LockHandler.cs b/Application/LockHandler.cs deleted file mode 100644 index fb768a00..00000000 --- a/Application/LockHandler.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using TweetDuck.Utils; -using TweetLib.Core.Application; - -namespace TweetDuck.Application{ - class LockHandler : IAppLockHandler{ - private const int WaitRetryDelay = 250; - private const int RestoreFailTimeout = 2000; - private const int CloseNaturallyTimeout = 10000; - private const int CloseKillTimeout = 5000; - - bool IAppLockHandler.RestoreProcess(Process process){ - if (process.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray - NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint)process.Id, 0); - - if (WindowsUtils.TrySleepUntil(() => CheckProcessExited(process) || (process.MainWindowHandle != IntPtr.Zero && process.Responding), RestoreFailTimeout, WaitRetryDelay)){ - return true; - } - } - - return false; - } - - bool IAppLockHandler.CloseProcess(Process process){ - try{ - if (process.CloseMainWindow()){ - // ReSharper disable once AccessToDisposedClosure - WindowsUtils.TrySleepUntil(() => CheckProcessExited(process), CloseNaturallyTimeout, WaitRetryDelay); - } - - if (!process.HasExited){ - process.Kill(); - // ReSharper disable once AccessToDisposedClosure - WindowsUtils.TrySleepUntil(() => CheckProcessExited(process), CloseKillTimeout, WaitRetryDelay); - } - - if (process.HasExited){ - process.Dispose(); - return true; - } - else{ - return false; - } - }catch(Exception ex) when (ex is InvalidOperationException || ex is Win32Exception){ - bool hasExited = CheckProcessExited(process); - process.Dispose(); - return hasExited; - } - } - - private static bool CheckProcessExited(Process process){ - process.Refresh(); - return process.HasExited; - } - } -} diff --git a/Management/LockManager.cs b/Management/LockManager.cs new file mode 100644 index 00000000..61b08c1d --- /dev/null +++ b/Management/LockManager.cs @@ -0,0 +1,129 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using TweetDuck.Dialogs; +using TweetDuck.Utils; +using TweetLib.Core.Systems.Startup; + +namespace TweetDuck.Management{ + sealed class LockManager{ + private const int WaitRetryDelay = 250; + private const int RestoreFailTimeout = 2000; + private const int CloseNaturallyTimeout = 10000; + private const int CloseKillTimeout = 5000; + + private readonly LockFile lockFile; + + public LockManager(string path){ + this.lockFile = new LockFile(path); + } + + public bool Lock(bool wasRestarted){ + return wasRestarted ? LaunchAfterRestart() : LaunchNormally(); + } + + public bool Unlock(){ + return lockFile.Unlock(); + } + + // Locking + + private bool LaunchNormally(){ + LockResult lockResult = lockFile.Lock(); + + if (lockResult is LockResult.HasProcess info){ + if (!RestoreProcess(info.Process) && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)){ + if (!CloseProcess(info.Process)){ + FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK); + return false; + } + + info.Dispose(); + lockResult = lockFile.Lock(); + } + else{ + return false; + } + } + + if (lockResult is LockResult.Fail fail){ + ShowGenericException(fail); + return false; + } + else if (lockResult != LockResult.Success){ + FormMessage.Error("TweetDuck Has Failed :(", "An unknown error occurred accessing the data folder. Please, make sure TweetDuck is not already running. If the problem persists, try restarting your system.", FormMessage.OK); + return false; + } + + return true; + } + + private bool LaunchAfterRestart(){ + LockResult lockResult = lockFile.LockWait(10000, WaitRetryDelay); + + while(lockResult != LockResult.Success){ + if (lockResult is LockResult.Fail fail){ + ShowGenericException(fail); + return false; + } + else if (!FormMessage.Warning("TweetDuck Cannot Restart", "TweetDuck is taking too long to close.", FormMessage.Retry, FormMessage.Exit)){ + return false; + } + + lockResult = lockFile.LockWait(5000, WaitRetryDelay); + } + + return true; + } + + // Helpers + + private static void ShowGenericException(LockResult.Fail fail){ + Program.Reporter.HandleException("TweetDuck Has Failed :(", "An unknown error occurred accessing the data folder. Please, make sure TweetDuck is not already running. If the problem persists, try restarting your system.", false, fail.Exception); + } + + private static bool RestoreProcess(Process process){ + if (process.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray + NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint)process.Id, 0); + + if (WindowsUtils.TrySleepUntil(() => CheckProcessExited(process) || (process.MainWindowHandle != IntPtr.Zero && process.Responding), RestoreFailTimeout, WaitRetryDelay)){ + return true; + } + } + + return false; + } + + private static bool CloseProcess(Process process){ + try{ + if (process.CloseMainWindow()){ + // ReSharper disable once AccessToDisposedClosure + WindowsUtils.TrySleepUntil(() => CheckProcessExited(process), CloseNaturallyTimeout, WaitRetryDelay); + } + + if (!process.HasExited){ + process.Kill(); + // ReSharper disable once AccessToDisposedClosure + WindowsUtils.TrySleepUntil(() => CheckProcessExited(process), CloseKillTimeout, WaitRetryDelay); + } + + if (process.HasExited){ + process.Dispose(); + return true; + } + else{ + return false; + } + }catch(Exception ex) when (ex is InvalidOperationException || ex is Win32Exception){ + bool hasExited = CheckProcessExited(process); + process.Dispose(); + return hasExited; + } + } + + private static bool CheckProcessExited(Process process){ + process.Refresh(); + return process.HasExited; + } + } +} diff --git a/Program.cs b/Program.cs index 26e8f438..226afb38 100644 --- a/Program.cs +++ b/Program.cs @@ -16,7 +16,6 @@ using TweetDuck.Utils; using TweetLib.Core; using TweetLib.Core.Collections; -using TweetLib.Core.Systems.Startup; using TweetLib.Core.Utils; using Win = System.Windows.Forms; @@ -72,7 +71,6 @@ static Program(){ Lib.Initialize(new App.Builder{ ErrorHandler = Reporter, - LockHandler = new LockHandler(), SystemHandler = new SystemHandler(), ResourceHandler = Resources }); @@ -95,42 +93,8 @@ private static void Main(){ return; } - if (Arguments.HasFlag(Arguments.ArgRestart)){ - LockManager.Result lockResult = LockManager.LockWait(10000); - - while(lockResult != LockManager.Result.Success){ - if (lockResult == LockManager.Result.Fail){ - FormMessage.Error("TweetDuck Has Failed :(", "An unknown error occurred accessing the data folder. Please, make sure TweetDuck is not already running. If the problem persists, try restarting your system.", FormMessage.OK); - return; - } - else if (!FormMessage.Warning("TweetDuck Cannot Restart", "TweetDuck is taking too long to close.", FormMessage.Retry, FormMessage.Exit)){ - return; - } - - lockResult = LockManager.LockWait(5000); - } - } - else{ - LockManager.Result lockResult = LockManager.Lock(); - - if (lockResult == LockManager.Result.HasProcess){ - if (!LockManager.RestoreLockingProcess() && FormMessage.Error("TweetDuck is Already Running", "Another instance of TweetDuck is already running.\nDo you want to close it?", FormMessage.Yes, FormMessage.No)){ - if (!LockManager.CloseLockingProcess()){ - FormMessage.Error("TweetDuck Has Failed :(", "Could not close the other process.", FormMessage.OK); - return; - } - - lockResult = LockManager.Lock(); - } - else{ - return; - } - } - - if (lockResult != LockManager.Result.Success){ - FormMessage.Error("TweetDuck Has Failed :(", "An unknown error occurred accessing the data folder. Please, make sure TweetDuck is not already running. If the problem persists, try restarting your system.", FormMessage.OK); - return; - } + if (!LockManager.Lock(Arguments.HasFlag(Arguments.ArgRestart))){ + return; } Config.LoadAll(); diff --git a/TweetDuck.csproj b/TweetDuck.csproj index 2a6b5750..f5105c85 100644 --- a/TweetDuck.csproj +++ b/TweetDuck.csproj @@ -53,7 +53,7 @@ <Reference Include="System.Windows.Forms" /> </ItemGroup> <ItemGroup> - <Compile Include="Application\LockHandler.cs" /> + <Compile Include="Management\LockManager.cs" /> <Compile Include="Application\SystemHandler.cs" /> <Compile Include="Browser\Adapters\CefScriptExecutor.cs" /> <Compile Include="Browser\Bridge\PropertyBridge.cs" /> @@ -436,4 +436,4 @@ IF EXIST "$(ProjectDir)bld\post_build.exe" ( </Target> <Import Project="packages\CefSharp.Common.81.3.100\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.81.3.100\build\CefSharp.Common.targets')" /> <Import Project="packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.81.3.100\build\CefSharp.WinForms.targets')" /> -</Project> +</Project> \ No newline at end of file diff --git a/lib/TweetLib.Core/App.cs b/lib/TweetLib.Core/App.cs index eacc6019..73499cfe 100644 --- a/lib/TweetLib.Core/App.cs +++ b/lib/TweetLib.Core/App.cs @@ -5,7 +5,6 @@ namespace TweetLib.Core{ public sealed class App{ #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. public static IAppErrorHandler ErrorHandler { get; private set; } - public static IAppLockHandler LockHandler { get; private set; } public static IAppSystemHandler SystemHandler { get; private set; } public static IAppResourceHandler ResourceHandler { get; private set; } #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. @@ -14,7 +13,6 @@ public sealed class App{ public sealed class Builder{ public IAppErrorHandler? ErrorHandler { get; set; } - public IAppLockHandler? LockHandler { get; set; } public IAppSystemHandler? SystemHandler { get; set; } public IAppResourceHandler? ResourceHandler { get; set; } @@ -22,7 +20,6 @@ public sealed class Builder{ internal void Initialize(){ App.ErrorHandler = Validate(ErrorHandler, nameof(ErrorHandler))!; - App.LockHandler = Validate(LockHandler, nameof(LockHandler))!; App.SystemHandler = Validate(SystemHandler, nameof(SystemHandler))!; App.ResourceHandler = Validate(ResourceHandler, nameof(ResourceHandler))!; } diff --git a/lib/TweetLib.Core/Application/IAppLockHandler.cs b/lib/TweetLib.Core/Application/IAppLockHandler.cs deleted file mode 100644 index 03bf89b8..00000000 --- a/lib/TweetLib.Core/Application/IAppLockHandler.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Diagnostics; - -namespace TweetLib.Core.Application{ - public interface IAppLockHandler{ - bool RestoreProcess(Process process); - bool CloseProcess(Process process); - } -} diff --git a/lib/TweetLib.Core/Systems/Startup/LockFile.cs b/lib/TweetLib.Core/Systems/Startup/LockFile.cs new file mode 100644 index 00000000..3da6ae43 --- /dev/null +++ b/lib/TweetLib.Core/Systems/Startup/LockFile.cs @@ -0,0 +1,125 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using TweetLib.Core.Utils; + +namespace TweetLib.Core.Systems.Startup{ + public sealed class LockFile{ + private static int CurrentProcessID{ + get{ + using Process me = Process.GetCurrentProcess(); + return me.Id; + } + } + + private readonly string path; + private FileStream? stream; + + public LockFile(string path){ + this.path = path; + } + + private void CreateLockFileStream(){ + stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); + stream.Write(BitConverter.GetBytes(CurrentProcessID), 0, sizeof(int)); + stream.Flush(true); + } + + private bool ReleaseLockFileStream(){ + if (stream != null){ + stream.Dispose(); + stream = null; + return true; + } + else{ + return false; + } + } + + public LockResult Lock(){ + if (stream != null){ + return LockResult.Success; + } + + try{ + CreateLockFileStream(); + return LockResult.Success; + }catch(DirectoryNotFoundException){ + try{ + FileUtils.CreateDirectoryForFile(path); + CreateLockFileStream(); + return LockResult.Success; + }catch(Exception e){ + ReleaseLockFileStream(); + return new LockResult.Fail(e); + } + }catch(IOException e){ + return DetermineLockingProcessOrFail(e); + }catch(Exception e){ + ReleaseLockFileStream(); + return new LockResult.Fail(e); + } + } + + public LockResult LockWait(int timeout, int retryDelay){ + for(int elapsed = 0; elapsed < timeout; elapsed += retryDelay){ + var result = Lock(); + + if (result is LockResult.HasProcess info){ + info.Dispose(); + Thread.Sleep(retryDelay); + } + else{ + return result; + } + } + + return Lock(); + } + + public bool Unlock(){ + if (ReleaseLockFileStream()){ + try{ + File.Delete(path); + }catch(Exception e){ + App.ErrorHandler.Log(e.ToString()); + return false; + } + } + + return true; + } + + private LockResult DetermineLockingProcessOrFail(Exception originalException){ + try{ + int pid; + + using(var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)){ + byte[] bytes = new byte[sizeof(int)]; + fileStream.Read(bytes, 0, bytes.Length); + pid = BitConverter.ToInt32(bytes, 0); + } + + try{ + var foundProcess = Process.GetProcessById(pid); + using var currentProcess = Process.GetCurrentProcess(); + + if (currentProcess.MainModule.FileVersionInfo.InternalName == foundProcess.MainModule.FileVersionInfo.InternalName){ + return new LockResult.HasProcess(foundProcess); + } + else{ + foundProcess.Close(); + } + }catch{ + // GetProcessById throws ArgumentException if the process is missing + // Process.MainModule can throw exceptions in some cases + } + + return new LockResult.Fail(originalException); + }catch(Exception e){ + return new LockResult.Fail(e); + } + } + } +} diff --git a/lib/TweetLib.Core/Systems/Startup/LockManager.cs b/lib/TweetLib.Core/Systems/Startup/LockManager.cs deleted file mode 100644 index 6bbe852f..00000000 --- a/lib/TweetLib.Core/Systems/Startup/LockManager.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Threading; - -namespace TweetLib.Core.Systems.Startup{ - public sealed class LockManager{ - private const int RetryDelay = 250; - - public enum Result{ - Success, HasProcess, Fail - } - - private readonly string file; - private FileStream? lockStream; - private Process? lockingProcess; - - public LockManager(string file){ - this.file = file; - } - - // Lock file - - private bool ReleaseLockFileStream(){ - if (lockStream != null){ - lockStream.Dispose(); - lockStream = null; - return true; - } - else{ - return false; - } - } - - private Result TryCreateLockFile(){ - void CreateLockFileStream(){ - lockStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read); - lockStream.Write(BitConverter.GetBytes(CurrentProcessID), 0, sizeof(int)); - lockStream.Flush(true); - } - - try{ - CreateLockFileStream(); - return Result.Success; - }catch(DirectoryNotFoundException){ - try{ - CreateLockFileStream(); - return Result.Success; - }catch{ - ReleaseLockFileStream(); - return Result.Fail; - } - }catch(IOException){ - return Result.HasProcess; - }catch{ - ReleaseLockFileStream(); - return Result.Fail; - } - } - - // Lock management - - public Result Lock(){ - if (lockStream != null){ - return Result.Success; - } - - Result initialResult = TryCreateLockFile(); - - if (initialResult == Result.HasProcess){ - try{ - int pid; - - using(FileStream fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)){ - byte[] bytes = new byte[sizeof(int)]; - fileStream.Read(bytes, 0, bytes.Length); - pid = BitConverter.ToInt32(bytes, 0); - } - - try{ - Process foundProcess = Process.GetProcessById(pid); - - if (MatchesCurrentProcess(foundProcess)){ - lockingProcess = foundProcess; - } - else{ - foundProcess.Close(); - } - }catch{ - // GetProcessById throws ArgumentException if the process is missing - // Process.MainModule can throw exceptions in some cases - } - - return lockingProcess == null ? Result.Fail : Result.HasProcess; - }catch{ - return Result.Fail; - } - } - - return initialResult; - } - - public Result LockWait(int timeout){ - for(int elapsed = 0; elapsed < timeout; elapsed += RetryDelay){ - Result result = Lock(); - - if (result == Result.HasProcess){ - Thread.Sleep(RetryDelay); - } - else{ - return result; - } - } - - return Lock(); - } - - public bool Unlock(){ - if (ReleaseLockFileStream()){ - try{ - File.Delete(file); - }catch(Exception e){ - App.ErrorHandler.Log(e.ToString()); - return false; - } - } - - return true; - } - - // Locking process - - public bool RestoreLockingProcess(){ - return lockingProcess != null && App.LockHandler.RestoreProcess(lockingProcess); - } - - public bool CloseLockingProcess(){ - if (lockingProcess != null && App.LockHandler.CloseProcess(lockingProcess)){ - lockingProcess = null; - return true; - } - - return false; - } - - // Utilities - - private static int CurrentProcessID{ - get{ - using Process me = Process.GetCurrentProcess(); - return me.Id; - } - } - - [SuppressMessage("ReSharper", "PossibleNullReferenceException")] - private static bool MatchesCurrentProcess(Process process){ - using Process current = Process.GetCurrentProcess(); - return current.MainModule.FileVersionInfo.InternalName == process.MainModule.FileVersionInfo.InternalName; - } - } -} diff --git a/lib/TweetLib.Core/Systems/Startup/LockResult.cs b/lib/TweetLib.Core/Systems/Startup/LockResult.cs new file mode 100644 index 00000000..7314cfa3 --- /dev/null +++ b/lib/TweetLib.Core/Systems/Startup/LockResult.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics; + +namespace TweetLib.Core.Systems.Startup{ + public class LockResult{ + private readonly string name; + + private LockResult(string name){ + this.name = name; + } + + public override string ToString(){ + return name; + } + + public static LockResult Success { get; } = new LockResult("Success"); + + public sealed class Fail : LockResult{ + public Exception Exception { get; } + + public Fail(Exception exception) : base("Fail"){ + this.Exception = exception; + } + } + + public sealed class HasProcess : LockResult, IDisposable{ + public Process Process { get; } + + public HasProcess(Process process) : base("HasProcess"){ + this.Process = process; + } + + public void Dispose(){ + Process.Dispose(); + } + } + } +}