Skip to content

Windows Fresh Start Fix#33

Merged
xskcdf merged 1 commit intoxnodeoncode:developmentfrom
xskcdf:development
Feb 28, 2026
Merged

Windows Fresh Start Fix#33
xskcdf merged 1 commit intoxnodeoncode:developmentfrom
xskcdf:development

Conversation

@xskcdf
Copy link
Collaborator

@xskcdf xskcdf commented Feb 28, 2026

Fixed issue with file locks when starting with fresh database.

@xskcdf xskcdf self-assigned this Feb 28, 2026
@xskcdf xskcdf added 🐛 bug Something isn't working 🔧 enhancement New feature or request priority:high labels Feb 28, 2026
@xskcdf xskcdf merged commit 9359dc4 into xnodeoncode:development Feb 28, 2026
2 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Windows “fresh start” flow by reducing SQLite file-lock issues when archiving an encrypted database and restarting with a new database.

Changes:

  • Clears SQLite connection pools and forces finalizer cleanup on Windows before moving the database file.
  • Adds Windows-specific retry logic for moving the DB file after a transient lock failure.
  • Attempts to remove SQLite WAL sidecar files (-wal, -shm) after archiving to avoid stale journals during fresh start.

Comment on lines +123 to +135
// 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);
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WAL sidecar delete failures are swallowed and the method still returns success. Since the app forces PRAGMA journal_mode = WAL, leaving locked -wal/-shm files behind can prevent the fresh database from opening on restart (same Windows lock issue). Consider adding retry/backoff for deleting these files and/or returning a failure result if they cannot be removed.

Suggested change
// 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);
// archived backup doesn't need them. If we cannot remove them after several attempts,
// treat this as a failure because a locked -wal/-shm can prevent a fresh database
// from opening on restart.
foreach (var sidecar in new[] { databasePath + "-wal", databasePath + "-shm" })
{
if (!File.Exists(sidecar)) continue;
var deleted = false;
const int maxDeleteAttempts = 3;
for (var attempt = 1; attempt <= maxDeleteAttempts && !deleted; attempt++)
{
try
{
File.Delete(sidecar);
deleted = true;
_logger.LogInformation("Removed WAL companion file: {Sidecar}", sidecar);
}
catch (IOException ex) when (OperatingSystem.IsWindows() && attempt < maxDeleteAttempts)
{
_logger.LogWarning(ex,
"WAL companion file delete failed on attempt {Attempt} for {Sidecar} (Windows lock). Retrying...",
attempt, sidecar);
await Task.Delay(200);
}
catch (UnauthorizedAccessException ex) when (OperatingSystem.IsWindows() && attempt < maxDeleteAttempts)
{
_logger.LogWarning(ex,
"WAL companion file delete failed on attempt {Attempt} for {Sidecar} (access denied). Retrying...",
attempt, sidecar);
await Task.Delay(200);
}
}
if (!deleted && File.Exists(sidecar))
{
var message =
$"Unable to remove WAL companion file '{sidecar}' after multiple attempts. " +
"A locked -wal/-shm file can prevent the new database from opening.";
_logger.LogError(message);
return (false, null, message);

Copilot uses AI. Check for mistakes.
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);
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When deletion of a WAL companion file fails, the warning log drops the exception details (only logs ex.Message). Logging the exception itself (e.g., passing ex to LogWarning) will preserve stack/Win32 error information which is important for diagnosing file lock issues.

Suggested change
_logger.LogWarning("Could not remove WAL companion file {Sidecar}: {Message}", sidecar, ex.Message);
_logger.LogWarning(ex, "Could not remove WAL companion file {Sidecar}", sidecar);

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +115
catch (IOException) when (OperatingSystem.IsWindows())
{
_logger.LogWarning("File move failed on first attempt (Windows lock), retrying after 500 ms");
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry path for File.Move logs a generic warning but discards the original IOException details. Catching into a variable and logging the exception (or at least its message) would make it much easier to confirm whether the failure was a sharing violation vs. another I/O issue.

Suggested change
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");

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +118
// 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);
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces OS-specific retry/GC and WAL cleanup behavior but there are no automated tests covering StartWithNewDatabaseAsync. Consider adding tests for the retry and sidecar cleanup behavior (e.g., by introducing a small filesystem abstraction to simulate locked files and verify retries / error results).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working 🔧 enhancement New feature or request priority:high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants