diff --git a/src/PersonalToolbox/App.xaml b/src/PersonalToolbox/App.xaml index b2c7b87..bc0452e 100644 --- a/src/PersonalToolbox/App.xaml +++ b/src/PersonalToolbox/App.xaml @@ -1,14 +1,14 @@ - + xmlns:views="clr-namespace:PersonalToolbox.Views"> + @@ -16,23 +16,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PersonalToolbox/App.xaml.cs b/src/PersonalToolbox/App.xaml.cs index a85a807..b971cca 100644 --- a/src/PersonalToolbox/App.xaml.cs +++ b/src/PersonalToolbox/App.xaml.cs @@ -1,6 +1,10 @@ using System.Configuration; using System.Data; +using System.IO; +using System.IO.Pipes; +using System.Text; using System.Windows; +using System.Threading; namespace PersonalToolbox; @@ -9,5 +13,136 @@ namespace PersonalToolbox; /// public partial class App : System.Windows.Application { + private const string ActivatePipeName = "PersonalToolbox.Activate"; + + private FileStream? _instanceLockFile; + private CancellationTokenSource? _activationPipeCancellation; + private MainWindow? _mainWindow; + + protected override async void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + ShutdownMode = ShutdownMode.OnExplicitShutdown; + + if (!TryAcquireInstanceLock()) + { + SignalExistingInstance(); + Environment.Exit(0); + return; + } + + var mainWindow = new MainWindow(); + _mainWindow = mainWindow; + MainWindow = mainWindow; + StartActivationPipe(); + + await mainWindow.InitializeShellAsync(); + if (!mainWindow.StartHiddenToTray) + { + mainWindow.ShowMainWindow(); + } + else + { + mainWindow.RefreshTrayMenu(); + } + + await mainWindow.RunStartupTasksAsync(); + } + + protected override void OnExit(ExitEventArgs e) + { + _activationPipeCancellation?.Cancel(); + _activationPipeCancellation?.Dispose(); + _instanceLockFile?.Dispose(); + base.OnExit(e); + } + + private bool TryAcquireInstanceLock() + { + try + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var configDirectory = Path.Combine(appData, "PersonalToolbox"); + Directory.CreateDirectory(configDirectory); + var lockPath = Path.Combine(configDirectory, "PersonalToolbox.lock"); + + _instanceLockFile = new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + _instanceLockFile.SetLength(0); + var processId = Encoding.UTF8.GetBytes(Environment.ProcessId.ToString()); + _instanceLockFile.Write(processId, 0, processId.Length); + _instanceLockFile.Flush(); + return true; + } + catch (IOException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + } + + private static void SignalExistingInstance() + { + for (var attempt = 0; attempt < 3; attempt++) + { + try + { + using var pipe = new NamedPipeClientStream(".", ActivatePipeName, PipeDirection.Out); + pipe.Connect(800); + using var writer = new StreamWriter(pipe, Encoding.UTF8) + { + AutoFlush = true + }; + writer.WriteLine("show"); + return; + } + catch (IOException) + { + } + catch (TimeoutException) + { + } + + Thread.Sleep(150); + } + } + + private void StartActivationPipe() + { + _activationPipeCancellation = new CancellationTokenSource(); + _ = Task.Run(() => ListenForActivationAsync(_activationPipeCancellation.Token)); + } + + private async Task ListenForActivationAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await using var pipe = new NamedPipeServerStream( + ActivatePipeName, + PipeDirection.In, + 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous); + + await pipe.WaitForConnectionAsync(cancellationToken); + using var reader = new StreamReader(pipe, Encoding.UTF8); + _ = await reader.ReadLineAsync(cancellationToken); + + await Dispatcher.BeginInvoke(() => _mainWindow?.ShowMainWindow()); + } + catch (OperationCanceledException) + { + return; + } + catch (IOException) + { + } + } + } } diff --git a/src/PersonalToolbox/MainWindow.xaml b/src/PersonalToolbox/MainWindow.xaml index 184e0ab..3c062ad 100644 --- a/src/PersonalToolbox/MainWindow.xaml +++ b/src/PersonalToolbox/MainWindow.xaml @@ -1,4 +1,4 @@ - @@ -20,8 +19,8 @@ - @@ -78,8 +77,8 @@ - @@ -93,7 +92,7 @@ + FontFamily="Segoe MDL2 Assets" + FontSize="16" + Foreground="{DynamicResource PrimaryBrush}" /> @@ -179,12 +178,23 @@ + + + - - - + + + + + + +