|
26 | 26 |
|
27 | 27 | var builder = WebApplication.CreateBuilder(args); |
28 | 28 |
|
29 | | -// CRITICAL: Handle .restore_pending BEFORE any DbContext registration |
30 | | -// This ensures encrypted database detection happens on the correct file |
31 | | -HandlePendingRestore(builder.Configuration); |
32 | | - |
33 | | -// Configure for Electron |
| 29 | +// Configure for Electron FIRST — this sets HybridSupport.IsElectronActive, which |
| 30 | +// HandlePendingRestore depends on to compute the correct Electron user-data DB path. |
34 | 31 | builder.WebHost.UseElectron(args); |
35 | 32 |
|
| 33 | +// CRITICAL: Handle .restore_pending BEFORE any DbContext registration. |
| 34 | +// Must run AFTER UseElectron so HybridSupport.IsElectronActive is true. |
| 35 | +HandlePendingRestore(builder.Configuration); |
| 36 | + |
36 | 37 | // Configure URLs - use specific port for Electron |
37 | 38 | if (HybridSupport.IsElectronActive) |
38 | 39 | { |
|
777 | 778 | // Local function to handle .restore_pending before service registration |
778 | 779 | static void HandlePendingRestore(IConfiguration configuration) |
779 | 780 | { |
780 | | - var connectionString = configuration.GetConnectionString("DefaultConnection"); |
781 | | - Console.WriteLine($"[Program.HandlePendingRestore] Checking for staged restore on database connection string: {connectionString}"); |
782 | | - if (string.IsNullOrEmpty(connectionString)) |
| 781 | + // CRITICAL: This runs before ANY service registration or DbContext creation. |
| 782 | + // It must compute the correct database path independently of the DI container. |
| 783 | + // |
| 784 | + // BUG FIXED: Previously this read the path from appsettings.json DefaultConnection, |
| 785 | + // which is a fallback path (e.g. Infrastructure/Data/app_v0.0.0.db) that is NEVER |
| 786 | + // used by the Electron app. The Electron app stores its database in the OS user-data |
| 787 | + // directory (e.g. %APPDATA%\Aquiis\app_v1.1.0.db on Windows). This mismatch meant |
| 788 | + // the staged restore was never found and never applied, leaving the app in a broken |
| 789 | + // state after an encrypt→decrypt cycle (the DPAPI key is deleted on successful |
| 790 | + // decryption, so on the next startup the app saw an encrypted DB with no key and |
| 791 | + // displayed the unlock screen indefinitely). |
| 792 | + |
| 793 | + string dbPath; |
| 794 | + |
| 795 | + if (HybridSupport.IsElectronActive) |
783 | 796 | { |
784 | | - // Can't proceed without connection string |
785 | | - return; |
| 797 | + // Replicate ElectronPathService.GetDatabasePathSync() without needing DI. |
| 798 | + var dbFileName = configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; |
| 799 | + |
| 800 | + string basePath; |
| 801 | + if (OperatingSystem.IsWindows()) |
| 802 | + basePath = Path.Combine( |
| 803 | + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Aquiis"); |
| 804 | + else if (OperatingSystem.IsMacOS()) |
| 805 | + basePath = Path.Combine( |
| 806 | + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), |
| 807 | + "Library", "Application Support", "Aquiis"); |
| 808 | + else // Linux |
| 809 | + basePath = Path.Combine( |
| 810 | + Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") |
| 811 | + ?? Path.Combine( |
| 812 | + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"), |
| 813 | + "Aquiis"); |
| 814 | + |
| 815 | + Directory.CreateDirectory(basePath); |
| 816 | + dbPath = Path.Combine(basePath, dbFileName); |
| 817 | + Console.WriteLine($"[Program.HandlePendingRestore] Electron mode — DB path: {dbPath}"); |
786 | 818 | } |
787 | | - |
788 | | - // Extract database path |
789 | | - var builder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder(connectionString); |
790 | | - var dbPath = builder.DataSource; |
791 | | - |
792 | | - if (!Path.IsPathRooted(dbPath)) |
| 819 | + else |
793 | 820 | { |
794 | | - dbPath = Path.Combine(Directory.GetCurrentDirectory(), dbPath); |
| 821 | + // Web / non-Electron: derive path from appsettings.json connection string. |
| 822 | + var connectionString = configuration.GetConnectionString("DefaultConnection"); |
| 823 | + Console.WriteLine($"[Program.HandlePendingRestore] Web mode — connection string: {connectionString}"); |
| 824 | + |
| 825 | + if (string.IsNullOrEmpty(connectionString)) |
| 826 | + { |
| 827 | + Console.WriteLine("[Program.HandlePendingRestore] No connection string found, skipping"); |
| 828 | + return; |
| 829 | + } |
| 830 | + |
| 831 | + var csBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder(connectionString); |
| 832 | + dbPath = csBuilder.DataSource; |
| 833 | + |
| 834 | + if (!Path.IsPathRooted(dbPath)) |
| 835 | + dbPath = Path.Combine(Directory.GetCurrentDirectory(), dbPath); |
| 836 | + |
| 837 | + Console.WriteLine($"[Program.HandlePendingRestore] Web mode — DB path: {dbPath}"); |
795 | 838 | } |
796 | | - |
| 839 | + |
797 | 840 | var stagedRestorePath = $"{dbPath}.restore_pending"; |
798 | | - |
799 | | - // Check if there's a staged restore waiting |
800 | | - if (File.Exists(stagedRestorePath)) |
| 841 | + |
| 842 | + if (!File.Exists(stagedRestorePath)) |
801 | 843 | { |
802 | | - Console.WriteLine($"[Program.HandlePendingRestore] Found staged restore file, applying it now: {stagedRestorePath}"); |
803 | | - |
804 | | - // Clear SQLite connection pool |
805 | | - Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); |
806 | | - |
807 | | - // Wait for connections to close |
808 | | - Thread.Sleep(500); |
809 | | - |
810 | | - // Backup current database if it exists |
811 | | - if (File.Exists(dbPath)) |
| 844 | + Console.WriteLine($"[Program.HandlePendingRestore] No staged restore pending at: {stagedRestorePath}"); |
| 845 | + return; |
| 846 | + } |
| 847 | + |
| 848 | + var pendingSize = new FileInfo(stagedRestorePath).Length; |
| 849 | + Console.WriteLine($"[Program.HandlePendingRestore] Staged restore found: {stagedRestorePath} ({pendingSize:N0} bytes)"); |
| 850 | + |
| 851 | + // At this point in startup no connections have been opened, but clear pools |
| 852 | + // as a safety measure and force a GC pass on Windows to release any lingering |
| 853 | + // SQLite native file handles from a previous process that didn't exit cleanly. |
| 854 | + Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); |
| 855 | + |
| 856 | + if (OperatingSystem.IsWindows()) |
| 857 | + { |
| 858 | + GC.Collect(); |
| 859 | + GC.WaitForPendingFinalizers(); |
| 860 | + GC.Collect(); |
| 861 | + Console.WriteLine("[Program.HandlePendingRestore] GC collection complete (Windows handle safety)"); |
| 862 | + } |
| 863 | + |
| 864 | + Thread.Sleep(300); |
| 865 | + |
| 866 | + if (File.Exists(dbPath)) |
| 867 | + { |
| 868 | + var currentSize = new FileInfo(dbPath).Length; |
| 869 | + Console.WriteLine($"[Program.HandlePendingRestore] Current DB: {dbPath} ({currentSize:N0} bytes) — backing up before replace"); |
| 870 | + |
| 871 | + // Remove WAL/SHM files — they belong to the outgoing session |
| 872 | + var walPath = $"{dbPath}-wal"; |
| 873 | + var shmPath = $"{dbPath}-shm"; |
| 874 | + if (File.Exists(walPath)) { File.Delete(walPath); Console.WriteLine("[Program.HandlePendingRestore] Deleted WAL file"); } |
| 875 | + if (File.Exists(shmPath)) { File.Delete(shmPath); Console.WriteLine("[Program.HandlePendingRestore] Deleted SHM file"); } |
| 876 | + |
| 877 | + var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); |
| 878 | + var beforeRestorePath = $"{dbPath}.beforeRestore.{timestamp}"; |
| 879 | + |
| 880 | + try |
812 | 881 | { |
813 | | - var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); |
814 | | - var beforeRestorePath = $"{dbPath}.beforeRestore.{timestamp}"; |
815 | 882 | File.Move(dbPath, beforeRestorePath); |
816 | | - Console.WriteLine($"Current database backed up to: {beforeRestorePath}"); |
| 883 | + Console.WriteLine($"[Program.HandlePendingRestore] Current DB backed up to: {beforeRestorePath}"); |
817 | 884 | } |
818 | | - |
819 | | - // Move staged restore into place |
| 885 | + catch (Exception ex) |
| 886 | + { |
| 887 | + Console.WriteLine($"[Program.HandlePendingRestore] ERROR: Cannot back up current DB — restore aborted."); |
| 888 | + Console.WriteLine($"[Program.HandlePendingRestore] {ex.GetType().Name}: {ex.Message}"); |
| 889 | + if (OperatingSystem.IsWindows()) |
| 890 | + Console.WriteLine($"[Program.HandlePendingRestore] HResult: 0x{ex.HResult:X8} " + |
| 891 | + "(0x80070020 = sharing violation / file locked by another process)"); |
| 892 | + Console.WriteLine("[Program.HandlePendingRestore] Staged file preserved for next startup attempt."); |
| 893 | + return; |
| 894 | + } |
| 895 | + } |
| 896 | + else |
| 897 | + { |
| 898 | + Console.WriteLine($"[Program.HandlePendingRestore] No existing DB at {dbPath} — placing staged restore directly"); |
| 899 | + } |
| 900 | + |
| 901 | + try |
| 902 | + { |
820 | 903 | File.Move(stagedRestorePath, dbPath); |
821 | | - Console.WriteLine("Staged restore applied successfully"); |
822 | | - |
823 | | - // Delete orphaned WAL/SHM files if they exist |
824 | | - var walPath = $"{dbPath}-wal"; |
825 | | - var shmPath = $"{dbPath}-shm"; |
826 | | - if (File.Exists(walPath)) File.Delete(walPath); |
827 | | - if (File.Exists(shmPath)) File.Delete(shmPath); |
| 904 | + Console.WriteLine($"[Program.HandlePendingRestore] ✅ Staged restore applied successfully."); |
| 905 | + Console.WriteLine($"[Program.HandlePendingRestore] New DB: {dbPath} ({new FileInfo(dbPath).Length:N0} bytes)"); |
| 906 | + } |
| 907 | + catch (Exception ex) |
| 908 | + { |
| 909 | + Console.WriteLine($"[Program.HandlePendingRestore] ERROR: Failed to move staged restore into place."); |
| 910 | + Console.WriteLine($"[Program.HandlePendingRestore] {ex.GetType().Name}: {ex.Message}"); |
| 911 | + if (OperatingSystem.IsWindows()) |
| 912 | + Console.WriteLine($"[Program.HandlePendingRestore] HResult: 0x{ex.HResult:X8}"); |
828 | 913 | } |
829 | 914 | } |
0 commit comments