diff --git a/CHANGELOG.md b/CHANGELOG.md index 5831353..2c32c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.8] - 2026-05-04 + +### Fixed +- Single-instance protection: launching PassKey when it is already running in the system tray + now brings the existing window to the foreground instead of silently starting (and immediately + hiding) a second invisible process. This was the primary cause of the "app installs but + doesn't start" symptom reported on v1.0.7. +- Startup crash log: if the application fails to initialise before the main window is created, + a diagnostic file is written to `%LOCALAPPDATA%\PassKey\startup-crash.log`. This makes + silent startup failures diagnosable without attaching a debugger. + ## [1.0.7] - 2026-05-02 ### Fixed @@ -117,7 +128,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Giuseppe Imperato** — concept, design, product decisions - **[Claude](https://www.anthropic.com/claude) by Anthropic** — architecture, implementation, documentation -[Unreleased]: https://github.com/pexatar/PassKey/compare/v1.0.7...HEAD +[Unreleased]: https://github.com/pexatar/PassKey/compare/v1.0.8...HEAD +[1.0.8]: https://github.com/pexatar/PassKey/compare/v1.0.7...v1.0.8 [1.0.7]: https://github.com/pexatar/PassKey/compare/v1.0.6...v1.0.7 [1.0.6]: https://github.com/pexatar/PassKey/compare/v1.0.5...v1.0.6 [1.0.5]: https://github.com/pexatar/PassKey/compare/v1.0.0...v1.0.5 diff --git a/Installer/PassKey.iss b/Installer/PassKey.iss index 4436142..6663869 100644 --- a/Installer/PassKey.iss +++ b/Installer/PassKey.iss @@ -4,8 +4,8 @@ [Setup] AppId={{A7F3C2D1-8E4B-4F9A-B6D5-3C1E7A2F0D84} AppName=PassKey -AppVersion=1.0.7 -AppVerName=PassKey 1.0.7 +AppVersion=1.0.8 +AppVerName=PassKey 1.0.8 AppPublisher=Giuseppe Imperato AppPublisherURL=https://github.com/pexatar/PassKey AppSupportURL=https://github.com/pexatar/PassKey/issues diff --git a/README.md b/README.md index 2967e84..784dcc4 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ PassKey stores your passwords, credit cards, identities, and secure notes in a l | Version | Platform | Type | |---------|----------|------| -| [v1.0.7](https://github.com/pexatar/PassKey/releases/tag/v1.0.7) | Windows x64 | Installer EXE | -| [v1.0.7](https://github.com/pexatar/PassKey/releases/tag/v1.0.7) | Windows x64 | Portable ZIP | +| [v1.0.8](https://github.com/pexatar/PassKey/releases/tag/v1.0.8) | Windows x64 | Installer EXE | +| [v1.0.8](https://github.com/pexatar/PassKey/releases/tag/v1.0.8) | Windows x64 | Portable ZIP | > **Requirements:** Windows 10 version 1809 (build 17763) or later, x64 processor. > No .NET runtime required — PassKey is fully self-contained. diff --git a/src/PassKey.Desktop/PassKey.Desktop.csproj b/src/PassKey.Desktop/PassKey.Desktop.csproj index eab9afb..0ed73e7 100644 --- a/src/PassKey.Desktop/PassKey.Desktop.csproj +++ b/src/PassKey.Desktop/PassKey.Desktop.csproj @@ -15,9 +15,9 @@ true false PassKey.Desktop - 1.0.7 - 1.0.7.0 - 1.0.7.0 + 1.0.8 + 1.0.8.0 + 1.0.8.0 app.manifest Assets\PassKey.ico DISABLE_XAML_GENERATED_MAIN diff --git a/src/PassKey.Desktop/Program.cs b/src/PassKey.Desktop/Program.cs index 54c826b..8882d9f 100644 --- a/src/PassKey.Desktop/Program.cs +++ b/src/PassKey.Desktop/Program.cs @@ -1,3 +1,4 @@ +using System.Reflection; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; @@ -5,18 +6,116 @@ namespace PassKey.Desktop; public static class Program { + // Named event used for single-instance detection and window-restore signalling. + // AutoReset: each Set() wakes exactly one WaitOne() call. + private const string ShowEventName = "PassKey.Desktop.ShowWindow"; + + // Path to the startup crash log — written only when Application.Start() throws. + private static readonly string CrashLogPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "PassKey", "startup-crash.log"); + [STAThread] public static void Main(string[] args) { - WinRT.ComWrappersSupport.InitializeComWrappers(); + // ── Single-instance ────────────────────────────────────────────────── + // If another instance is already running, signal it to restore its window + // and exit immediately. This prevents the "app doesn't appear to start" + // symptom when PassKey is already running minimised in the system tray. + + EventWaitHandle? showEvent = null; + bool isFirstInstance = true; + + try + { + showEvent = new EventWaitHandle( + initialState: false, + mode: EventResetMode.AutoReset, + name: ShowEventName, + createdNew: out isFirstInstance); + } + catch + { + // Named-event creation failed (unusual system configuration) — + // treat as first instance so the app always starts. + } + if (!isFirstInstance) + { + try { showEvent?.Set(); } catch { } + showEvent?.Dispose(); + return; + } + + // Start a daemon thread that listens for subsequent launch attempts and + // restores the main window via the UI dispatcher whenever signalled. + if (showEvent != null) + { + new Thread(() => MonitorShowEvent(showEvent)) + { + IsBackground = true, + Name = "PassKey.SingleInstance.Monitor" + }.Start(); + } + + // ── App startup ────────────────────────────────────────────────────── // Note: ApplySavedLanguage() is called inside App() constructor before // InitializeComponent(), following the official Microsoft docs pattern. - Application.Start(p => + try + { + WinRT.ComWrappersSupport.InitializeComWrappers(); + + Application.Start(p => + { + var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + _ = new App(); + }); + } + catch (Exception ex) + { + // Write a crash log so users can share it for support. + // The log is only created on an actual startup failure. + WriteCrashLog(ex); + throw; + } + } + + /// + /// Background loop: waits for the named event to be signalled (a subsequent + /// launch attempt), then marshals a call + /// onto the UI thread so the window becomes visible again. + /// + private static void MonitorShowEvent(EventWaitHandle showEvent) + { + while (true) + { + try + { + showEvent.WaitOne(); + App.MainWindow?.DispatcherQueue.TryEnqueue(() => App.MainWindow?.RestoreWindow()); + } + catch + { + // Silently ignore — this is a non-critical background thread. + } + } + } + + private static void WriteCrashLog(Exception ex) + { + try + { + var logDir = Path.GetDirectoryName(CrashLogPath)!; + Directory.CreateDirectory(logDir); + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + File.AppendAllText( + CrashLogPath, + $"[{DateTime.Now:s}] PassKey v{version} startup crash:{Environment.NewLine}{ex}{Environment.NewLine}{Environment.NewLine}"); + } + catch { - var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); - SynchronizationContext.SetSynchronizationContext(context); - _ = new App(); - }); + // If we can't write the log, there is nothing more we can do. + } } }