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
46 changes: 32 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,31 +138,49 @@ jobs:
echo "is_prerelease=$($isPrerelease.ToString().ToLower())" >> $env:GITHUB_OUTPUT
shell: pwsh

- name: Build portable package
run: |
$tag = "${{ steps.version.outputs.tag }}"
Compress-Archive -Path publish-fx\* -DestinationPath "ManagedDrive-$tag-win-x64-portable.zip"
shell: pwsh

- name: Prepare installer inputs
- name: Download WinFsp MSI
# Single source of the bundled WinFsp release for the steps below; bump alongside
# WinFspPrerequisite.cs, installer/ManagedDrive.iss and Directory.Packages.props.
env:
WINFSP_TAG: v2.2B5
WINFSP_MSI: winfsp-2.2.26286.msi
WINFSP_SHA256: 293FFB9BCC0B625C85079FE85FA77F8FE98170ABA51A02AC9C83D58F6C3B0E2F
run: |
Copy-Item -Path publish-fx -Destination installer\publish-fx -Recurse
"WINFSP_MSI=$env:WINFSP_MSI" >> $env:GITHUB_ENV
"WINFSP_SHA256=$env:WINFSP_SHA256" >> $env:GITHUB_ENV
Invoke-WebRequest `
-Uri "https://github.com/winfsp/winfsp/releases/download/v2.2B4/winfsp-2.2.26215.msi" `
-OutFile "installer\winfsp-2.2.26215.msi"
-Uri "https://github.com/winfsp/winfsp/releases/download/$env:WINFSP_TAG/$env:WINFSP_MSI" `
-OutFile $env:WINFSP_MSI
shell: pwsh

- name: Verify WinFsp MSI checksum
# Pins the download to the exact bytes we've tested against; bump this hash
# deliberately (alongside the URL above) when upgrading the bundled WinFsp version.
# Pins the download to the exact bytes we've tested against; bump WINFSP_SHA256
# deliberately (alongside WINFSP_TAG/WINFSP_MSI above) when upgrading the bundled WinFsp version.
run: |
$expectedHash = "2ECB5C89405488A95BBD8A01875E02C48534FD37BBDFD84488F7590464D65944"
$actualHash = (Get-FileHash "installer\winfsp-2.2.26215.msi" -Algorithm SHA256).Hash
$expectedHash = $env:WINFSP_SHA256
$actualHash = (Get-FileHash $env:WINFSP_MSI -Algorithm SHA256).Hash
if ($actualHash -ne $expectedHash) {
throw "WinFsp MSI checksum mismatch: expected $expectedHash, got $actualHash"
}
shell: pwsh

- name: Build portable package
# The portable ZIP carries the WinFsp MSI in prerequisites\ so the app can install it offline;
# it is removed again afterwards because the Inno Setup installer bundles its own copy.
run: |
$tag = "${{ steps.version.outputs.tag }}"
New-Item -ItemType Directory -Path publish-fx\prerequisites | Out-Null
Copy-Item $env:WINFSP_MSI publish-fx\prerequisites\
Compress-Archive -Path publish-fx\* -DestinationPath "ManagedDrive-$tag-win-x64-portable.zip"
Remove-Item publish-fx\prerequisites -Recurse -Force
shell: pwsh

- name: Prepare installer inputs
run: |
Copy-Item -Path publish-fx -Destination installer\publish-fx -Recurse
Copy-Item $env:WINFSP_MSI installer\
shell: pwsh

- name: Install Inno Setup
# Chocolatey's "innosetup" package tops out at 6.x; JRSoftware.InnoSetup.7 is the
# separate winget package for Inno Setup 7. "--scope machine" keeps the install
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dotnet run --project src/ManagedDrive.Cli -- list

The solution file is `ManagedDrive.slnx` (Visual Studio 2022+ format).

**WinFsp prerequisite:** `winfsp-msil.dll` must be present at `C:\Program Files (x86)\WinFsp\bin\`. Install exactly [WinFsp 2.2.26215 (2026 Beta4)](https://github.com/winfsp/winfsp/releases/tag/v2.2B4) before building or running — download the MSI directly; do not use `winget install WinFsp.WinFsp`, as the winget package lags behind this release.
**WinFsp prerequisite:** `winfsp-msil.dll` must be present at `C:\Program Files (x86)\WinFsp\bin\`. Install exactly [WinFsp 2.2.26286 (2026 Beta5)](https://github.com/winfsp/winfsp/releases/tag/v2.2B5) before building or running — download the MSI directly; do not use `winget install WinFsp.WinFsp`, as the winget package lags behind this release. At startup `WinFspPrerequisite.Check` compares the installed `winfsp-msil.dll` with that version (`RequiredVersion`, the release tag and the MSI SHA256 live there too; bump them together with `winfsp.net`, `installer/ManagedDrive.iss` and the `ci.yml` WinFsp env values); a missing or older version offers `WinFspInstaller` (copies the MSI shipped in the portable ZIP's `prerequisites\`, or downloads it, to `%LOCALAPPDATA%\ManagedDrive\updates`; verifies the SHA256 through a handle that stays open during the run; runs `msiexec` elevated). A newer WinFsp is accepted, a declined update for an older one is remembered per installed/required pair (`AppConfiguration.SkippedWinFspVersion`), and only a missing runtime is fatal.

## Architecture

Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageVersion Include="xunit.v3" Version="4.0.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.1" />
<PackageVersion Include="winfsp.net" Version="2.2.26215" />
<PackageVersion Include="winfsp.net" Version="2.2.26286" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="System.CommandLine" Version="2.0.12" />
<PackageVersion Include="Spectre.Console" Version="0.57.2" />
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The ZIP also includes `mdrive.exe`, a companion CLI (see [CLI Usage](#cli-usage)
| Requirement | Notes |
|---|---|
| **Windows 10 / 11 (64-bit)** | ARM64 is not currently tested |
| **[WinFsp 2.2.26215 (2026 Beta4)](https://github.com/winfsp/winfsp/releases/tag/v2.2B4)** | Must be installed before running ManagedDrive. Download the installer directly: [winfsp-2.2.26215.msi](https://github.com/winfsp/winfsp/releases/download/v2.2B4/winfsp-2.2.26215.msi) — do not use `winget install WinFsp.WinFsp`, as the winget package lags behind the latest release. The managed assembly `winfsp-msil.dll` is installed to `C:\Program Files (x86)\WinFsp\bin\` and is referenced by the project automatically. |
| **[WinFsp 2.2.26286 (2026 Beta5)](https://github.com/winfsp/winfsp/releases/tag/v2.2B5)** | The installer bundles it, and the app offers to download and install it when it is missing or older (a newer version is accepted); the portable ZIP also carries the installer in `prerequisites\`. To install it manually, download the installer directly: [winfsp-2.2.26286.msi](https://github.com/winfsp/winfsp/releases/download/v2.2B5/winfsp-2.2.26286.msi) — do not use `winget install WinFsp.WinFsp`, as the winget package lags behind the latest release. The managed assembly `winfsp-msil.dll` is installed to `C:\Program Files (x86)\WinFsp\bin\` and is referenced by the project automatically. |
| **[.NET 10 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/10.0)** | Required for the `-portable` ZIP (framework-dependent). |
| **.NET 10 SDK** | Required to build. |

Expand Down Expand Up @@ -307,7 +307,7 @@ ZIP 中还包含 `mdrive.exe`(配套命令行工具,见下方[命令行用
| 要求 | 说明 |
|---|---|
| **Windows 10 / 11(64 位)** | 暂未测试 ARM64 |
| **[WinFsp 2.2.26215(2026 Beta4)](https://github.com/winfsp/winfsp/releases/tag/v2.2B4)** | 必须安装此版本才能运行 ManagedDrive。请直接下载安装包:[winfsp-2.2.26215.msi](https://github.com/winfsp/winfsp/releases/download/v2.2B4/winfsp-2.2.26215.msi)——不要使用 `winget install WinFsp.WinFsp` 安装,因为该 winget 包更新不及时,落后于最新发布版本。托管程序集 `winfsp-msil.dll` 将安装至 `C:\Program Files (x86)\WinFsp\bin\`,项目会自动引用。 |
| **[WinFsp 2.2.26286(2026 Beta5)](https://github.com/winfsp/winfsp/releases/tag/v2.2B5)** | 安装程序已内置,应用在检测到缺失或版本过旧时也会提示自动下载安装(更新版本可直接使用),便携版压缩包的 `prerequisites\` 目录同样附带安装包。如需手动安装,请直接下载安装包:[winfsp-2.2.26286.msi](https://github.com/winfsp/winfsp/releases/download/v2.2B5/winfsp-2.2.26286.msi)——不要使用 `winget install WinFsp.WinFsp` 安装,因为该 winget 包更新不及时,落后于最新发布版本。托管程序集 `winfsp-msil.dll` 将安装至 `C:\Program Files (x86)\WinFsp\bin\`,项目会自动引用。 |
| **[.NET 10 桌面运行时](https://dotnet.microsoft.com/download/dotnet/10.0)** | "绿色版"(框架依赖型)ZIP 需要。 |
| **.NET 10 SDK** | 编译所需。 |

Expand Down
8 changes: 4 additions & 4 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Developer-facing documentation for ManagedDrive — building from source, soluti
### Getting Started

```powershell
# 1. Download and install WinFsp 2.2.26215 (2026 Beta4)
# https://github.com/winfsp/winfsp/releases/download/v2.2B4/winfsp-2.2.26215.msi
# 1. Download and install WinFsp 2.2.26286 (2026 Beta5) - or let ManagedDrive offer to install it on first start
# https://github.com/winfsp/winfsp/releases/download/v2.2B5/winfsp-2.2.26286.msi

# 2. Clone the repository
git clone https://github.com/coldhighsun/ManagedDrive
Expand Down Expand Up @@ -100,8 +100,8 @@ BenchmarkDotNet will prompt you to pick which benchmark class(es) to run (`Seque
### 快速开始

```powershell
# 1. 下载并安装 WinFsp 2.2.26215(2026 Beta4)
# https://github.com/winfsp/winfsp/releases/download/v2.2B4/winfsp-2.2.26215.msi
# 1. 下载并安装 WinFsp 2.2.26286(2026 Beta5)——也可以首次启动时由 ManagedDrive 提示自动安装
# https://github.com/winfsp/winfsp/releases/download/v2.2B5/winfsp-2.2.26286.msi

# 2. 克隆仓库
git clone https://github.com/coldhighsun/ManagedDrive
Expand Down
4 changes: 2 additions & 2 deletions docs/THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ ManagedDrive uses the following open-source software.

## WinFsp

- **Package:** `winfsp.net` 2.2.26215
- **Package:** `winfsp.net` 2.2.26286
- **Copyright:** © 2015-2026 Bill Zissimopoulos
- **License:** Dual-licensed under the GNU General Public License v2 (with a FUSE linking exception) or a commercial license. See [License.txt](https://github.com/winfsp/winfsp/blob/master/License.txt) and [winfsp.dev](https://winfsp.dev/) for the authoritative terms.
- The `ManagedDrive-Setup-*.exe` installer bundles and redistributes the official, unmodified WinFsp installer (`winfsp-2.2.26215.msi`, downloaded from the [WinFsp v2.2B4 release](https://github.com/winfsp/winfsp/releases/tag/v2.2B4)) so it can be installed automatically alongside ManagedDrive.
- The `ManagedDrive-Setup-*.exe` installer bundles and redistributes the official, unmodified WinFsp installer (`winfsp-2.2.26286.msi`, downloaded from the [WinFsp v2.2B5 release](https://github.com/winfsp/winfsp/releases/tag/v2.2B5)) so it can be installed automatically alongside ManagedDrive.

## SharpCompress

Expand Down
16 changes: 10 additions & 6 deletions installer/ManagedDrive.iss
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
#define AppVersionTag "v" + AppVersion
#endif

#define WinFspMsiName "winfsp-2.2.26215.msi"
#define WinFspBuild "26286"
#define WinFspVersion "2.2." + WinFspBuild
#define WinFspMsiName "winfsp-" + WinFspVersion + ".msi"
#define HelperServiceName "ManagedDriveHelper"
#define HelperServiceExeName "ManagedDriveHelper.exe"
; Must match App.xaml.cs::SingleInstanceMutexName exactly - this is how Setup detects a running
Expand Down Expand Up @@ -93,13 +95,14 @@ var
// LaunchAppAsUser()'s explorer.exe workaround below exists to avoid.
LaunchAppCheckBox: TNewCheckBox;

// Mirrors ManagedDrive.App's App.xaml.cs::CheckWinFspPrerequisite() detection:
// HKLM InstallDir -> <InstallDir>\bin\winfsp-msil.dll must exist with a 2.2.x file version.
// Mirrors ManagedDrive.App's WinFspPrerequisite.Check() detection: HKLM InstallDir ->
// <InstallDir>\bin\winfsp-msil.dll must exist with at least the bundled version ({#WinFspVersion}).
// An older version is replaced by the bundled MSI; a newer one is kept (MSI refuses downgrades).
function IsWinFspInstalled(): Boolean;
var
InstallDir, DllPath: string;
VersionMS, VersionLS: Cardinal;
Major, Minor: Integer;
Major, Minor, Build: Cardinal;
begin
Result := False;

Expand All @@ -118,7 +121,8 @@ begin

Major := VersionMS shr 16;
Minor := VersionMS and $FFFF;
Result := (Major = 2) and (Minor = 2);
Build := VersionLS shr 16;
Result := (Major > 2) or ((Major = 2) and ((Minor > 2) or ((Minor = 2) and (Build >= {#WinFspBuild}))));
end;

// Mirrors the check dotnet's own bootstrapper uses to detect an installed
Expand Down Expand Up @@ -427,7 +431,7 @@ begin
if not IsWinFspInstalled() then
InstallWinFspSilently()
else
Log('WinFsp 2.2.x already installed; skipping.');
Log('WinFsp {#WinFspVersion} already installed; skipping.');

// Reinstall/upgrade: stop the running service *before* [Files] copies the new
// ManagedDriveHelper.exe over it. CloseApplicationsFilter/Restart Manager only cover
Expand Down
94 changes: 83 additions & 11 deletions src/ManagedDrive.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private async void App_Startup(object sender, StartupEventArgs e)
// Before the splash, so its warning box is not covered by it. Shutdown() only queues the
// exit; return so nothing below runs — in particular no MainViewModel gets created, so
// App_Exit has no (empty) disk list to save over the user's settings.
if (!CheckWinFspPrerequisite())
if (!await CheckWinFspPrerequisiteAsync(_settings, config))
{
Shutdown();
return;
Expand Down Expand Up @@ -561,30 +561,102 @@ void CountDiskDone(int index)
private const int AutoMountConcurrency = 2;

/// <summary>
/// Verifies WinFsp is installed; if not, tells the user and offers to open its download page.
/// Verifies the required WinFsp version is installed; if not, offers to download and install it.
/// </summary>
/// <returns>
/// <c>true</c> if WinFsp is installed and startup can continue; <c>false</c> if the app must exit.
/// </returns>
private static bool CheckWinFspPrerequisite()
private static async Task<bool> CheckWinFspPrerequisiteAsync(SettingsStore settings, AppConfiguration config)
{
if (WinFspPrerequisite.IsInstalled())
var check = WinFspPrerequisite.Check();
if (!WinFspPrerequisite.ShouldPrompt(check, config.SkippedWinFspVersion))
{
return true;
}

var result = MessageBox.Show(
Loc.Get("Msg.WinFspMissingBody"),
Loc.Get("Msg.WinFspMissingTitle"),
var missing = check.Status == WinFspStatus.Missing;

var question = missing
? Loc.Format("Msg.WinFspMissingBody", WinFspPrerequisite.RequiredVersionText)
: Loc.Format("Msg.WinFspMismatchBody", check.InstalledVersion!, WinFspPrerequisite.RequiredVersionText);

var answer = MessageBox.Show(
question,
Loc.Get(missing ? "Msg.WinFspMissingTitle" : "Msg.WinFspTitle"),
MessageBoxButton.YesNo,
MessageBoxImage.Error);
missing ? MessageBoxImage.Error : MessageBoxImage.Question);

// An older build still works; only a missing runtime is fatal. When the user says no, or the update
// cannot be done (UAC declined, offline), remember the installed/required pair so it is not asked
// about again on every start.
void RememberSkip()
{
if (!missing)
{
var skipKey = WinFspPrerequisite.SkipKey(check.InstalledVersion!);
settings.Update(current => current with { SkippedWinFspVersion = skipKey });
}
}

if (result == MessageBoxResult.Yes)
if (answer != MessageBoxResult.Yes)
{
Process.Start(new ProcessStartInfo("https://github.com/winfsp/winfsp/releases/tag/v2.2B4") { UseShellExecute = true });
RememberSkip();
return !missing;
}

return false;
var installer = WinFspInstaller.CreateDefault();
WinFspInstallResult result;
System.Windows.Input.Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
try
{
result = await installer.InstallAsync();
}
finally
{
System.Windows.Input.Mouse.OverrideCursor = null;
}

switch (result)
{
case WinFspInstallResult.Success:
return true;
case WinFspInstallResult.RebootRequired:
MessageBox.Show(
Loc.Format("Msg.WinFspRebootRequired", WinFspPrerequisite.RequiredVersionText),
Loc.Get("Msg.WinFspTitle"),
MessageBoxButton.OK,
MessageBoxImage.Information);
return false;
case WinFspInstallResult.Declined:
// Nothing changed; explain why a missing runtime still ends the app instead of closing silently.
if (missing)
{
OfferManualWinFspDownload(Loc.Format("Msg.WinFspDeclined", WinFspPrerequisite.RequiredVersionText));
}

RememberSkip();
return !missing;
default:
Log.Warning("Automatic WinFsp install failed: {Error}", installer.LastError);
OfferManualWinFspDownload(Loc.Format("Msg.WinFspInstallFailed", installer.LastError ?? string.Empty));
RememberSkip();
return !missing;
}
}

/// <summary>
/// Shows <paramref name="message"/> and opens the WinFsp release page when the user agrees.
/// </summary>
private static void OfferManualWinFspDownload(string message)
{
if (MessageBox.Show(
message,
Loc.Get("Msg.WinFspTitle"),
MessageBoxButton.YesNo,
MessageBoxImage.Error) == MessageBoxResult.Yes)
{
Process.Start(new ProcessStartInfo(WinFspPrerequisite.ReleasePageUrl) { UseShellExecute = true });
}
}

/// <summary>
Expand Down
Loading
Loading