Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="System.CommandLine" Version="2.0.10" />
<PackageVersion Include="Spectre.Console" Version="0.57.2" />
<PackageVersion Include="SharpCompress" Version="0.50.1" />
<PackageVersion Include="SharpCompress" Version="0.50.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.10" />
Expand All @@ -22,5 +22,7 @@
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ThrottledLogging" Version="1.0.10" />
<PackageVersion Include="ZstdSharp.Port" Version="0.8.8" />
</ItemGroup>
</Project>
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,13 @@ Measured with [BenchmarkDotNet](https://benchmarkdotnet.org/) (Intel Core i9-139

| Scenario | RAM Disk | NVMe SSD | Ratio |
|---|---:|---:|---:|
| Sequential write, 4 KB | 2.2 MB/s | 0.8 MB/s | **2.9× faster** |
| Sequential write, 1 MB | 580 MB/s | 104 MB/s | **5.6× faster** |
| Sequential read, 4 KB / 1 MB | 3.0 MB/s / 643 MB/s | 3.0 MB/s / 677 MB/s | ≈ parity |
| Random 4 KB read, 30 seeks over 16 MB | 1.9–2.3 ms | 1.6 ms | ~1.2–1.4× slower |
| 30× small-file (4 KB) create+write | 51.6 ms (1.72 ms/file) | 80.7 ms (2.69 ms/file) | **1.6× faster** |
| Sequential write, 4 KB | 1.31 MB/s | 0.87 MB/s | **1.5× faster** |
| Sequential write, 1 MB | 321.9 MB/s | 84.6 MB/s | **3.8× faster** |
| Sequential read (OS cache), 4 KB / 1 MB | 3.4 MB/s / 531.6 MB/s | 4.5 MB/s / 955.5 MB/s | ~1.3–1.8× slower |
| Random 4 KB read, 30 seeks over 16 MB | 1.7× slower (OS cache) – 1.6× faster (uncached) | — | mixed |
| 30× small-file (4 KB) create+write | 84.1 ms (2.81 ms/file) | 144.8 ms (4.83 ms/file) | **1.7× faster** |

Writes and small-file create+write win big by skipping block allocation, journaling, and the physical write; sequential reads land near parity with an OS page-cache hit (a user-mode file system can't consistently beat the kernel's own DRAM cache); random reads are modestly slower since each seek pays a kernel–userspace round-trip through WinFsp. Run `dotnet run --project benchmarks/ManagedDrive.Benchmarks -c Release` for current numbers on your own hardware (see [Running Benchmarks](#running-benchmarks) below).
Writes and small-file create+write win big by skipping block allocation, journaling, and the physical write; sequential reads trail the NVMe drive even on an OS page-cache hit (a user-mode file system can't consistently beat the kernel's own DRAM cache, and every WinFsp callback pays a kernel–userspace round trip); random reads land close to parity, faster or slower depending on whether the physical disk's own cache is warm. Run `dotnet run --project benchmarks/ManagedDrive.Benchmarks -c Release` for current numbers on your own hardware (see [Running Benchmarks](#running-benchmarks) below).

### Running Tests

Expand All @@ -169,7 +169,7 @@ BenchmarkDotNet will prompt you to pick which benchmark class(es) to run (`Seque
`mdrive.exe` ships alongside `ManagedDrive.exe` and forwards commands to the running app over a named pipe, so scripts can drive ManagedDrive without opening the UI. If the app isn't already running, `mdrive` launches it and retries for up to 10 seconds before giving up.

```powershell
mdrive mount C:\disks\scratch.mdr R: --auto-mount --compression Optimal
mdrive mount C:\disks\scratch.mdr R: --auto-mount --compression Optimal --custom-zstd-level 19
mdrive list
mdrive save R:
mdrive format R: --yes
Expand All @@ -179,7 +179,7 @@ mdrive exit

| Command | Description |
|---|---|
| `mount <image-path> <drive-letter> [options]` | Mounts an existing `.mdr` image at a drive letter. Options: `--read-only`, `--auto-mount`, `--auto-save-minutes`, `--compression <None\|Fastest\|Optimal\|SmallestSize>`, `--max-snapshot-count`, `--max-snapshot-size-mb`, `--high-usage-warn-percent`, `--password`, `--password-file` (mutually exclusive; needed only if the image is encrypted — `--password-file` reads the first line of a file and is recommended over `--password` to avoid exposing it in shell history or the process list). Any option left unset keeps the image's saved profile value (or its default). |
| `mount <image-path> <drive-letter> [options]` | Mounts an existing `.mdr` image at a drive letter. Options: `--read-only`, `--auto-mount`, `--auto-save-minutes`, `--compression <None\|Fastest\|Optimal\|SmallestSize>`, `--custom-zstd-level <1-22>` (overrides the preset Zstd level mapped from `--compression`; only takes effect when the compression level is not `None`), `--max-snapshot-count`, `--max-snapshot-size-mb`, `--high-usage-warn-percent`, `--password`, `--password-file` (mutually exclusive; needed only if the image is encrypted — `--password-file` reads the first line of a file and is recommended over `--password` to avoid exposing it in shell history or the process list). Any option left unset keeps the image's saved profile value (or its default). |
| `mount-archive <archive-path> [drive-letter]` | Imports an archive (zip/7z/rar/tar/...) as a read-only disk and opens it in Explorer once mounted. `drive-letter` is optional — if omitted, the first free letter from `Z:` down to `D:` is used. Used internally by the Explorer right-click menu entry. |
| `unmount <drive-letter>` | Unmounts a mounted disk. |
| `format <drive-letter> --yes` | Deletes all files on a mounted disk. Requires `--yes`/`-y` to confirm. |
Expand Down Expand Up @@ -365,13 +365,13 @@ ManagedDrive 使用 **WinFsp**(Windows 文件系统代理)将内存目录树

| 场景 | 内存盘 | NVMe SSD | 倍率 |
|---|---:|---:|---:|
| 顺序写入,4 KB | 2.2 MB/s | 0.8 MB/s | **快 2.9×** |
| 顺序写入,1 MB | 580 MB/s | 104 MB/s | **快 5.6×** |
| 顺序读取,4 KB / 1 MB | 3.0 MB/s / 643 MB/s | 3.0 MB/s / 677 MB/s | ≈ 持平 |
| 随机 4 KB 读取,对 16 MB 文件寻址 30 次 | 1.9–2.3 ms | 1.6 ms | 慢 ~1.2–1.4× |
| 30 次小文件(4 KB)创建+写入 | 51.6 ms(1.72 ms/文件) | 80.7 ms(2.69 ms/文件) | **快 1.6×** |
| 顺序写入,4 KB | 1.31 MB/s | 0.87 MB/s | **快 1.5×** |
| 顺序写入,1 MB | 321.9 MB/s | 84.6 MB/s | **快 3.8×** |
| 顺序读取(OS 缓存),4 KB / 1 MB | 3.4 MB/s / 531.6 MB/s | 4.5 MB/s / 955.5 MB/s | 慢 ~1.3–1.8× |
| 随机 4 KB 读取,对 16 MB 文件寻址 30 次 | 慢 1.7×(OS 缓存)~快 1.6×(未缓存) | — | 不一致 |
| 30 次小文件(4 KB)创建+写入 | 84.1 ms(2.81 ms/文件) | 144.8 ms(4.83 ms/文件) | **快 1.7×** |

写入及小文件创建+写入优势明显,因为跳过了物理块分配、日志记录和实际落盘;顺序读取与 OS 页缓存命中基本持平(用户态文件系统无法稳定超越内核自身的 DRAM 缓存);随机读取略慢,因为每次寻址都要经过 WinFsp 的内核–用户态往返。运行 `dotnet run --project benchmarks/ManagedDrive.Benchmarks -c Release` 可在你自己的硬件上获取当前数据(见下方[运行基准测试](#running-benchmarks-zh))。
写入及小文件创建+写入优势明显,因为跳过了物理块分配、日志记录和实际落盘;顺序读取即便命中 OS 页缓存也不及 NVMe 硬盘(用户态文件系统无法稳定超越内核自身的 DRAM 缓存,且每次 WinFsp 回调都要经过一次内核–用户态往返);随机读取基本持平,具体快慢取决于物理磁盘自身缓存是否命中。运行 `dotnet run --project benchmarks/ManagedDrive.Benchmarks -c Release` 可在你自己的硬件上获取当前数据(见下方[运行基准测试](#running-benchmarks-zh))。

### 运行测试

Expand All @@ -398,7 +398,7 @@ BenchmarkDotNet 会提示你选择要运行的基准测试类(`SequentialReadW
`mdrive.exe` 随 `ManagedDrive.exe` 一同发布,通过命名管道将命令转发给正在运行的应用,因此脚本无需打开界面即可操作 ManagedDrive。若应用尚未运行,`mdrive` 会自动启动它,并在最长 10 秒内重试。

```powershell
mdrive mount C:\disks\scratch.mdr R: --auto-mount --compression Optimal
mdrive mount C:\disks\scratch.mdr R: --auto-mount --compression Optimal --custom-zstd-level 19
mdrive list
mdrive save R:
mdrive format R: --yes
Expand Down
44 changes: 43 additions & 1 deletion THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,48 @@ ManagedDrive uses the following open-source software.

## SharpCompress

- **Package:** `SharpCompress` 0.49.1
- **Package:** `SharpCompress` 0.50.3
- **Copyright:** Copyright (c) 2025 Adam Hathcock
- **License:** [MIT License](https://github.com/adamhathcock/sharpcompress/blob/master/LICENSE)

## ZstdSharp.Port

- **Package:** `ZstdSharp.Port` 0.8.8
- **Copyright:** Copyright (c) Oleg Stepanischev
- **License:** [MIT License](https://github.com/oleg-st/ZstdSharp/blob/master/LICENSE)

## ThrottledLogging

- **Package:** `ThrottledLogging` 1.0.10
- **Copyright:** Copyright (c) coldhighsun
- **License:** [MIT License](https://github.com/coldhighsun/ThrottledLogging/blob/master/LICENSE)

## System.CommandLine

- **Package:** `System.CommandLine` 2.0.10
- **Copyright:** © Microsoft Corporation
- **License:** [MIT License](https://github.com/dotnet/command-line-api/blob/main/LICENSE.md)

## Spectre.Console

- **Package:** `Spectre.Console` 0.57.2
- **Copyright:** Copyright (c) Patrik Svensson, Phil Scott, Nils Andresen, Cédric Luthi
- **License:** [MIT License](https://github.com/spectreconsole/spectre.console/blob/main/LICENSE.md)

## YamlDotNet

- **Package:** `YamlDotNet` 18.1.0
- **Copyright:** Copyright (c) Antoine Aubry and contributors
- **License:** [MIT License](https://github.com/aaubry/YamlDotNet/blob/master/LICENSE.txt)

## Microsoft.Extensions.* (Hosting.WindowsServices, Logging, Logging.Abstractions, DependencyInjection)

- **Packages:** `Microsoft.Extensions.Hosting.WindowsServices`, `Microsoft.Extensions.Logging`, `Microsoft.Extensions.Logging.Abstractions`, `Microsoft.Extensions.DependencyInjection` 10.0.10
- **Copyright:** © Microsoft Corporation
- **License:** [MIT License](https://github.com/dotnet/runtime/blob/main/LICENSE.TXT)

## Serilog and Serilog sinks (Serilog, Serilog.Extensions.Logging, Serilog.Sinks.File, Serilog.Sinks.Async)

- **Packages:** `Serilog` 4.4.0, `Serilog.Extensions.Logging` 10.0.0, `Serilog.Sinks.File` 7.0.0, `Serilog.Sinks.Async` 2.1.0
- **Copyright:** Copyright © Serilog Contributors
- **License:** [Apache License 2.0](https://github.com/serilog/serilog/blob/dev/LICENSE)
53 changes: 26 additions & 27 deletions src/ManagedDrive.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,7 @@ private void App_Exit(object sender, ExitEventArgs e)
{
_logger.LogInformation("App_Exit invoked.");

if (_sessionEndingSaveHandler != null)
{
SystemEvents.SessionEnding -= _sessionEndingSaveHandler.OnSessionEnding;
}
if (_mountManager != null && _trayIconController != null)
{
_mountManager.ActivityDetected -= _trayIconController.OnActivityDetected;
}
_mainViewModel?.SaveSettings();
_trayIconController?.Dispose();
_mainViewModel?.Dispose();

_cliPipeServer?.Dispose();
TeardownBeforeMountManagerDispose();

// Safety net: if ShutdownAsync already disposed the mount manager, this is a no-op.
// Bounded so a stuck final save can't hang process exit indefinitely.
Expand Down Expand Up @@ -153,7 +141,9 @@ private async void App_Startup(object sender, StartupEventArgs e)
_trayTooltipController = new(_mainViewModel, _trayIconController);
_tempDirCompatChecker = new(_settings, _trayIconController, () => _mainWindow is { IsLoaded: true } ? _mainWindow : null);
_mountManager.ActivityDetected += _trayIconController.OnActivityDetected;
_diskNotificationService = new(_mainViewModel, _trayIconController, () => _mainWindow!.IsVisible);
_diskNotificationService = new(
_mainViewModel, _trayIconController, () => _mainWindow!.IsVisible,
_serviceProvider!.GetRequiredService<ILogger<DiskNotificationService>>());

// Constructed before AutoMountDisksAsync so that an auto-mounted disk which is already the
// TEMP target gets its global symlink published at startup. Rooted as a field only to keep
Expand Down Expand Up @@ -438,10 +428,6 @@ private async Task ShutdownAsync()
{
_logger.LogInformation("ShutdownAsync starting.");

if (_sessionEndingSaveHandler != null)
{
SystemEvents.SessionEnding -= _sessionEndingSaveHandler.OnSessionEnding;
}
_isExiting = true;

if (_mainViewModel != null)
Expand All @@ -450,15 +436,7 @@ private async Task ShutdownAsync()
ShowMainWindow();
}

_cliPipeServer?.Dispose();
_mainViewModel?.SaveSettings();
_trayIconController?.Dispose();
_mainViewModel?.Dispose();

if (_mountManager != null && _trayIconController != null)
{
_mountManager.ActivityDetected -= _trayIconController.OnActivityDetected;
}
TeardownBeforeMountManagerDispose();

await Task.Run(() => _mountManager?.Dispose((disk, diskFraction, overallFraction, totalBytes) =>
Application.Current.Dispatcher.BeginInvoke(() =>
Expand All @@ -467,4 +445,25 @@ await Task.Run(() => _mountManager?.Dispose((disk, diskFraction, overallFraction
_logger.LogInformation("ShutdownAsync completed; shutting down application.");
Shutdown();
}

/// <summary>
/// Common teardown shared by <see cref="App_Exit"/> and <see cref="ShutdownAsync"/>, run
/// before either one disposes <see cref="_mountManager"/> (which they do differently: a
/// bounded-wait safety net vs. an awaited call with exit-save progress reporting).
/// </summary>
private void TeardownBeforeMountManagerDispose()
{
if (_sessionEndingSaveHandler != null)
{
SystemEvents.SessionEnding -= _sessionEndingSaveHandler.OnSessionEnding;
}
if (_mountManager != null && _trayIconController != null)
{
_mountManager.ActivityDetected -= _trayIconController.OnActivityDetected;
}
_cliPipeServer?.Dispose();
_mainViewModel?.SaveSettings();
_trayIconController?.Dispose();
_mainViewModel?.Dispose();
}
}
8 changes: 8 additions & 0 deletions src/ManagedDrive.App/Cli/MainViewModelCliDiskController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ namespace ManagedDrive.App.Cli;
/// <c>ManagedDrive.Cli.Core</c> project (which cannot reference the WPF app layer without creating
/// a circular project reference) can drive disk mount/unmount/list operations.
/// </summary>
/// <remarks>
/// This class is the entire contract surface between the CLI pipe server and <see cref="MainViewModel"/>:
/// <see cref="MainViewModel.ExitWithoutConfirmation"/>, <see cref="MainViewModel.FormatByMountPointAsync"/>,
/// <see cref="MainViewModel.MountArchiveAsync"/>, <see cref="MainViewModel.MountImageAsync"/>,
/// <see cref="MainViewModel.SaveByMountPointAsync"/>, and <see cref="MainViewModel.UnmountByMountPointAsync"/>
/// (plus the read-only <see cref="MainViewModel.Disks"/> collection for <see cref="ListDisks"/>). Changing
/// any of their signatures requires updating this adapter in lockstep.
/// </remarks>
internal sealed class MainViewModelCliDiskController(MainViewModel mainViewModel) : ICliDiskController
{
/// <summary>
Expand Down
8 changes: 4 additions & 4 deletions src/ManagedDrive.App/Infrastructure/WindowMaximizeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ public static void HookMaximizeBehavior(Window window)
window.SourceInitialized += (_, _) =>
{
var handle = new WindowInteropHelper(window).Handle;
if (HwndSource.FromHwnd(handle) is HwndSource source)
if (HwndSource.FromHwnd(handle) is { } source)
{
source.AddHook((IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) =>
WndProc(hwnd, msg, wParam, lParam, ref handled, window));
source.AddHook((hwnd, msg, _, lParam, ref handled) =>
WndProc(hwnd, msg, lParam, ref handled, window));
}
};

Expand Down Expand Up @@ -65,7 +65,7 @@ private static void ClampMaximizedToWorkArea(Window window)
true);
}

private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled, Window window)
private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr lParam, ref bool handled, Window window)
{
if (msg == WM_GETMINMAXINFO)
{
Expand Down
9 changes: 4 additions & 5 deletions src/ManagedDrive.App/Localization/Strings.en-US.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@
<sys:String x:Key="CompressionLevel.Fastest">Fast</sys:String>
<sys:String x:Key="CompressionLevel.Optimal">Balanced</sys:String>
<sys:String x:Key="CompressionLevel.SmallestSize">Max</sys:String>
<sys:String x:Key="CreateDisk.CompressionWarning">Balanced/Max compression may significantly increase save times. Choose carefully.</sys:String>
<sys:String x:Key="CreateDisk.CustomZstdLevel">Custom Zstd level:</sys:String>
<sys:String x:Key="Tip.CustomZstdLevel">Advanced: override the compression preset above with an exact Zstd level (1 = fastest, 22 = smallest). Leave unchecked to use the preset's level.</sys:String>
<sys:String x:Key="Val.BadCustomZstdLevel">Enter a custom Zstd level between 1 and 22.</sys:String>
<sys:String x:Key="CreateDisk.SaveOnExit">Save image on exit</sys:String>
<sys:String x:Key="CreateDisk.AutoSave">Auto-save the image, interval (minutes):</sys:String>
<sys:String x:Key="CreateDisk.AutoSaveInterval">Interval (min)</sys:String>
Expand Down Expand Up @@ -315,10 +317,7 @@
<sys:String x:Key="About.Description">An in-memory RAM disk for Windows.</sys:String>
<sys:String x:Key="About.Copyright">© 2026 coldhighsun</sys:String>
<sys:String x:Key="About.ViewOnGitHub">View on GitHub</sys:String>
<sys:String x:Key="About.ViewWinFsp">Powered by WinFsp</sys:String>
<sys:String x:Key="About.WinFspLicense">© 2015-2026 Bill Zissimopoulos — GNU GPLv2 (FUSE exception) or commercial license</sys:String>
<sys:String x:Key="About.ViewSharpCompress">Powered by SharpCompress</sys:String>
<sys:String x:Key="About.SharpCompressLicense">© 2025 Adam Hathcock — MIT License</sys:String>
<sys:String x:Key="About.ViewThirdPartyNotices">Third-Party Notices</sys:String>
<sys:String x:Key="About.UpdateAvailable">A new version ({0}) is available. Click to download.</sys:String>

<!-- Update available -->
Expand Down
Loading
Loading