Conversation
Windows Fresh Start Fix
There was a problem hiding this comment.
Pull request overview
Updates the database “start fresh” flow to better handle Windows-specific SQLite file locking when archiving an encrypted database and restarting with a new one.
Changes:
- Clears SQLite connection pools and forces Windows GC/finalizers prior to moving the DB file.
- Adds a Windows-only retry for
File.Moveafter a short delay when anIOExceptionoccurs. - Attempts to clean up WAL sidecar files (
-wal,-shm) to avoid stale journal interference on restart.
| 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); |
There was a problem hiding this comment.
The WAL sidecar deletion failure path only logs ex.Message and drops the exception/stack trace. Logging the exception object (e.g., _logger.LogWarning(ex, ...)) will preserve the full context for troubleshooting.
| _logger.LogWarning("Could not remove WAL companion file {Sidecar}: {Message}", sidecar, ex.Message); | |
| _logger.LogWarning(ex, "Could not remove WAL companion file {Sidecar}", sidecar); |
| // 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); |
There was a problem hiding this comment.
The app explicitly enables SQLite WAL mode (see SqlCipherConnectionInterceptor.cs:60-63), so the -wal file can contain committed transactions not yet checkpointed into the main DB. Deleting databasePath-wal/-shm here can make the archived backup incomplete/unrestorable. Instead, archive the sidecar files alongside the moved DB (e.g., move/rename to ${archivedPath}-wal and ${archivedPath}-shm), and only delete them from the original location after they’ve been safely moved (or at least avoid deleting if the move fails).
| // 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); | |
| // Move WAL companion files alongside the archived database if present. These are | |
| // created when WAL mode is active and may contain committed transactions that have | |
| // not yet been checkpointed into the main database file. For the backup to be | |
| // complete and restorable, we archive these sidecar files together with the main DB. | |
| var sidecars = new[] | |
| { | |
| new { Source = databasePath + "-wal", Destination = archivedPath + "-wal" }, | |
| new { Source = databasePath + "-shm", Destination = archivedPath + "-shm" } | |
| }; | |
| foreach (var sidecar in sidecars) | |
| { | |
| if (!File.Exists(sidecar.Source)) continue; | |
| try | |
| { | |
| if (File.Exists(sidecar.Destination)) | |
| { | |
| File.Delete(sidecar.Destination); | |
| } | |
| File.Move(sidecar.Source, sidecar.Destination); | |
| _logger.LogInformation("Archived WAL companion file from {Source} to {Destination}", sidecar.Source, sidecar.Destination); | |
| } | |
| catch (Exception ex) | |
| { | |
| // Non-fatal: if we can't move the sidecar, log and continue. The main | |
| // database has already been moved, and leaving the original WAL/SHM | |
| // next to the soon-to-be-recreated database is safer than deleting it. | |
| _logger.LogWarning( | |
| "Could not archive WAL companion file {Source} to {Destination}: {Message}", | |
| sidecar.Source, | |
| sidecar.Destination, | |
| ex.Message); |
| catch (IOException) when (OperatingSystem.IsWindows()) | ||
| { | ||
| _logger.LogWarning("File move failed on first attempt (Windows lock), retrying after 500 ms"); |
There was a problem hiding this comment.
In the Windows retry path you catch IOException but don’t log the exception details, which makes it harder to diagnose persistent lock issues in the field. Consider capturing the exception (catch (IOException ex)) and logging it (e.g., _logger.LogWarning(ex, ...)) before the retry.
| catch (IOException) when (OperatingSystem.IsWindows()) | |
| { | |
| _logger.LogWarning("File move failed on first attempt (Windows lock), retrying after 500 ms"); | |
| catch (IOException ex) when (OperatingSystem.IsWindows()) | |
| { | |
| _logger.LogWarning(ex, "File move failed on first attempt (Windows lock), retrying after 500 ms"); |
Merge pull request #34 from xnodeoncode/development
Fixed issue with starting with fresh database.