From 9ab935eb3399999aa21e471dc7c2ac1b76b62c24 Mon Sep 17 00:00:00 2001 From: Ashleigh Adams Date: Wed, 1 Oct 2025 10:03:53 +0100 Subject: [PATCH 1/2] Only allow a single instance at a time --- src/Keyden.App/App.axaml.cs | 108 +++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/src/Keyden.App/App.axaml.cs b/src/Keyden.App/App.axaml.cs index 145dc83..3daae28 100644 --- a/src/Keyden.App/App.axaml.cs +++ b/src/Keyden.App/App.axaml.cs @@ -7,11 +7,16 @@ 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; namespace Keyden; @@ -55,8 +60,100 @@ public static T GetKeyedService(object? key) where private SshAgent? Agent { get; set; } private KeydenSettings? Settings { get; set; } + public static string AppPipePath + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return "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) + { + using var pipeServer = new NamedPipeServerStream( + pipeName: AppPipePath, + direction: PipeDirection.InOut, + maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous | PipeOptions.WriteThrough | PipeOptions.CurrentUserOnly, + inBufferSize: 1, + outBufferSize: 1); + + while (!ct.IsCancellationRequested) + { + try + { + await pipeServer.WaitForConnectionAsync(ct); + var byteRead = pipeServer.ReadByte(); + if (byteRead == 0x0A) + { + ShowMainWindow(); + pipeServer.WriteByte(0xA0); + pipeServer.Flush(); + } + } + catch (OperationCanceledException) { } + catch (IOException) { } + finally + { + if (pipeServer.IsConnected) + pipeServer.Disconnect(); + } + } + } + + private CancellationTokenSource AppPipeCts = new(); + public override void OnFrameworkInitializationCompleted() { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime) + { + try + { + using var client = new NamedPipeClientStream(pipeName: AppPipePath); + if (client.IsConnected) + { + client.ReadTimeout = 1000; + client.WriteTimeout = 1000; + client.WriteByte(0x0A); + client.Flush(); + if (client.ReadByte() == 0xA0) + Environment.Exit(0); + } + } + catch (IOException) { } + catch (TimeoutException) { } + } + var collection = new ServiceCollection(); collection.AddSingleton(SystemServices); @@ -70,13 +167,15 @@ 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(); @@ -84,6 +183,11 @@ public override void OnFrameworkInitializationCompleted() base.OnFrameworkInitializationCompleted(); } + private void DesktopAppExit(object? sender, ControlledApplicationLifetimeExitEventArgs e) + { + AppPipeCts.Cancel(); + } + private static SettingsWindow? SettingsWindow { get; set; } public static void ShowSettingsWindow() { From aeb19049e97facdcfd88de112be283e7b05a1715 Mon Sep 17 00:00:00 2001 From: Ashleigh Adams Date: Thu, 2 Oct 2025 00:15:08 +0100 Subject: [PATCH 2/2] Open only a single instance of the application --- src/Keyden.App/AgentK.cs | 10 ++++ src/Keyden.App/App.axaml.cs | 85 ++++++++++++++++++++---------- src/Keyden.Core/ISystemServices.cs | 3 +- 3 files changed, 68 insertions(+), 30 deletions(-) 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 3daae28..1745e5d 100644 --- a/src/Keyden.App/App.axaml.cs +++ b/src/Keyden.App/App.axaml.cs @@ -17,6 +17,7 @@ using System.Runtime.InteropServices; using System.Reflection.Metadata; using System.Threading; +using System.Threading.Tasks; namespace Keyden; @@ -64,8 +65,10 @@ public static string AppPipePath { get { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return "keyden"; + if (false&&RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return $"{Environment.UserName}/keyden"; + } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { @@ -99,51 +102,67 @@ public static string AppPipePath private async void MainAppPipeThread(CancellationToken ct) { - using var pipeServer = new NamedPipeServerStream( - pipeName: AppPipePath, - direction: PipeDirection.InOut, - maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances, - transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.Asynchronous | PipeOptions.WriteThrough | PipeOptions.CurrentUserOnly, - inBufferSize: 1, - outBufferSize: 1); - - while (!ct.IsCancellationRequested) + var agentK = GetService(); + try { - 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) { - await pipeServer.WaitForConnectionAsync(ct); - var byteRead = pipeServer.ReadByte(); - if (byteRead == 0x0A) + try { - ShowMainWindow(); - pipeServer.WriteByte(0xA0); - pipeServer.Flush(); + 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 (OperationCanceledException) { } - catch (IOException) { } - 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.ReadTimeout = 1000; - client.WriteTimeout = 1000; client.WriteByte(0x0A); client.Flush(); if (client.ReadByte() == 0xA0) @@ -152,6 +171,10 @@ public override void OnFrameworkInitializationCompleted() } catch (IOException) { } catch (TimeoutException) { } + catch (Exception ex) + { + singleInstanceEx = ex; + } } var collection = new ServiceCollection(); @@ -181,6 +204,12 @@ public override void OnFrameworkInitializationCompleted() 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) 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,