From 5c2a96eee0ae23929976ecd1997536f4c5f65605 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 28 Jan 2021 19:14:58 +0100 Subject: [PATCH] Startup - Bring existing instances to foreground/focus them Bootstrapper - Cleaned up code into a separate state manager --- src/Artemis.UI/ApplicationStateManager.cs | 127 ++++++++++++++++++++++ src/Artemis.UI/Bootstrapper.cs | 85 ++------------- 2 files changed, 137 insertions(+), 75 deletions(-) create mode 100644 src/Artemis.UI/ApplicationStateManager.cs diff --git a/src/Artemis.UI/ApplicationStateManager.cs b/src/Artemis.UI/ApplicationStateManager.cs new file mode 100644 index 000000000..d07a0aa17 --- /dev/null +++ b/src/Artemis.UI/ApplicationStateManager.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using Artemis.Core; +using Artemis.UI.Utilities; +using Stylet; + +namespace Artemis.UI +{ + public class ApplicationStateManager + { + // ReSharper disable once NotAccessedField.Local - Kept in scope to ensure it does not get released + private Mutex _artemisMutex; + + public ApplicationStateManager(string[] startupArguments) + { + StartupArguments = startupArguments; + IsElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + + Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; + Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; + } + + public string[] StartupArguments { get; } + public bool IsElevated { get; } + + public bool FocusExistingInstance() + { + _artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535d", out bool createdNew); + if (createdNew) + return false; + + try + { + // Blocking is required here otherwise Artemis shuts down before the remote call gets a chance to finish + RemoteFocus().GetAwaiter().GetResult(); + } + catch (Exception) + { + // Not much could go wrong here but this code runs so early it'll crash if something does go wrong + return true; + } + + return true; + } + + private async Task RemoteFocus() + { + // At this point we cannot read the database yet to retrieve the web server port. + // Instead use the method external applications should use as well. + if (!File.Exists(Path.Combine(Constants.DataFolder, "webserver.txt"))) + return; + + string url = await File.ReadAllTextAsync(Path.Combine(Constants.DataFolder, "webserver.txt")); + using HttpClient client = new(); + await client.PostAsync(url + "api/remote/bring-to-foreground", null!); + } + + private void UtilitiesOnRestartRequested(object sender, RestartEventArgs e) + { + List argsList = new(); + argsList.AddRange(StartupArguments); + if (e.ExtraArgs != null) + argsList.AddRange(e.ExtraArgs.Except(argsList)); + string args = argsList.Any() ? "-ArgumentList " + string.Join(',', argsList) : ""; + string command = + $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; " + + "(Get-Process 'Artemis.UI').kill(); " + + $"Start-Process -FilePath '{Constants.ExecutablePath}' -WorkingDirectory '{Constants.ApplicationFolder}' {args}}}\""; + // Elevated always runs with RunAs + if (e.Elevate) + { + ProcessStartInfo info = new() + { + Arguments = command.Replace("}\"", " -Verb RunAs}\""), + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + } + // Non-elevated runs regularly if currently not elevated + else if (!IsElevated) + { + ProcessStartInfo info = new() + { + Arguments = command, + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + } + // Non-elevated runs via a utility method is currently elevated (de-elevating is hacky) + else + { + string powerShell = Path.Combine(Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "powershell.exe"); + ProcessUtilities.RunAsDesktopUser(powerShell, command, true); + } + + // Lets try a graceful shutdown, PowerShell will kill if needed + Execute.OnUIThread(() => Application.Current.Shutdown()); + } + + private void UtilitiesOnShutdownRequested(object sender, EventArgs e) + { + // Use PowerShell to kill the process after 2 sec just in case + ProcessStartInfo info = new() + { + Arguments = "-Command \"& {Start-Sleep -s 2; (Get-Process 'Artemis.UI').kill()}", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + + Execute.OnUIThread(() => Application.Current.Shutdown()); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index 0e66fd085..446cba8cb 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -1,16 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; -using System.Runtime.InteropServices; -using System.Security.Principal; using System.Threading.Tasks; using System.Windows; using System.Windows.Markup; using System.Windows.Threading; -using Artemis.Core; using Artemis.Core.Ninject; using Artemis.Core.Services; using Artemis.UI.Ninject; @@ -19,7 +14,6 @@ using Artemis.UI.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Stylet; -using Artemis.UI.Utilities; using Ninject; using Serilog; using Stylet; @@ -28,6 +22,7 @@ namespace Artemis.UI { public class Bootstrapper : NinjectBootstrapper { + private ApplicationStateManager _applicationStateManager; private ICoreService _core; public static List StartupArguments { get; private set; } @@ -41,14 +36,18 @@ namespace Artemis.UI protected override void Launch() { - // TODO: Move shutdown code out of bootstrapper - Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; - Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; + _applicationStateManager = new ApplicationStateManager(Args); Core.Utilities.PrepareFirstLaunch(); ILogger logger = Kernel.Get(); - IViewManager viewManager = Kernel.Get(); + if (_applicationStateManager.FocusExistingInstance()) + { + logger.Information("Shutting down because a different instance is already running."); + Application.Current.Shutdown(1); + return; + } + IViewManager viewManager = Kernel.Get(); StartupArguments = Args.ToList(); // Create the Artemis core @@ -78,7 +77,7 @@ namespace Artemis.UI try { _core.StartupArguments = StartupArguments; - _core.IsElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + _core.IsElevated = _applicationStateManager.IsElevated; _core.Initialize(); } catch (Exception e) @@ -125,67 +124,6 @@ namespace Artemis.UI e.Handled = true; } - private void UtilitiesOnShutdownRequested(object sender, EventArgs e) - { - // Use PowerShell to kill the process after 2 sec just in case - ProcessStartInfo info = new() - { - Arguments = "-Command \"& {Start-Sleep -s 2; (Get-Process 'Artemis.UI').kill()}", - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = "PowerShell.exe" - }; - Process.Start(info); - - Execute.OnUIThread(() => Application.Current.Shutdown()); - } - - private void UtilitiesOnRestartRequested(object sender, RestartEventArgs e) - { - List argsList = new(); - argsList.AddRange(Args); - if (e.ExtraArgs != null) - argsList.AddRange(e.ExtraArgs.Except(argsList)); - string args = argsList.Any() ? "-ArgumentList " + string.Join(',', argsList) : ""; - string command = - $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; " + - $"(Get-Process 'Artemis.UI').kill(); " + - $"Start-Process -FilePath '{Constants.ExecutablePath}' -WorkingDirectory '{Constants.ApplicationFolder}' {args}}}\""; - // Elevated always runs with RunAs - if (e.Elevate) - { - ProcessStartInfo info = new() - { - Arguments = command.Replace("}\"", " -Verb RunAs}\""), - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = "PowerShell.exe" - }; - Process.Start(info); - } - // Non-elevated runs regularly if currently not elevated - else if (!_core.IsElevated) - { - ProcessStartInfo info = new() - { - Arguments = command, - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = "PowerShell.exe" - }; - Process.Start(info); - } - // Non-elevated runs via a utility method is currently elevated (de-elevating is hacky) - else - { - string powerShell = Path.Combine(Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "powershell.exe"); - ProcessUtilities.RunAsDesktopUser(powerShell, command, true); - } - - // Lets try a graceful shutdown, PowerShell will kill if needed - Execute.OnUIThread(() => Application.Current.Shutdown()); - } - private void HandleFatalException(Exception e, ILogger logger) { logger.Fatal(e, "Fatal exception during initialization, shutting down."); @@ -201,8 +139,5 @@ namespace Artemis.UI Environment.Exit(1); }); } - - [DllImport("user32.dll")] - private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); } } \ No newline at end of file