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();