diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 751702129..1de1f02e4 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -24,10 +24,15 @@ namespace Artemis.Core /// public static readonly string ExecutablePath = Utilities.GetCurrentLocation(); + /// + /// The base path for Artemis application data folder + /// + public static readonly string BaseFolder = Environment.GetFolderPath(OperatingSystem.IsWindows() ? Environment.SpecialFolder.ApplicationData : Environment.SpecialFolder.LocalApplicationData); + /// /// The full path to the Artemis data folder /// - public static readonly string DataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Artemis"); + public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); /// /// The full path to the Artemis logs folder diff --git a/src/Avalonia/Artemis.UI.Linux/App.axaml.cs b/src/Avalonia/Artemis.UI.Linux/App.axaml.cs index 9c4c5e695..4d79f7afc 100644 --- a/src/Avalonia/Artemis.UI.Linux/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Linux/App.axaml.cs @@ -2,14 +2,19 @@ using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.Threading; using ReactiveUI; +using Ninject; +using Avalonia.Controls.ApplicationLifetimes; namespace Artemis.UI.Linux { public class App : Application { + private StandardKernel? _kernel; + private ApplicationStateManager? _applicationStateManager; + public override void Initialize() { - ArtemisBootstrapper.Bootstrap(this); + _kernel = ArtemisBootstrapper.Bootstrap(this); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } @@ -17,6 +22,8 @@ namespace Artemis.UI.Linux public override void OnFrameworkInitializationCompleted() { ArtemisBootstrapper.Initialize(); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs b/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs new file mode 100644 index 000000000..75d346164 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs @@ -0,0 +1,141 @@ +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 Artemis.Core; +using Artemis.UI.Shared.Services.Interfaces; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; +using Ninject; + +namespace Artemis.UI.Linux +{ + public class ApplicationStateManager + { + private readonly IWindowService _windowService; + + // ReSharper disable once NotAccessedField.Local - Kept in scope to ensure it does not get released + private Mutex? _artemisMutex; + + public ApplicationStateManager(IKernel kernel, string[] startupArguments) + { + _windowService = kernel.Get(); + StartupArguments = startupArguments; + + Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; + Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; + + // On OS shutdown dispose the kernel just so device providers get a chance to clean up + if (Application.Current.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) + { + controlledApplicationLifetime.Exit += (_, _) => + { + RunForcedShutdownIfEnabled(); + kernel.Dispose(); + }; + } + } + + public string[] StartupArguments { get; } + + public bool FocusExistingInstance() + { + _artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535d", out bool createdNew); + if (createdNew) + return false; + + return RemoteFocus(); + } + + public void DisplayException(Exception e) + { + try + { + _windowService.ShowExceptionDialog("An unhandled exception occured", e); + } + catch + { + // ignored, we tried + } + } + + private bool 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"))) + { + KillOtherInstances(); + return false; + } + + string url = File.ReadAllText(Path.Combine(Constants.DataFolder, "webserver.txt")); + using HttpClient client = new(); + try + { + CancellationTokenSource cts = new(); + cts.CancelAfter(2000); + + HttpResponseMessage httpResponseMessage = client.Send(new HttpRequestMessage(HttpMethod.Post, url + "remote/bring-to-foreground"), cts.Token); + httpResponseMessage.EnsureSuccessStatusCode(); + return true; + } + catch (Exception) + { + KillOtherInstances(); + return false; + } + } + + private void KillOtherInstances() + { + // Kill everything else heh + List processes = Process.GetProcessesByName("Artemis.UI").Where(p => p.Id != Process.GetCurrentProcess().Id).ToList(); + foreach (Process process in processes) + { + try + { + process.Kill(true); + } + catch (Exception) + { + // ignored + } + } + } + + private void UtilitiesOnRestartRequested(object? sender, RestartEventArgs e) + { + List argsList = new(); + argsList.AddRange(StartupArguments); + if (e.ExtraArgs != null) + argsList.AddRange(e.ExtraArgs.Except(argsList)); + + //TODO: start new instance with correct arguments + + if (Application.Current.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) + Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown()); + } + + private void UtilitiesOnShutdownRequested(object? sender, EventArgs e) + { + RunForcedShutdownIfEnabled(); + + if (Application.Current.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) + Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown()); + } + + private void RunForcedShutdownIfEnabled() + { + if (StartupArguments.Contains("--disable-forced-shutdown")) + return; + + //todo + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj index 4e4e770d6..002253126 100644 --- a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj +++ b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 4571535af..0fc43dcba 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -53,6 +53,15 @@ "System.Reactive": "5.0.0" } }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Direct", + "requested": "[2.80.3, )", + "resolved": "2.80.3", + "contentHash": "LYl/mvEXrsKMdDNPVjA4ul8JDDGZI8DIkFE0a5GdhaC/aooxgwjuaXZ9NfPg4cJsRf8tb6VhGHvjSNUngNOcJw==", + "dependencies": { + "SkiaSharp": "2.80.3" + } + }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", "resolved": "2.1.0.2020091801", @@ -678,14 +687,6 @@ "SkiaSharp": "2.80.2" } }, - "SkiaSharp.NativeAssets.Linux": { - "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", - "dependencies": { - "SkiaSharp": "2.80.2" - } - }, "Splat": { "type": "Transitive", "resolved": "13.1.63", diff --git a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs index c1a74ec2a..ef5d53e15 100644 --- a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs @@ -1,4 +1,5 @@ using Artemis.Core.Ninject; +using Artemis.Core; using Artemis.UI.Exceptions; using Artemis.UI.Ninject; using Artemis.UI.Screens.Root; @@ -20,6 +21,8 @@ namespace Artemis.UI { if (_application != null || _kernel != null) throw new ArtemisUIException("UI already bootstrapped"); + + Utilities.PrepareFirstLaunch(); _application = application; _kernel = new StandardKernel();