diff --git a/src/Keyden.App/AgentK.cs b/src/Keyden.App/AgentK.cs index f24a8f5..0800e80 100644 --- a/src/Keyden.App/AgentK.cs +++ b/src/Keyden.App/AgentK.cs @@ -386,6 +386,16 @@ private class KeyInfo } private readonly Dictionary KeyInfos = new(); + public void AddActivity(string title, string description, string icon = "fa-circle-info", ActivityImportance importance = ActivityImportance.Normal) + { + NewActivity?.Invoke(new ActivityItem() + { + Title = title, + Description = description, + Icon = icon, + Importance = importance, + }); + } private AuthRequired QueryAuth(SshKey key, SshKeyOptions options, ClientInfo clientInfo) { diff --git a/src/Keyden.App/App.axaml.cs b/src/Keyden.App/App.axaml.cs index 145dc83..1745e5d 100644 --- a/src/Keyden.App/App.axaml.cs +++ b/src/Keyden.App/App.axaml.cs @@ -7,11 +7,17 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; -using Keyden.ViewModels; using Keyden.Views; using Avalonia.Controls; using System.Text.Json; using System.Text; +using System.IO.Pipes; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Reflection.Metadata; +using System.Threading; +using System.Threading.Tasks; namespace Keyden; @@ -55,8 +61,122 @@ public static T GetKeyedService(object? key) where private SshAgent? Agent { get; set; } private KeydenSettings? Settings { get; set; } + public static string AppPipePath + { + get + { + if (false&&RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return $"{Environment.UserName}/keyden"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var uid = Unix.Getuid().ToString(CultureInfo.InvariantCulture); + var socketDirectory = $"/run/user/{uid}"; + + // check for legacy linux systems + if (!Directory.Exists(socketDirectory)) + socketDirectory = Directory.Exists("/run") ? "/run" : "/var/run"; + + if (!Directory.Exists(socketDirectory)) + return "keyden"; + + socketDirectory += "/keyden"; + var socketPath = $"{socketDirectory}/keyden.sock"; + + if (!Directory.Exists(socketDirectory)) + Directory.CreateDirectory(socketDirectory); + return socketPath; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var uid = Unix.Getuid().ToString(CultureInfo.InvariantCulture); + return $"/var/run/{uid}-keyden.sock"; + } + + return "keyden"; + } + } + + private async void MainAppPipeThread(CancellationToken ct) + { + var agentK = GetService(); + try + { + using var pipeServer = new NamedPipeServerStream( + pipeName: AppPipePath, + direction: PipeDirection.InOut, + maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous | PipeOptions.WriteThrough | PipeOptions.CurrentUserOnly | PipeOptions.FirstPipeInstance, + inBufferSize: 1, + outBufferSize: 1); + + + while (!ct.IsCancellationRequested) + { + try + { + await pipeServer.WaitForConnectionAsync(ct); + var byteRead = pipeServer.ReadByte(); + if (byteRead == 0x0A) + { + agentK.AddActivity("Keyden opened", "Another Keyden instance was opened, showing main window instead."); + + ShowMainWindow(); + pipeServer.WriteByte(0xA0); + pipeServer.Flush(); + await Task.Delay(100); + } + } + catch (OperationCanceledException) { } + catch (IOException) { } + catch (Exception ex) + { + agentK.AddActivity("Error in single instance host", ex.ToString(), "fa-circle-exclamation", ViewModels.ActivityImportance.Warning); + } + finally + { + if (pipeServer.IsConnected) + pipeServer.Disconnect(); + } + } + } + catch (Exception ex) + { + agentK.AddActivity("Critical error in single instance host", ex.ToString(), "fa-circle-exclamation", ViewModels.ActivityImportance.Critical); + } + } + + private CancellationTokenSource AppPipeCts = new(); + public override void OnFrameworkInitializationCompleted() { + Exception? singleInstanceEx = null; + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime) + { + try + { + using var client = new NamedPipeClientStream(pipeName: AppPipePath); + client.Connect(100); + if (client.IsConnected) + { + client.WriteByte(0x0A); + client.Flush(); + if (client.ReadByte() == 0xA0) + Environment.Exit(0); + } + } + catch (IOException) { } + catch (TimeoutException) { } + catch (Exception ex) + { + singleInstanceEx = ex; + } + } + var collection = new ServiceCollection(); collection.AddSingleton(SystemServices); @@ -70,18 +190,31 @@ public override void OnFrameworkInitializationCompleted() if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown; - bool isAutomaticStart = SystemServices.IsAutomaticStart || desktop.Args?.Contains("--hide") == true; if (!isAutomaticStart) ShowMainWindow(); + + MainAppPipeThread(AppPipeCts.Token); + desktop.Exit += DesktopAppExit; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) singleViewPlatform.MainView = new MainView(); base.OnFrameworkInitializationCompleted(); + + if (singleInstanceEx is not null) + { + var agentK = GetService(); + agentK.AddActivity("Error in single instance client", singleInstanceEx.ToString(), "fa-circle-exclamation", ViewModels.ActivityImportance.Critical); + } + } + + private void DesktopAppExit(object? sender, ControlledApplicationLifetimeExitEventArgs e) + { + AppPipeCts.Cancel(); } private static SettingsWindow? SettingsWindow { get; set; } diff --git a/src/Keyden.Core/ISystemServices.cs b/src/Keyden.Core/ISystemServices.cs index 8e8e601..5f03b2b 100644 --- a/src/Keyden.Core/ISystemServices.cs +++ b/src/Keyden.Core/ISystemServices.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO.Pipes; using System.Threading; using System.Threading.Tasks; @@ -13,7 +12,7 @@ public interface ISystemServices bool IsAutomaticStart { get; } TimeSpan UserIdleDuration { get; } - public event EventHandler MachineLocked; + event EventHandler MachineLocked; Task TryAuthUser( AuthRequired authRequired,