fix(windows): rewrite the preference store in place when a reader vetoes the rename

dart:io opens files without FILE_SHARE_DELETE, so MoveFileExW with
MOVEFILE_REPLACE_EXISTING - and any other replacement strategy - fails
while such a reader holds the document open. Fall back to the upstream
in-place rewrite so a hostile reader costs at most crash-atomicity for
that one write instead of silently dropping it, and sweep the staging
copy the rename did not consume.
This commit is contained in:
edde746
2026-08-09 18:51:47 +02:00
parent 5a3a1f3c70
commit 8e1be64d8d
@@ -427,7 +427,29 @@ Future<bool> _writePreferences(
staged.parent.createSync(recursive: true);
}
staged.writeAsStringSync(stringMap, flush: true);
staged.renameSync(localDataFile.path);
try {
staged.renameSync(localDataFile.path);
} on FileSystemException {
// PLEZY DELTA: a reader that denies delete sharing vetoes every rename
// over the document. `MoveFileExW` with MOVEFILE_REPLACE_EXISTING — and
// even a POSIX-semantics rename — fails while an open handle on the
// destination lacks FILE_SHARE_DELETE, and dart:io's own `File.open`
// produces exactly such a handle (`_wopen` shares read/write only), as
// do some antivirus and indexing services. The atomic path is not slow
// there; it is unavailable.
//
// Fall back to the upstream in-place rewrite: the sharing mode that
// blocks the rename still admits opening the document for write. This
// reintroduces the truncate window only while such a reader holds the
// store — a brief loss of crash-atomicity beats turning every
// preference write into a silent no-op on exactly the machines whose
// scanners are most likely to have damaged the store in the first
// place.
localDataFile.writeAsStringSync(stringMap, flush: true);
// The staging copy holds the whole document, credentials included, and
// was not consumed by a rename, so it must not outlive the write.
_removeStagingFile(localDataFile);
}
} catch (e) {
debugPrint('Error saving preferences to disk: $e');
return false;