From 7da1882e4b42081918193ab995bd1100d4619f02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:47:28 +0000 Subject: [PATCH 1/2] Initial plan From 828c2880b91c54cdc55c616741f1ea1827dbc037 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:53:49 +0000 Subject: [PATCH 2/2] Fix read-only open behavior, Studio prompts, and multiprocess docs --- README-5.0.0-preview.0.md | 8 ++-- README.md | 5 +- src/BLite.Core/Storage/PageFile.cs | 8 +++- .../MultiProcessAccessConfigTests.cs | 47 +++++++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 9 ++-- tools/BLite.Studio/Views/MainWindow.axaml.cs | 6 +-- 6 files changed, 68 insertions(+), 15 deletions(-) diff --git a/README-5.0.0-preview.0.md b/README-5.0.0-preview.0.md index e2675e7..a52c8de 100644 --- a/README-5.0.0-preview.0.md +++ b/README-5.0.0-preview.0.md @@ -528,7 +528,7 @@ This feature is **opt-in** — all existing single-process behaviour is preserve ```csharp var config = new PageFileConfig { - EnableMultiProcessAccess = true, + AllowMultiProcessAccess = true, }; // Process A (writer) @@ -539,7 +539,7 @@ using var db = new AppDb("shared.db", config); ``` > [!IMPORTANT] -> Multi-process WAL requires all cooperating processes to open the database with `EnableMultiProcessAccess = true`. A process that opens with `EnableMultiProcessAccess = false` will hold `FileShare.None` and block other processes. +> Multi-process WAL requires all cooperating processes to open the database with `AllowMultiProcessAccess = true`. A process that opens with `AllowMultiProcessAccess = false` will hold `FileShare.None` and block other processes. --- @@ -628,7 +628,7 @@ v5.0.0-preview.0 is **fully backwards-compatible** with v4.4.2 databases. No fil | Add audit trail | Call `ConfigureAudit(...)` after construction | | Add GDPR annotations | Annotate properties with `[PersonalData]` and rebuild | | Enable Strict mode | Add `HasGdprMode(GdprMode.Strict)` + configure encryption + audit | -| Enable multi-process access | Set `PageFileConfig.EnableMultiProcessAccess = true` on all processes | +| Enable multi-process access | Set `PageFileConfig.AllowMultiProcessAccess = true` on all processes | --- @@ -706,7 +706,7 @@ v5.0.0-preview.0 is **fully backwards-compatible** with v4.4.2 databases. No fil ## Known limitations in this preview - `RotateEncryptionKeyAsync` is implemented but not yet stress-tested under concurrent write load. Use it during a maintenance window. -- Multi-process WAL (`EnableMultiProcessAccess`) is not yet supported on WASM/Browser targets (tracked in [WASM_SUPPORT.md](WASM_SUPPORT.md)). +- Multi-process WAL (`AllowMultiProcessAccess`) is not yet supported on WASM/Browser targets (tracked in [WASM_SUPPORT.md](WASM_SUPPORT.md)). - Argon2id KDF is reserved for a future release. PBKDF2-SHA256 (600 000 iterations) and HKDF-SHA256 are the only supported KDFs in this preview. - Source-generator GDPR metadata emission (`PersonalDataFields`) requires .NET SDK 9+ (Roslyn 4.x). Fallback reflection path is fully functional on all supported runtimes. diff --git a/README.md b/README.md index 8437927..2e95045 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ await db.VacuumAsync(); A `.wal-shm` sidecar enables **N-reader / 1-writer** access across OS processes (opt-in): ```csharp -var config = new PageFileConfig { EnableMultiProcessAccess = true }; +var config = new PageFileConfig { AllowMultiProcessAccess = true }; using var db = new AppDb("shared.db", config); // open from multiple processes ``` @@ -1082,7 +1082,7 @@ We are actively building the core. Here is where we stand: - ✅ **GDPR Compliance Primitives (v5.0.0)**: `[PersonalData]` annotation, `DataSensitivity` levels, Subject Export (`ExportSubjectDataAsync` — Art. 15/20), Database Inspection (`InspectDatabase` — Art. 30), CDC Field Masking (WP2 — `RevealPersonalData`, `IncludeOnlyFields`, `ExcludeFields`), and `GdprMode.Strict` (Art. 25 privacy-by-default orchestration). - ✅ **Generalized Retention Policy (v5.0.0)**: `HasRetentionPolicy` now applies to any typed collection (not only `TimeSeries`). Supports `maxAge`, `maxDocumentCount`, and configurable `RetentionTrigger` (on-insert or scheduled). - ✅ **Secure Erase & VACUUM (v5.0.0)**: `HasSecureErase(true)` zeros the storage slot on delete for GDPR Art. 17. `VacuumAsync()` compacts the database and reclaims free space. -- ✅ **Multi-Process WAL (v5.0.0)**: `.wal-shm` sidecar enables N-reader / 1-writer access across OS processes. Opt in via `PageFileConfig.EnableMultiProcessAccess = true`. +- ✅ **Multi-Process WAL (v5.0.0)**: `.wal-shm` sidecar enables N-reader / 1-writer access across OS processes. Opt in via `PageFileConfig.AllowMultiProcessAccess = true`. ## 🔮 Future Vision @@ -1127,4 +1127,3 @@ Special thanks to the community members who helped improve BLite: Licensed under the MIT License. Use it freely in personal and commercial projects. - diff --git a/src/BLite.Core/Storage/PageFile.cs b/src/BLite.Core/Storage/PageFile.cs index 7ec4587..b804f65 100644 --- a/src/BLite.Core/Storage/PageFile.cs +++ b/src/BLite.Core/Storage/PageFile.cs @@ -354,10 +354,14 @@ public void Open() var fileExists = File.Exists(_filePath); + var isReadOnlyAccess = _config.Access == MemoryMappedFileAccess.Read; + var fileMode = isReadOnlyAccess ? FileMode.Open : FileMode.OpenOrCreate; + var fileAccess = isReadOnlyAccess ? FileAccess.Read : FileAccess.ReadWrite; + _fileStream = new FileStream( _filePath, - FileMode.OpenOrCreate, - FileAccess.ReadWrite, + fileMode, + fileAccess, _config.AllowMultiProcessAccess ? FileShare.ReadWrite : FileShare.None, bufferSize: 4096, #if NET6_0_OR_GREATER diff --git a/tests/BLite.Tests/MultiProcessAccessConfigTests.cs b/tests/BLite.Tests/MultiProcessAccessConfigTests.cs index 6cacf78..f670faf 100644 --- a/tests/BLite.Tests/MultiProcessAccessConfigTests.cs +++ b/tests/BLite.Tests/MultiProcessAccessConfigTests.cs @@ -102,4 +102,51 @@ public void StorageEngine_ForwardsAllowMultiProcessAccess_ToOwnedWal() try { Directory.Delete(dir, recursive: true); } catch { /* best-effort */ } } } + + [Fact] + public void StorageEngine_ReadOnlyReader_CanOpenAlongsideWriter_WhenMultiProcessEnabled() + { + var dir = Path.Combine(Path.GetTempPath(), $"blite_mpread_{Guid.NewGuid():N}"); + Directory.CreateDirectory(dir); + try + { + var dbPath = Path.Combine(dir, "shared.db"); + var writerConfig = PageFileConfig.Default with { AllowMultiProcessAccess = true }; + var readerConfig = writerConfig with { Access = System.IO.MemoryMappedFiles.MemoryMappedFileAccess.Read }; + + using var writer = new StorageEngine(dbPath, writerConfig); + using var reader = new StorageEngine(dbPath, readerConfig); + + Assert.NotNull(writer.SharedMemory); + Assert.NotNull(reader.SharedMemory); + } + finally + { + try { Directory.Delete(dir, recursive: true); } catch { /* best-effort */ } + } + } + + [Fact] + public void PageFile_ReadOnlyMode_DoesNotCreateMissingFile() + { + var dir = Path.Combine(Path.GetTempPath(), $"blite_readonly_{Guid.NewGuid():N}"); + Directory.CreateDirectory(dir); + try + { + var dbPath = Path.Combine(dir, "missing.db"); + var config = PageFileConfig.Default with + { + Access = System.IO.MemoryMappedFiles.MemoryMappedFileAccess.Read, + AllowMultiProcessAccess = true + }; + + using var pageFile = new PageFile(dbPath, config); + Assert.Throws(() => pageFile.Open()); + Assert.False(File.Exists(dbPath)); + } + finally + { + try { Directory.Delete(dir, recursive: true); } catch { /* best-effort */ } + } + } } diff --git a/tools/BLite.Studio/ViewModels/MainWindowViewModel.cs b/tools/BLite.Studio/ViewModels/MainWindowViewModel.cs index d3ba550..ef0071f 100644 --- a/tools/BLite.Studio/ViewModels/MainWindowViewModel.cs +++ b/tools/BLite.Studio/ViewModels/MainWindowViewModel.cs @@ -262,10 +262,13 @@ private async Task TryOpen(string path, int presetValue, bool readOnly) if (string.IsNullOrEmpty(EncryptionPassphrase)) throw new InvalidOperationException("A passphrase is required when encryption is enabled."); + var access = IsReadOnly + ? MemoryMappedFileAccess.Read + : MemoryMappedFileAccess.ReadWrite; var crypto = new CryptoOptions(EncryptionPassphrase); - var baseConfig = new PageFileConfig { AllowMultiProcessAccess = true }; + var baseConfig = new PageFileConfig { AllowMultiProcessAccess = true, Access = access }; _engine = new BLiteEngine(path, crypto, baseConfig: baseConfig); - _openedConfig = PageFileConfig.Default with { AllowMultiProcessAccess = true }; + _openedConfig = PageFileConfig.Default with { AllowMultiProcessAccess = true, Access = access }; } else { @@ -308,7 +311,7 @@ private async Task TryOpen(string path, int presetValue, bool readOnly) } catch (Exception ex) { - StatusMessage = $"Errore: {ex.Message}"; + StatusMessage = $"Error: {ex.Message}"; IsDatabaseOpen = false; } } diff --git a/tools/BLite.Studio/Views/MainWindow.axaml.cs b/tools/BLite.Studio/Views/MainWindow.axaml.cs index 627a257..b8cb2e1 100644 --- a/tools/BLite.Studio/Views/MainWindow.axaml.cs +++ b/tools/BLite.Studio/Views/MainWindow.axaml.cs @@ -20,13 +20,13 @@ private async void Browse_Click(object? sender, RoutedEventArgs e) var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { - Title = "Apri database BLite", + Title = "Open BLite database", AllowMultiple = false, - // Nessun filtro: qualsiasi estensione è accettata + // Keep broad filters so users can open existing files with custom extensions. FileTypeFilter = [ new FilePickerFileType("Database BLite") { Patterns = ["*.db", "*.blite", "*.blt"] }, - new FilePickerFileType("Tutti i file") { Patterns = ["*.*"] }, + new FilePickerFileType("All files") { Patterns = ["*.*"] }, ] });