From 157deb3c58003c800a590196063683dc5e7fb201 Mon Sep 17 00:00:00 2001 From: Solid One Date: Sat, 28 Feb 2026 15:15:25 -0600 Subject: [PATCH] see revisions.md for list of changes. --- .../Services/DatabaseUnlockService.cs | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/2-Aquiis.Application/Services/DatabaseUnlockService.cs b/2-Aquiis.Application/Services/DatabaseUnlockService.cs index 973498d..0502781 100644 --- a/2-Aquiis.Application/Services/DatabaseUnlockService.cs +++ b/2-Aquiis.Application/Services/DatabaseUnlockService.cs @@ -71,7 +71,7 @@ public DatabaseUnlockService( } /// - /// Archive encrypted database and create fresh database when password forgotten + /// Archive encrypted database and create fresh database when password forgotten. /// /// Path to encrypted database /// (Success, ArchivedPath, ErrorMessage) @@ -92,12 +92,54 @@ public DatabaseUnlockService( var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); var archivedPath = Path.Combine(backupsDir, $"{dbFileNameWithoutExt}.{timestamp}.encrypted.db"); - // Move encrypted database to backups - File.Move(databasePath, archivedPath); + // On Windows the OS enforces mandatory file locks. Even after SqliteConnection is + // disposed, the connection pool keeps the Win32 file handle open until explicitly + // cleared. Clear all pools and give the GC a chance to release any lingering + // handles before we attempt the file move. + SqliteConnection.ClearAllPools(); + if (OperatingSystem.IsWindows()) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + // Move the main database file. Retry once on Windows in case a finalizer + // hadn't yet released its handle on the first attempt. + try + { + File.Move(databasePath, archivedPath); + } + catch (IOException) when (OperatingSystem.IsWindows()) + { + _logger.LogWarning("File move failed on first attempt (Windows lock), retrying after 500 ms"); + await Task.Delay(500); + File.Move(databasePath, archivedPath); + } + + // Remove WAL companion files if present. These are created when WAL mode is active + // and must be cleaned up so the fresh database starts without a stale journal. + // On Windows these files may also be locked; delete rather than move since the + // archived backup doesn't need them. + foreach (var sidecar in new[] { databasePath + "-wal", databasePath + "-shm" }) + { + if (!File.Exists(sidecar)) continue; + try + { + File.Delete(sidecar); + _logger.LogInformation("Removed WAL companion file: {Sidecar}", sidecar); + } + catch (Exception ex) + { + // Non-fatal: a stale WAL without its main database is harmless. + _logger.LogWarning("Could not remove WAL companion file {Sidecar}: {Message}", sidecar, ex.Message); + } + } + _logger.LogInformation("Encrypted database archived to: {ArchivedPath}", archivedPath); - // New unencrypted database will be created automatically on app restart - // The app will detect no database exists and go through first-time setup + // New database will be created automatically on app restart — the app detects + // no database exists and runs through first-time setup / migrations. return (true, archivedPath, null); } catch (Exception ex)