From 30ded117b7e20dbc2955048ba324b424f182c5be Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 11:39:40 +0000 Subject: [PATCH 1/5] fix(wsl): read package version from registry first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prefer HKLM Lxss\Msi\Version (same source WSL uses) so setup does not spawn wsl.exe before wslcsdk.dll is available. Keep CLI only as fallback; live SDK still uses Service::version. Co-authored-by: 绿岸公式 --- CHANGELOG.md | 5 +++ Cargo.toml | 1 + README.md | 2 +- src/wslc/mod.rs | 36 ++++++++++++----- src/wslc/sdk_fetch.rs | 93 +++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 122 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e2f09..820bf88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ## [Unreleased] +### 变更 + +- 启动时优先从注册表 `HKLM\...\Lxss\Msi\Version` 读取 WSL 包版本(与 WSL 自身一致),不再默认跑 `wsl --version`;仅在注册表缺失时回退 CLI。SDK 加载后的版本仍用 `wslc::Service::version()`。 + - Prefer the WSL MSI registry version for the first-launch gate; CLI is fallback only. Live SDK version still comes from `Service::version()`. + ## [0.1.2] - 2026-10-11 ### 修复 diff --git a/Cargo.toml b/Cargo.toml index 2d55283..6f9a2a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ ureq = { version = "2.12", default-features = true, features = ["json"] } zip = { version = "2", default-features = false, features = ["deflate"] } windows-sys = { version = "0.61", features = [ "Win32_Foundation", + "Win32_System_Registry", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging", ] } diff --git a/README.md b/README.md index 0f34998..3639136 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ |------|----------|----------| | ![总览](docs/screenshots/overview.png) | ![容器日志](docs/screenshots/containers-logs.png) | ![拉取镜像](docs/screenshots/images-pull.png) | -用独立会话 `wslc-desktop`(`%LOCALAPPDATA%\WSLcDesktop\`)管理容器、镜像与 VHD 卷,与默认 `wslc` CLI 会话隔离。Rust 绑定使用自维护 fork [`timeroute/wslc-rs`](https://github.com/timeroute/wslc-rs)(OpenContainer / Logs / named volumes)。MSI **不捆绑** `wslcsdk.dll`:首次启动以阻塞弹窗安装/更新 SDK,之后复用 `%LOCALAPPDATA%\WSLcDesktop\sdk\` 缓存(仅在版本不兼容或手动重试时再下载)。同一用户会话只允许一个实例。 +用独立会话 `wslc-desktop`(`%LOCALAPPDATA%\WSLcDesktop\`)管理容器、镜像与 VHD 卷,与默认 `wslc` CLI 会话隔离。Rust 绑定使用自维护 fork [`timeroute/wslc-rs`](https://github.com/timeroute/wslc-rs)(OpenContainer / Logs / named volumes)。MSI **不捆绑** `wslcsdk.dll`:首次启动以阻塞弹窗安装/更新 SDK,之后复用 `%LOCALAPPDATA%\WSLcDesktop\sdk\` 缓存(仅在版本不兼容或手动重试时再下载)。启动门禁优先读注册表 `Lxss\Msi\Version`(与 WSL 一致),SDK 连上后版本来自 `Service::version()`。同一用户会话只允许一个实例。 ## 安装(推荐) diff --git a/src/wslc/mod.rs b/src/wslc/mod.rs index 92123f3..836d5f5 100644 --- a/src/wslc/mod.rs +++ b/src/wslc/mod.rs @@ -223,21 +223,35 @@ impl WslcClient { "Checking WSL version (require {required} or newer)…" ))); - let output = sdk_fetch::probe_wsl_version_output(); - if let Some(out) = &output { - for line in out.lines() { - let line = line.trim(); - if !line.is_empty() { - let _ = tx.send(SdkSetupEvent::Log(format!("wsl: {line}"))); - } - } + // Prefer the MSI registry value WSL itself writes (no process spawn / + // UTF-16 pipe). Fall back to `wsl --version` only when that is absent. + // `wslc::Service::version()` needs wslcsdk.dll and cannot run yet. + let verdict = if let Some(version) = sdk_fetch::probe_wsl_registry_version() { + let _ = tx.send(SdkSetupEvent::Log(format!( + "WSL package version from registry (Lxss\\Msi): {version}" + ))); + sdk_version::judge_wsl_version(Some(&version)) } else { let _ = tx.send(SdkSetupEvent::Log( - "wsl --version produced no output (is WSL installed and on PATH?)".into(), + "Registry Lxss\\Msi\\Version missing; falling back to `wsl --version`".into(), )); - } + let output = sdk_fetch::probe_wsl_version_output(); + if let Some(out) = &output { + for line in out.lines() { + let line = line.trim(); + if !line.is_empty() { + let _ = tx.send(SdkSetupEvent::Log(format!("wsl: {line}"))); + } + } + } else { + let _ = tx.send(SdkSetupEvent::Log( + "wsl --version produced no output (is WSL installed and on PATH?)".into(), + )); + } + sdk_version::judge_wsl_version(output.as_deref()) + }; - match sdk_version::judge_wsl_version(output.as_deref()) { + match verdict { sdk_version::WslVersionVerdict::Ok(version) => { let _ = tx.send(SdkSetupEvent::Log(format!( "WSL {version} meets the {required}+ requirement" diff --git a/src/wslc/sdk_fetch.rs b/src/wslc/sdk_fetch.rs index f309b86..a73defc 100644 --- a/src/wslc/sdk_fetch.rs +++ b/src/wslc/sdk_fetch.rs @@ -215,7 +215,27 @@ fn native_arch_folder() -> &'static str { } } +/// Installed WSL package version from the machine registry (same source WSL uses). +/// +/// Reads `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Msi\Version`. +/// This avoids spawning `wsl.exe` (and its UTF-16 pipe output) before the SDK DLL +/// is available. `wslc::Service::version()` only works after `wslcsdk.dll` loads. +pub(crate) fn probe_wsl_registry_version() -> Option { + for subkey in [ + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Msi", + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\MSI", + ] { + if let Some(raw) = read_hk_lm_string(subkey, "Version") { + if let Some(v) = extract_version(&raw) { + return Some(v); + } + } + } + None +} + /// Raw `wsl --version` text, if the command ran and printed anything. +/// Fallback when the MSI registry value is missing (unusual / Store-only installs). pub(crate) fn probe_wsl_version_output() -> Option { for args in [ ["wsl", "--version"].as_slice(), @@ -231,8 +251,10 @@ pub(crate) fn probe_wsl_version_output() -> Option { } fn probe_host_version() -> Result { - // Prefer the same WSL version the setup gate uses so the active stamp does - // not flip between `wslc version` and `wsl --version` across launches. + // Prefer registry (stable, no process spawn) — same source as the setup gate. + if let Some(v) = probe_wsl_registry_version() { + return Ok(v); + } if let Some(out) = probe_wsl_version_output() { match judge_wsl_version(Some(&out)) { WslVersionVerdict::Ok(v) | WslVersionVerdict::TooOld { found: v } => { @@ -256,10 +278,75 @@ fn probe_host_version() -> Result { } } Err(anyhow!( - "could not read wslc/wsl version (is WSL installed and on PATH?)" + "could not read WSL package version from registry or CLI (is WSL installed?)" )) } +fn read_hk_lm_string(subkey: &str, value_name: &str) -> Option { + use windows_sys::Win32::Foundation::{ERROR_MORE_DATA, ERROR_SUCCESS}; + use windows_sys::Win32::System::Registry::{ + RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_LOCAL_MACHINE, KEY_READ, REG_SZ, + }; + + let subkey_w = wide(subkey); + let value_w = wide(value_name); + unsafe { + let mut hkey = std::ptr::null_mut(); + let status = RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + subkey_w.as_ptr(), + 0, + KEY_READ, + &mut hkey, + ); + if status != ERROR_SUCCESS { + return None; + } + + let mut kind = 0u32; + let mut byte_len = 0u32; + let query = RegQueryValueExW( + hkey, + value_w.as_ptr(), + std::ptr::null(), + &mut kind, + std::ptr::null_mut(), + &mut byte_len, + ); + if (query != ERROR_SUCCESS && query != ERROR_MORE_DATA) || kind != REG_SZ || byte_len < 2 { + let _ = RegCloseKey(hkey); + return None; + } + + let mut buf = vec![0u16; (byte_len as usize).div_ceil(2)]; + let mut buf_bytes = (buf.len() * 2) as u32; + let status = RegQueryValueExW( + hkey, + value_w.as_ptr(), + std::ptr::null(), + &mut kind, + buf.as_mut_ptr().cast::(), + &mut buf_bytes, + ); + let _ = RegCloseKey(hkey); + if status != ERROR_SUCCESS || kind != REG_SZ { + return None; + } + let units = buf_bytes as usize / 2; + let text = String::from_utf16_lossy(&buf[..units]); + let trimmed = text.trim_end_matches('\0').trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + } +} + +fn wide(s: &str) -> Vec { + s.encode_utf16().chain(std::iter::once(0)).collect() +} + fn run_hidden(args: &[&str]) -> Result { let (program, rest) = args .split_first() From 929fe2c2dc8089fbf475638dd83dc9fab489ca8a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 11:44:46 +0000 Subject: [PATCH 2/5] refactor(wsl): split sdk install into probe/cache/fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep ensure_sdk_dir as cache-hit → list → download. Move registry/CLI probing and stamp/DLL paths out so the fetch module stays focused. Co-authored-by: 绿岸公式 --- CHANGELOG.md | 2 + src/wslc/mod.rs | 53 ++++--- src/wslc/sdk_cache.rs | 79 +++++++++ src/wslc/sdk_fetch.rs | 361 +++++++----------------------------------- src/wslc/wsl_probe.rs | 194 +++++++++++++++++++++++ 5 files changed, 359 insertions(+), 330 deletions(-) create mode 100644 src/wslc/sdk_cache.rs create mode 100644 src/wslc/wsl_probe.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 820bf88..7245a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - 启动时优先从注册表 `HKLM\...\Lxss\Msi\Version` 读取 WSL 包版本(与 WSL 自身一致),不再默认跑 `wsl --version`;仅在注册表缺失时回退 CLI。SDK 加载后的版本仍用 `wslc::Service::version()`。 - Prefer the WSL MSI registry version for the first-launch gate; CLI is fallback only. Live SDK version still comes from `Service::version()`. +- 拆分 SDK 安装逻辑:`wsl_probe`(版本探测)、`sdk_cache`(本地 stamp/DLL)、`sdk_fetch`(NuGet 下载解压),主路径只保留「缓存命中 / 列包 / 下载」。 + - Split SDK install into probe / cache / fetch modules so the download path stays short. ## [0.1.2] - 2026-10-11 diff --git a/src/wslc/mod.rs b/src/wslc/mod.rs index 836d5f5..c954b3d 100644 --- a/src/wslc/mod.rs +++ b/src/wslc/mod.rs @@ -15,6 +15,8 @@ mod live; mod paths; mod prefs; #[cfg(windows)] +mod sdk_cache; +#[cfg(windows)] mod sdk_fetch; mod sdk_version; pub mod setup_log; @@ -22,6 +24,8 @@ pub mod setup_log; mod syspath; pub mod templates; mod types; +#[cfg(windows)] +mod wsl_probe; pub use prefs::SessionPrefs; pub use templates::{ContainerTemplate, TemplateEditError, TemplateStore, UpsertKind}; @@ -223,35 +227,34 @@ impl WslcClient { "Checking WSL version (require {required} or newer)…" ))); - // Prefer the MSI registry value WSL itself writes (no process spawn / - // UTF-16 pipe). Fall back to `wsl --version` only when that is absent. - // `wslc::Service::version()` needs wslcsdk.dll and cannot run yet. - let verdict = if let Some(version) = sdk_fetch::probe_wsl_registry_version() { - let _ = tx.send(SdkSetupEvent::Log(format!( - "WSL package version from registry (Lxss\\Msi): {version}" - ))); - sdk_version::judge_wsl_version(Some(&version)) - } else { - let _ = tx.send(SdkSetupEvent::Log( - "Registry Lxss\\Msi\\Version missing; falling back to `wsl --version`".into(), - )); - let output = sdk_fetch::probe_wsl_version_output(); - if let Some(out) = &output { - for line in out.lines() { - let line = line.trim(); - if !line.is_empty() { - let _ = tx.send(SdkSetupEvent::Log(format!("wsl: {line}"))); - } - } - } else { + // Registry first; CLI only if missing. SDK API needs wslcsdk.dll loaded. + let probe = wsl_probe::probe_wsl(); + match &probe { + wsl_probe::WslProbe::Registry { version } => { + let _ = tx.send(SdkSetupEvent::Log(format!( + "WSL package version from registry (Lxss\\Msi): {version}" + ))); + } + wsl_probe::WslProbe::Cli { raw } => { let _ = tx.send(SdkSetupEvent::Log( - "wsl --version produced no output (is WSL installed and on PATH?)".into(), + "Registry Lxss\\Msi\\Version missing; falling back to `wsl --version`".into(), )); + if raw.trim().is_empty() { + let _ = tx.send(SdkSetupEvent::Log( + "wsl --version produced no output (is WSL installed and on PATH?)".into(), + )); + } else { + for line in raw.lines() { + let line = line.trim(); + if !line.is_empty() { + let _ = tx.send(SdkSetupEvent::Log(format!("wsl: {line}"))); + } + } + } } - sdk_version::judge_wsl_version(output.as_deref()) - }; + } - match verdict { + match probe.verdict() { sdk_version::WslVersionVerdict::Ok(version) => { let _ = tx.send(SdkSetupEvent::Log(format!( "WSL {version} meets the {required}+ requirement" diff --git a/src/wslc/sdk_cache.rs b/src/wslc/sdk_cache.rs new file mode 100644 index 0000000..9fc75c2 --- /dev/null +++ b/src/wslc/sdk_cache.rs @@ -0,0 +1,79 @@ +//! On-disk cache for extracted `wslcsdk.dll` under `%LOCALAPPDATA%\WSLcDesktop\sdk`. + +#![cfg(windows)] + +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +use super::paths; +use super::sdk_version::cache_compatible; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActiveStamp { + pub host_version: String, + pub nuget_version: String, + pub arch: String, +} + +pub fn root() -> PathBuf { + paths::app_data_dir().join("sdk") +} + +pub fn native_arch() -> &'static str { + match std::env::consts::ARCH { + "x86_64" => "win-x64", + "aarch64" => "win-arm64", + _ => "win-x64", + } +} + +pub fn stamp_path() -> PathBuf { + root().join("active.json") +} + +pub fn package_dir(nuget: &str, arch: &str) -> PathBuf { + root().join(format!("nuget-{nuget}")).join(arch) +} + +pub fn dll_ready(dir: &Path) -> bool { + dir.join("wslcsdk.dll").is_file() +} + +/// Look up the active stamp's DLL when present and (optionally) host-compatible. +pub fn lookup(host: Option<&str>, arch: &str) -> Option<(PathBuf, ActiveStamp)> { + let raw = fs::read_to_string(stamp_path()).ok()?; + let stamp: ActiveStamp = serde_json::from_str(&raw).ok()?; + if stamp.arch != arch { + return None; + } + if let Some(host) = host + && !cache_compatible(host, &stamp.nuget_version) + { + return None; + } + let dir = package_dir(&stamp.nuget_version, arch); + dll_ready(&dir).then_some((dir, stamp)) +} + +pub fn write_stamp(host: &str, nuget: &str, arch: &str) -> Result<()> { + let path = stamp_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let stamp = ActiveStamp { + host_version: host.to_string(), + nuget_version: nuget.to_string(), + arch: arch.to_string(), + }; + fs::write(path, serde_json::to_string_pretty(&stamp)?)?; + Ok(()) +} + +pub fn clear_package(nuget: &str, arch: &str) { + let dll = package_dir(nuget, arch).join("wslcsdk.dll"); + let _ = fs::remove_file(dll); + let _ = fs::remove_file(stamp_path()); +} diff --git a/src/wslc/sdk_fetch.rs b/src/wslc/sdk_fetch.rs index a73defc..5b05012 100644 --- a/src/wslc/sdk_fetch.rs +++ b/src/wslc/sdk_fetch.rs @@ -1,28 +1,25 @@ -//! Download and cache `wslcsdk.dll` matched to the host `wslc` / WSL version. +//! Download `wslcsdk.dll` from NuGet when the local cache cannot serve the host. //! -//! WSL ships `wslc.exe` but not the C SDK DLL. On first launch (and whenever the -//! host version changes) we resolve the best [Microsoft.WSL.Containers] NuGet -//! package and extract `runtimes/win-*/native/wslcsdk.dll` under LocalAppData. +//! Cache lookup / stamps live in [`super::sdk_cache`]; host probing in +//! [`super::wsl_probe`]. This module only lists packages, downloads, and extracts. #![cfg(windows)] use std::fs::{self, File}; use std::io::{copy, Read, Write}; use std::path::{Path, PathBuf}; -use std::process::Command; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use anyhow::{anyhow, Context as _, Result}; use serde::{Deserialize, Serialize}; -use super::sdk_version::{ - cache_compatible, decode_command_output, extract_version, judge_wsl_version, pick_nuget_version, - WslVersionVerdict, -}; +use super::sdk_cache; +use super::sdk_version::pick_nuget_version; +use super::wsl_probe; const NUGET_INDEX: &str = "https://api.nuget.org/v3-flatcontainer/microsoft.wsl.containers/index.json"; -const INDEX_MAX_AGE: Duration = Duration::from_secs(60 * 60); // 1 hour +const INDEX_MAX_AGE: Duration = Duration::from_secs(60 * 60); #[derive(Debug, Clone)] pub enum FetchEvent { @@ -37,13 +34,6 @@ struct IndexCache { versions: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ActiveStamp { - host_version: String, - nuget_version: String, - arch: String, -} - /// Ensure a host-matched `wslcsdk.dll` is on disk; return its directory for PATH. /// /// Progress values are 0–100 for this download/cache stage. @@ -53,66 +43,52 @@ pub fn ensure_sdk_dir( ) -> Result { let mut emit = |ev: FetchEvent| on_event(ev); - emit(FetchEvent::Status("Detecting WSL / wslc version…".into())); + emit(FetchEvent::Status("Detecting WSL version…".into())); emit(FetchEvent::Progress(5.0)); - let host = match probe_host_version() { + let host = match wsl_probe::host_version() { Ok(v) => { emit(FetchEvent::Log(format!("Host version: {v}"))); v } Err(err) => { - emit(FetchEvent::Log(format!( - "Could not read wslc/wsl version: {err:#}" - ))); + emit(FetchEvent::Log(format!("Could not read WSL version: {err:#}"))); emit(FetchEvent::Log( - "Install or update WSL (`wsl --install` / `wsl --update`), ensure it is on PATH, then retry." - .into(), + "Install or update WSL (`wsl --install` / `wsl --update`), then retry.".into(), )); "unknown".into() } }; - let arch = native_arch_folder(); + let arch = sdk_cache::native_arch(); emit(FetchEvent::Log(format!("Native arch folder: {arch}"))); emit(FetchEvent::Progress(12.0)); - let stamp_path = sdk_root().join("active.json"); - - // Prefer a compatible on-disk DLL before touching NuGet. Host probe strings - // may drift across launches; only major-behind caches are rejected. - if !force_redownload { - if let Some((dir, stamp)) = try_active_sdk_dir(&stamp_path, Some(&host), arch) { - if stamp.host_version != host { - emit(FetchEvent::Log(format!( - "Host probe drifted ({} → {}); keeping cached SDK {}", - stamp.host_version, host, stamp.nuget_version - ))); - let _ = write_stamp(&stamp_path, &host, &stamp.nuget_version, arch); - } - emit(FetchEvent::Status("Using cached SDK".into())); + if !force_redownload + && let Some((dir, stamp)) = sdk_cache::lookup(Some(&host), arch) + { + if stamp.host_version != host { emit(FetchEvent::Log(format!( - "Cache hit (active stamp): {}", - dir.join("wslcsdk.dll").display() + "Host probe drifted ({} → {}); keeping cached SDK {}", + stamp.host_version, host, stamp.nuget_version ))); - emit(FetchEvent::Progress(100.0)); - return Ok(dir); + let _ = sdk_cache::write_stamp(&host, &stamp.nuget_version, arch); } + return cache_hit(&mut emit, &dir); } - emit(FetchEvent::Status("Listing Microsoft.WSL.Containers on NuGet…".into())); - let versions = match nuget_versions(&mut emit) { + emit(FetchEvent::Status( + "Listing Microsoft.WSL.Containers on NuGet…".into(), + )); + let versions = match list_nuget_versions(&mut emit) { Ok(versions) => versions, Err(err) => { - // Last resort: any previously activated DLL, even if host string drifted. - if !force_redownload { - if let Some((dir, _)) = try_active_sdk_dir(&stamp_path, None, arch) { - emit(FetchEvent::Log(format!( - "NuGet unavailable ({err:#}); reusing active SDK cache" - ))); - emit(FetchEvent::Status("Using cached SDK".into())); - emit(FetchEvent::Progress(100.0)); - return Ok(dir); - } + if !force_redownload + && let Some((dir, _)) = sdk_cache::lookup(None, arch) + { + emit(FetchEvent::Log(format!( + "NuGet unavailable ({err:#}); reusing active SDK cache" + ))); + return cache_hit(&mut emit, &dir); } return Err(err).context("list Microsoft.WSL.Containers versions"); } @@ -129,46 +105,23 @@ pub fn ensure_sdk_dir( "Selected NuGet package version: {nuget} (host {host})" ))); - let dir = sdk_root().join(format!("nuget-{nuget}")).join(arch); - let dll = dir.join("wslcsdk.dll"); - - if force_redownload && dll.is_file() { + let dir = sdk_cache::package_dir(&nuget, arch); + if force_redownload && sdk_cache::dll_ready(&dir) { emit(FetchEvent::Log(format!( - "Retry requested — removing cached DLL at {}", - dll.display() + "Retry requested — clearing cached DLL for {nuget}" ))); - let _ = fs::remove_file(&dll); - let _ = fs::remove_file(&stamp_path); + sdk_cache::clear_package(&nuget, arch); } - if dll.is_file() { - if let Ok(raw) = fs::read_to_string(&stamp_path) { - if let Ok(stamp) = serde_json::from_str::(&raw) { - if stamp.host_version == host - && stamp.nuget_version == nuget - && stamp.arch == arch - { - emit(FetchEvent::Status("Using cached SDK".into())); - emit(FetchEvent::Log(format!( - "Cache hit: {}", - dll.display() - ))); - emit(FetchEvent::Progress(100.0)); - return Ok(dir); - } - } - } - // Package already extracted; host string may have changed. - if nuget_dir_ready(&dir) { - emit(FetchEvent::Status("Refreshing SDK stamp…".into())); - emit(FetchEvent::Log(format!( - "Reusing extracted DLL at {}", - dll.display() - ))); - write_stamp(&stamp_path, &host, &nuget, arch)?; - emit(FetchEvent::Progress(100.0)); - return Ok(dir); - } + if sdk_cache::dll_ready(&dir) { + emit(FetchEvent::Status("Refreshing SDK stamp…".into())); + emit(FetchEvent::Log(format!( + "Reusing extracted DLL at {}", + dir.join("wslcsdk.dll").display() + ))); + sdk_cache::write_stamp(&host, &nuget, arch)?; + emit(FetchEvent::Progress(100.0)); + return Ok(dir); } emit(FetchEvent::Status(format!( @@ -177,224 +130,24 @@ pub fn ensure_sdk_dir( emit(FetchEvent::Progress(28.0)); download_nuget_dll(&nuget, arch, &dir, &mut emit) .with_context(|| format!("download Microsoft.WSL.Containers {nuget} ({arch})"))?; - write_stamp(&stamp_path, &host, &nuget, arch)?; + sdk_cache::write_stamp(&host, &nuget, arch)?; emit(FetchEvent::Status("SDK installed".into())); emit(FetchEvent::Progress(100.0)); Ok(dir) } -fn write_stamp(path: &Path, host: &str, nuget: &str, arch: &str) -> Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - let stamp = ActiveStamp { - host_version: host.to_string(), - nuget_version: nuget.to_string(), - arch: arch.to_string(), - }; - fs::write(path, serde_json::to_string_pretty(&stamp)?)?; - Ok(()) -} - -fn nuget_dir_ready(dir: &Path) -> bool { - dir.join("wslcsdk.dll").is_file() -} - -fn sdk_root() -> PathBuf { - let base = std::env::var_os("LOCALAPPDATA") - .map(PathBuf::from) - .unwrap_or_else(|| std::env::temp_dir().join("WSLcDesktop")); - base.join("WSLcDesktop").join("sdk") -} - -fn native_arch_folder() -> &'static str { - match std::env::consts::ARCH { - "x86_64" => "win-x64", - "aarch64" => "win-arm64", - _ => "win-x64", - } -} - -/// Installed WSL package version from the machine registry (same source WSL uses). -/// -/// Reads `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Msi\Version`. -/// This avoids spawning `wsl.exe` (and its UTF-16 pipe output) before the SDK DLL -/// is available. `wslc::Service::version()` only works after `wslcsdk.dll` loads. -pub(crate) fn probe_wsl_registry_version() -> Option { - for subkey in [ - r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Msi", - r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\MSI", - ] { - if let Some(raw) = read_hk_lm_string(subkey, "Version") { - if let Some(v) = extract_version(&raw) { - return Some(v); - } - } - } - None -} - -/// Raw `wsl --version` text, if the command ran and printed anything. -/// Fallback when the MSI registry value is missing (unusual / Store-only installs). -pub(crate) fn probe_wsl_version_output() -> Option { - for args in [ - ["wsl", "--version"].as_slice(), - ["wsl.exe", "--version"].as_slice(), - ] { - if let Ok(out) = run_hidden(args) { - if !out.trim().is_empty() { - return Some(out); - } - } - } - None -} - -fn probe_host_version() -> Result { - // Prefer registry (stable, no process spawn) — same source as the setup gate. - if let Some(v) = probe_wsl_registry_version() { - return Ok(v); - } - if let Some(out) = probe_wsl_version_output() { - match judge_wsl_version(Some(&out)) { - WslVersionVerdict::Ok(v) | WslVersionVerdict::TooOld { found: v } => { - return Ok(v); - } - WslVersionVerdict::Missing => { - if let Some(v) = extract_version(&out) { - return Ok(v); - } - } - } - } - for args in [ - ["wslc", "version"].as_slice(), - ["wslc.exe", "version"].as_slice(), - ] { - if let Ok(out) = run_hidden(args) { - if let Some(v) = extract_version(&out) { - return Ok(v); - } - } - } - Err(anyhow!( - "could not read WSL package version from registry or CLI (is WSL installed?)" - )) -} - -fn read_hk_lm_string(subkey: &str, value_name: &str) -> Option { - use windows_sys::Win32::Foundation::{ERROR_MORE_DATA, ERROR_SUCCESS}; - use windows_sys::Win32::System::Registry::{ - RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_LOCAL_MACHINE, KEY_READ, REG_SZ, - }; - - let subkey_w = wide(subkey); - let value_w = wide(value_name); - unsafe { - let mut hkey = std::ptr::null_mut(); - let status = RegOpenKeyExW( - HKEY_LOCAL_MACHINE, - subkey_w.as_ptr(), - 0, - KEY_READ, - &mut hkey, - ); - if status != ERROR_SUCCESS { - return None; - } - - let mut kind = 0u32; - let mut byte_len = 0u32; - let query = RegQueryValueExW( - hkey, - value_w.as_ptr(), - std::ptr::null(), - &mut kind, - std::ptr::null_mut(), - &mut byte_len, - ); - if (query != ERROR_SUCCESS && query != ERROR_MORE_DATA) || kind != REG_SZ || byte_len < 2 { - let _ = RegCloseKey(hkey); - return None; - } - - let mut buf = vec![0u16; (byte_len as usize).div_ceil(2)]; - let mut buf_bytes = (buf.len() * 2) as u32; - let status = RegQueryValueExW( - hkey, - value_w.as_ptr(), - std::ptr::null(), - &mut kind, - buf.as_mut_ptr().cast::(), - &mut buf_bytes, - ); - let _ = RegCloseKey(hkey); - if status != ERROR_SUCCESS || kind != REG_SZ { - return None; - } - let units = buf_bytes as usize / 2; - let text = String::from_utf16_lossy(&buf[..units]); - let trimmed = text.trim_end_matches('\0').trim(); - if trimmed.is_empty() { - None - } else { - Some(trimmed.to_string()) - } - } -} - -fn wide(s: &str) -> Vec { - s.encode_utf16().chain(std::iter::once(0)).collect() -} - -fn run_hidden(args: &[&str]) -> Result { - let (program, rest) = args - .split_first() - .ok_or_else(|| anyhow!("empty command"))?; - let mut cmd = Command::new(program); - cmd.args(rest); - { - use std::os::windows::process::CommandExt as _; - const CREATE_NO_WINDOW: u32 = 0x0800_0000; - cmd.creation_flags(CREATE_NO_WINDOW); - } - let output = cmd - .output() - .with_context(|| format!("spawn `{program}`"))?; - // `wsl.exe` writes UTF-16LE when stdout is a pipe, not a console. - let mut text = decode_command_output(&output.stdout); - if text.trim().is_empty() { - text = decode_command_output(&output.stderr); - } - if !output.status.success() && text.trim().is_empty() { - return Err(anyhow!("`{program}` exited {}", output.status)); - } - Ok(text) -} - -fn try_active_sdk_dir( - stamp_path: &Path, - host: Option<&str>, - arch: &str, -) -> Option<(PathBuf, ActiveStamp)> { - let raw = fs::read_to_string(stamp_path).ok()?; - let stamp: ActiveStamp = serde_json::from_str(&raw).ok()?; - if stamp.arch != arch { - return None; - } - if let Some(host) = host - && !cache_compatible(host, &stamp.nuget_version) - { - return None; - } - let dir = sdk_root() - .join(format!("nuget-{}", stamp.nuget_version)) - .join(arch); - nuget_dir_ready(&dir).then_some((dir, stamp)) +fn cache_hit(emit: &mut dyn FnMut(FetchEvent), dir: &Path) -> Result { + emit(FetchEvent::Status("Using cached SDK".into())); + emit(FetchEvent::Log(format!( + "Cache hit: {}", + dir.join("wslcsdk.dll").display() + ))); + emit(FetchEvent::Progress(100.0)); + Ok(dir.to_path_buf()) } -fn nuget_versions(emit: &mut dyn FnMut(FetchEvent)) -> Result> { - let cache_path = sdk_root().join("nuget-index.json"); +fn list_nuget_versions(emit: &mut dyn FnMut(FetchEvent)) -> Result> { + let cache_path = sdk_cache::root().join("nuget-index.json"); let cached = fs::read_to_string(&cache_path) .ok() .and_then(|raw| serde_json::from_str::(&raw).ok()) @@ -434,7 +187,7 @@ fn nuget_versions(emit: &mut dyn FnMut(FetchEvent)) -> Result> { match fetched { Ok(versions) => { - fs::create_dir_all(sdk_root())?; + fs::create_dir_all(sdk_cache::root())?; let cache = IndexCache { fetched_unix: now_unix(), versions: versions.clone(), @@ -495,7 +248,6 @@ fn download_nuget_dll( written += n as u64; if total > 0 { let frac = written as f32 / total as f32; - // Map download into 28–75%. emit(FetchEvent::Progress(28.0 + frac * 47.0)); } else if written % (512 * 1024) < 64 * 1024 { emit(FetchEvent::Log(format!( @@ -523,7 +275,6 @@ fn download_nuget_dll( let mut out = File::create(&dll_path)?; copy(&mut entry, &mut out)?; out.flush()?; - let _ = fs::remove_file(&tmp_nupkg); if !dll_path.is_file() { diff --git a/src/wslc/wsl_probe.rs b/src/wslc/wsl_probe.rs new file mode 100644 index 0000000..11f7a10 --- /dev/null +++ b/src/wslc/wsl_probe.rs @@ -0,0 +1,194 @@ +//! Resolve the installed WSL package version before `wslcsdk.dll` is available. +//! +//! Order: machine registry (same source WSL uses) → `wsl --version` → `wslc version`. +//! After the SDK loads, callers should use `wslc::Service::version()` instead. + +#![cfg(windows)] + +use std::process::Command; + +use anyhow::{anyhow, Context as _, Result}; + +use super::sdk_version::{ + decode_command_output, extract_version, judge_wsl_version, WslVersionVerdict, +}; + +/// How the host WSL package version was obtained. +#[derive(Debug, Clone)] +pub enum WslProbe { + /// `HKLM\...\Lxss\Msi\Version` + Registry { version: String }, + /// Raw `wsl --version` text (may be empty if the command failed). + Cli { raw: String }, +} + +impl WslProbe { + pub fn verdict(&self) -> WslVersionVerdict { + match self { + Self::Registry { version } => judge_wsl_version(Some(version)), + Self::Cli { raw } => { + let text = raw.trim(); + judge_wsl_version(if text.is_empty() { None } else { Some(text) }) + } + } + } +} + +/// Probe for the first-launch WSL gate (registry, else CLI). +pub fn probe_wsl() -> WslProbe { + if let Some(version) = registry_version() { + return WslProbe::Registry { version }; + } + WslProbe::Cli { + raw: cli_wsl_version_output().unwrap_or_default(), + } +} + +/// Best-effort host version string for NuGet package selection. +pub fn host_version() -> Result { + match probe_wsl() { + WslProbe::Registry { version } => Ok(version), + WslProbe::Cli { raw } => match judge_wsl_version(Some(raw.trim()).filter(|s| !s.is_empty())) + { + WslVersionVerdict::Ok(v) | WslVersionVerdict::TooOld { found: v } => Ok(v), + WslVersionVerdict::Missing => { + if let Some(v) = extract_version(&raw) { + return Ok(v); + } + wslc_cli_version().ok_or_else(|| { + anyhow!( + "could not read WSL package version from registry or CLI (is WSL installed?)" + ) + }) + } + }, + } +} + +fn registry_version() -> Option { + for subkey in [ + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Msi", + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\MSI", + ] { + if let Some(raw) = read_hk_lm_string(subkey, "Version") + && let Some(v) = extract_version(&raw) + { + return Some(v); + } + } + None +} + +fn cli_wsl_version_output() -> Option { + for args in [ + ["wsl", "--version"].as_slice(), + ["wsl.exe", "--version"].as_slice(), + ] { + if let Ok(out) = run_hidden(args) + && !out.trim().is_empty() + { + return Some(out); + } + } + None +} + +fn wslc_cli_version() -> Option { + for args in [ + ["wslc", "version"].as_slice(), + ["wslc.exe", "version"].as_slice(), + ] { + if let Ok(out) = run_hidden(args) + && let Some(v) = extract_version(&out) + { + return Some(v); + } + } + None +} + +fn read_hk_lm_string(subkey: &str, value_name: &str) -> Option { + use windows_sys::Win32::Foundation::{ERROR_MORE_DATA, ERROR_SUCCESS}; + use windows_sys::Win32::System::Registry::{ + RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_LOCAL_MACHINE, KEY_READ, REG_SZ, + }; + + let subkey_w = wide(subkey); + let value_w = wide(value_name); + unsafe { + let mut hkey = std::ptr::null_mut(); + if RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + subkey_w.as_ptr(), + 0, + KEY_READ, + &mut hkey, + ) != ERROR_SUCCESS + { + return None; + } + + let mut kind = 0u32; + let mut byte_len = 0u32; + let query = RegQueryValueExW( + hkey, + value_w.as_ptr(), + std::ptr::null(), + &mut kind, + std::ptr::null_mut(), + &mut byte_len, + ); + if (query != ERROR_SUCCESS && query != ERROR_MORE_DATA) || kind != REG_SZ || byte_len < 2 { + let _ = RegCloseKey(hkey); + return None; + } + + let mut buf = vec![0u16; (byte_len as usize).div_ceil(2)]; + let mut buf_bytes = (buf.len() * 2) as u32; + let status = RegQueryValueExW( + hkey, + value_w.as_ptr(), + std::ptr::null(), + &mut kind, + buf.as_mut_ptr().cast::(), + &mut buf_bytes, + ); + let _ = RegCloseKey(hkey); + if status != ERROR_SUCCESS || kind != REG_SZ { + return None; + } + let units = buf_bytes as usize / 2; + let text = String::from_utf16_lossy(&buf[..units]); + let trimmed = text.trim_end_matches('\0').trim(); + (!trimmed.is_empty()).then(|| trimmed.to_string()) + } +} + +fn wide(s: &str) -> Vec { + s.encode_utf16().chain(std::iter::once(0)).collect() +} + +fn run_hidden(args: &[&str]) -> Result { + let (program, rest) = args + .split_first() + .ok_or_else(|| anyhow!("empty command"))?; + let mut cmd = Command::new(program); + cmd.args(rest); + { + use std::os::windows::process::CommandExt as _; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + let output = cmd + .output() + .with_context(|| format!("spawn `{program}`"))?; + // `wsl.exe` writes UTF-16LE when stdout is a pipe, not a console. + let mut text = decode_command_output(&output.stdout); + if text.trim().is_empty() { + text = decode_command_output(&output.stderr); + } + if !output.status.success() && text.trim().is_empty() { + return Err(anyhow!("`{program}` exited {}", output.status)); + } + Ok(text) +} From e16d507e22ef14e8ef917398151efd1de0217bbe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 11:46:48 +0000 Subject: [PATCH 3/5] refactor(wsl): drop CLI version probe, registry only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WSL 2.9.3+ always writes Lxss\Msi\Version; missing key means not installed. Removes wsl.exe spawn and UTF-16 pipe path. Co-authored-by: 绿岸公式 --- CHANGELOG.md | 4 +- README.md | 2 +- src/i18n.rs | 2 +- src/wslc/mod.rs | 39 ++++--------- src/wslc/wsl_probe.rs | 128 +++++------------------------------------- 5 files changed, 31 insertions(+), 144 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7245a7f..8e596df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ ### 变更 -- 启动时优先从注册表 `HKLM\...\Lxss\Msi\Version` 读取 WSL 包版本(与 WSL 自身一致),不再默认跑 `wsl --version`;仅在注册表缺失时回退 CLI。SDK 加载后的版本仍用 `wslc::Service::version()`。 - - Prefer the WSL MSI registry version for the first-launch gate; CLI is fallback only. Live SDK version still comes from `Service::version()`. +- 启动门禁只读注册表 `HKLM\...\Lxss\Msi\Version`(与 WSL 自身一致),不再调用 `wsl --version`。缺键即视为未安装。SDK 加载后的版本仍用 `wslc::Service::version()`。 + - First-launch WSL check reads only the MSI registry version; no CLI. Live SDK version still comes from `Service::version()`. - 拆分 SDK 安装逻辑:`wsl_probe`(版本探测)、`sdk_cache`(本地 stamp/DLL)、`sdk_fetch`(NuGet 下载解压),主路径只保留「缓存命中 / 列包 / 下载」。 - Split SDK install into probe / cache / fetch modules so the download path stays short. diff --git a/README.md b/README.md index 3639136..58cc3ec 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ |------|----------|----------| | ![总览](docs/screenshots/overview.png) | ![容器日志](docs/screenshots/containers-logs.png) | ![拉取镜像](docs/screenshots/images-pull.png) | -用独立会话 `wslc-desktop`(`%LOCALAPPDATA%\WSLcDesktop\`)管理容器、镜像与 VHD 卷,与默认 `wslc` CLI 会话隔离。Rust 绑定使用自维护 fork [`timeroute/wslc-rs`](https://github.com/timeroute/wslc-rs)(OpenContainer / Logs / named volumes)。MSI **不捆绑** `wslcsdk.dll`:首次启动以阻塞弹窗安装/更新 SDK,之后复用 `%LOCALAPPDATA%\WSLcDesktop\sdk\` 缓存(仅在版本不兼容或手动重试时再下载)。启动门禁优先读注册表 `Lxss\Msi\Version`(与 WSL 一致),SDK 连上后版本来自 `Service::version()`。同一用户会话只允许一个实例。 +用独立会话 `wslc-desktop`(`%LOCALAPPDATA%\WSLcDesktop\`)管理容器、镜像与 VHD 卷,与默认 `wslc` CLI 会话隔离。Rust 绑定使用自维护 fork [`timeroute/wslc-rs`](https://github.com/timeroute/wslc-rs)(OpenContainer / Logs / named volumes)。MSI **不捆绑** `wslcsdk.dll`:首次启动以阻塞弹窗安装/更新 SDK,之后复用 `%LOCALAPPDATA%\WSLcDesktop\sdk\` 缓存(仅在版本不兼容或手动重试时再下载)。启动门禁只读注册表 `Lxss\Msi\Version`(与 WSL 一致),SDK 连上后版本来自 `Service::version()`。同一用户会话只允许一个实例。 ## 安装(推荐) diff --git a/src/i18n.rs b/src/i18n.rs index 9076b29..c3501fd 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -357,7 +357,7 @@ pub static EN: Strings = Strings { sdk_setup_step_checking: "Checking", sdk_setup_step_passed: "Passed", sdk_setup_step_failed: "Failed", - sdk_wsl_missing: "WSL is missing, or its version could not be read. Install or update WSL with `wsl --update`, then retry.", + sdk_wsl_missing: "WSL is missing, or its package version was not found in the registry. Install or update WSL with `wsl --update`, then retry.", sdk_wsl_too_old: "WSL {found} is older than {required}. Run `wsl --update`, then retry.", sdk_wsl_ok: "WSL {version} (requires {required}+)", sdk_install_ok: "SDK {version}", diff --git a/src/wslc/mod.rs b/src/wslc/mod.rs index c954b3d..e0da573 100644 --- a/src/wslc/mod.rs +++ b/src/wslc/mod.rs @@ -227,34 +227,19 @@ impl WslcClient { "Checking WSL version (require {required} or newer)…" ))); - // Registry first; CLI only if missing. SDK API needs wslcsdk.dll loaded. - let probe = wsl_probe::probe_wsl(); - match &probe { - wsl_probe::WslProbe::Registry { version } => { - let _ = tx.send(SdkSetupEvent::Log(format!( - "WSL package version from registry (Lxss\\Msi): {version}" - ))); - } - wsl_probe::WslProbe::Cli { raw } => { - let _ = tx.send(SdkSetupEvent::Log( - "Registry Lxss\\Msi\\Version missing; falling back to `wsl --version`".into(), - )); - if raw.trim().is_empty() { - let _ = tx.send(SdkSetupEvent::Log( - "wsl --version produced no output (is WSL installed and on PATH?)".into(), - )); - } else { - for line in raw.lines() { - let line = line.trim(); - if !line.is_empty() { - let _ = tx.send(SdkSetupEvent::Log(format!("wsl: {line}"))); - } - } - } - } + // Registry only (same key WSL writes). No CLI — missing key means not installed. + let found = wsl_probe::package_version(); + if let Some(version) = &found { + let _ = tx.send(SdkSetupEvent::Log(format!( + "WSL package version from registry (Lxss\\Msi): {version}" + ))); + } else { + let _ = tx.send(SdkSetupEvent::Log( + "Registry Lxss\\Msi\\Version missing (WSL MSI not installed?)".into(), + )); } - match probe.verdict() { + match sdk_version::judge_wsl_version(found.as_deref()) { sdk_version::WslVersionVerdict::Ok(version) => { let _ = tx.send(SdkSetupEvent::Log(format!( "WSL {version} meets the {required}+ requirement" @@ -272,7 +257,7 @@ impl WslcClient { state: SetupStepState::Failed(SetupFault::WslMissing), }); let _ = tx.send(SdkSetupEvent::Finished(Err( - "WSL is missing or its version could not be read".into(), + "WSL is missing or its version could not be read from the registry".into(), ))); false } diff --git a/src/wslc/wsl_probe.rs b/src/wslc/wsl_probe.rs index 11f7a10..9c8e4d3 100644 --- a/src/wslc/wsl_probe.rs +++ b/src/wslc/wsl_probe.rs @@ -1,71 +1,16 @@ -//! Resolve the installed WSL package version before `wslcsdk.dll` is available. +//! Read the installed WSL package version from the machine registry. //! -//! Order: machine registry (same source WSL uses) → `wsl --version` → `wslc version`. -//! After the SDK loads, callers should use `wslc::Service::version()` instead. +//! Same source WSL itself uses: `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Msi\Version`. +//! No CLI. After `wslcsdk.dll` loads, use `wslc::Service::version()` instead. #![cfg(windows)] -use std::process::Command; +use anyhow::{anyhow, Result}; -use anyhow::{anyhow, Context as _, Result}; +use super::sdk_version::extract_version; -use super::sdk_version::{ - decode_command_output, extract_version, judge_wsl_version, WslVersionVerdict, -}; - -/// How the host WSL package version was obtained. -#[derive(Debug, Clone)] -pub enum WslProbe { - /// `HKLM\...\Lxss\Msi\Version` - Registry { version: String }, - /// Raw `wsl --version` text (may be empty if the command failed). - Cli { raw: String }, -} - -impl WslProbe { - pub fn verdict(&self) -> WslVersionVerdict { - match self { - Self::Registry { version } => judge_wsl_version(Some(version)), - Self::Cli { raw } => { - let text = raw.trim(); - judge_wsl_version(if text.is_empty() { None } else { Some(text) }) - } - } - } -} - -/// Probe for the first-launch WSL gate (registry, else CLI). -pub fn probe_wsl() -> WslProbe { - if let Some(version) = registry_version() { - return WslProbe::Registry { version }; - } - WslProbe::Cli { - raw: cli_wsl_version_output().unwrap_or_default(), - } -} - -/// Best-effort host version string for NuGet package selection. -pub fn host_version() -> Result { - match probe_wsl() { - WslProbe::Registry { version } => Ok(version), - WslProbe::Cli { raw } => match judge_wsl_version(Some(raw.trim()).filter(|s| !s.is_empty())) - { - WslVersionVerdict::Ok(v) | WslVersionVerdict::TooOld { found: v } => Ok(v), - WslVersionVerdict::Missing => { - if let Some(v) = extract_version(&raw) { - return Ok(v); - } - wslc_cli_version().ok_or_else(|| { - anyhow!( - "could not read WSL package version from registry or CLI (is WSL installed?)" - ) - }) - } - }, - } -} - -fn registry_version() -> Option { +/// Installed WSL package version (`x.y.z`), if the MSI registry value is present. +pub fn package_version() -> Option { for subkey in [ r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Msi", r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\MSI", @@ -79,32 +24,14 @@ fn registry_version() -> Option { None } -fn cli_wsl_version_output() -> Option { - for args in [ - ["wsl", "--version"].as_slice(), - ["wsl.exe", "--version"].as_slice(), - ] { - if let Ok(out) = run_hidden(args) - && !out.trim().is_empty() - { - return Some(out); - } - } - None -} - -fn wslc_cli_version() -> Option { - for args in [ - ["wslc", "version"].as_slice(), - ["wslc.exe", "version"].as_slice(), - ] { - if let Ok(out) = run_hidden(args) - && let Some(v) = extract_version(&out) - { - return Some(v); - } - } - None +/// Host version for NuGet package selection. +pub fn host_version() -> Result { + package_version().ok_or_else(|| { + anyhow!( + "WSL package version not found in registry (Lxss\\Msi\\Version); \ + install or update WSL with `wsl --update`" + ) + }) } fn read_hk_lm_string(subkey: &str, value_name: &str) -> Option { @@ -167,28 +94,3 @@ fn read_hk_lm_string(subkey: &str, value_name: &str) -> Option { fn wide(s: &str) -> Vec { s.encode_utf16().chain(std::iter::once(0)).collect() } - -fn run_hidden(args: &[&str]) -> Result { - let (program, rest) = args - .split_first() - .ok_or_else(|| anyhow!("empty command"))?; - let mut cmd = Command::new(program); - cmd.args(rest); - { - use std::os::windows::process::CommandExt as _; - const CREATE_NO_WINDOW: u32 = 0x0800_0000; - cmd.creation_flags(CREATE_NO_WINDOW); - } - let output = cmd - .output() - .with_context(|| format!("spawn `{program}`"))?; - // `wsl.exe` writes UTF-16LE when stdout is a pipe, not a console. - let mut text = decode_command_output(&output.stdout); - if text.trim().is_empty() { - text = decode_command_output(&output.stderr); - } - if !output.status.success() && text.trim().is_empty() { - return Err(anyhow!("`{program}` exited {}", output.status)); - } - Ok(text) -} From f807cb6d1985accab3c75414420f25dcf37ecd4b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 11:48:31 +0000 Subject: [PATCH 4/5] refactor(wsl): drop CLI UTF-16/text parsers from sdk_version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registry already yields x.y.z[.w]; keep SemVer, package gate, and NuGet matching only. Co-authored-by: 绿岸公式 --- CHANGELOG.md | 2 + src/i18n.rs | 2 +- src/wslc/mod.rs | 2 +- src/wslc/sdk_version.rs | 413 +++++----------------------------------- src/wslc/wsl_probe.rs | 6 +- 5 files changed, 50 insertions(+), 375 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e596df..d997283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - First-launch WSL check reads only the MSI registry version; no CLI. Live SDK version still comes from `Service::version()`. - 拆分 SDK 安装逻辑:`wsl_probe`(版本探测)、`sdk_cache`(本地 stamp/DLL)、`sdk_fetch`(NuGet 下载解压),主路径只保留「缓存命中 / 列包 / 下载」。 - Split SDK install into probe / cache / fetch modules so the download path stays short. +- 删除面向 `wsl --version` 的 UTF-16 / 多行文本解析;`sdk_version` 只保留 SemVer 与 NuGet 匹配。 + - Drop CLI text/UTF-16 parsing; `sdk_version` keeps SemVer + NuGet matching only. ## [0.1.2] - 2026-10-11 diff --git a/src/i18n.rs b/src/i18n.rs index c3501fd..d9399d4 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -568,7 +568,7 @@ pub static ZH: Strings = Strings { sdk_setup_step_checking: "检查中", sdk_setup_step_passed: "通过", sdk_setup_step_failed: "失败", - sdk_wsl_missing: "未检测到 WSL,或无法读取版本。请运行 `wsl --update` 安装或更新后再重试。", + sdk_wsl_missing: "未检测到 WSL,或注册表中没有包版本。请运行 `wsl --update` 安装或更新后再重试。", sdk_wsl_too_old: "WSL {found} 低于要求的 {required}。请运行 `wsl --update` 后再重试。", sdk_wsl_ok: "WSL {version}(要求 {required}+)", sdk_install_ok: "SDK {version}", diff --git a/src/wslc/mod.rs b/src/wslc/mod.rs index e0da573..f90388b 100644 --- a/src/wslc/mod.rs +++ b/src/wslc/mod.rs @@ -239,7 +239,7 @@ impl WslcClient { )); } - match sdk_version::judge_wsl_version(found.as_deref()) { + match sdk_version::judge_package_version(found.as_deref()) { sdk_version::WslVersionVerdict::Ok(version) => { let _ = tx.send(SdkSetupEvent::Log(format!( "WSL {version} meets the {required}+ requirement" diff --git a/src/wslc/sdk_version.rs b/src/wslc/sdk_version.rs index 1dda791..a95ab49 100644 --- a/src/wslc/sdk_version.rs +++ b/src/wslc/sdk_version.rs @@ -1,9 +1,4 @@ -//! Host / NuGet version matching (no Windows-only deps). -//! -//! `wsl.exe --version` writes UTF-16LE when stdout is a pipe (no console). -//! Reading those bytes as UTF-8 leaves NUL gaps (`W\0S\0L`) or, once NULs are -//! shown as spaces, `W S L Hr,g: 3 . 0 . 1 . 0`. `版本` is the UTF-16LE -//! code units `48 72 2C 67`. Either form hides `3.0.1.0` from a naive scan. +//! SemVer helpers for the WSL package gate and NuGet SDK matching. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct SemVer { @@ -13,8 +8,9 @@ pub struct SemVer { } impl SemVer { + /// Parse `x.y`, `x.y.z`, or `x.y.z.w` (components after patch ignored). pub fn parse(s: &str) -> Option { - let mut parts = s.split('.'); + let mut parts = s.trim().split('.'); let major = parts.next()?.parse().ok()?; let minor = parts.next()?.parse().ok()?; let patch = parts.next().unwrap_or("0").parse().ok()?; @@ -47,101 +43,27 @@ pub enum WslVersionVerdict { TooOld { found: String }, } -/// Decode a captured stdout/stderr buffer. -/// -/// Order: UTF-16 BOM, then a UTF-16LE NUL pattern, then strip NULs that sit -/// between ASCII bytes, then lossy UTF-8. -pub fn decode_command_output(bytes: &[u8]) -> String { - if bytes.is_empty() { - return String::new(); - } - if let Some(text) = decode_utf16(bytes) - && plausible_text(&text) - { - return text; - } - if let Some(text) = strip_nuls_between_ascii(bytes) { - return text; - } - String::from_utf8_lossy(bytes).into_owned() -} - -/// Classify `wsl --version` output. -/// -/// Prefers a line labeled `WSL` (English or `WSL 版本`) and ignores `WSLg`. -/// A single bare version token is accepted. Multiple unlabeled versions are -/// treated as unreadable so a kernel version cannot pass the check by accident. -/// NUL-separated and spaced UTF-16 misreads are repaired before that scan. -/// `3.0.1.0` satisfies `>= 2.9.3` (the fourth component is ignored). +/// Classify a registry package version string (`3.0.1` / `3.0.1.0`). #[cfg_attr(not(windows), allow(dead_code))] -pub fn judge_wsl_version(output: Option<&str>) -> WslVersionVerdict { - let Some(text) = output.map(str::trim).filter(|s| !s.is_empty()) else { +pub fn judge_package_version(version: Option<&str>) -> WslVersionVerdict { + let Some(raw) = version.map(str::trim).filter(|s| !s.is_empty()) else { return WslVersionVerdict::Missing; }; - let normalized = normalize_command_text(text); - let version = labeled_wsl_version(&normalized).or_else(|| sole_version(&normalized)); - match version { - Some(v) => match SemVer::parse(&v) { - Some(sv) if sv >= MIN_WSL_VERSION => WslVersionVerdict::Ok(v), - Some(_) => WslVersionVerdict::TooOld { found: v }, - None => WslVersionVerdict::Missing, - }, - None => WslVersionVerdict::Missing, - } -} - -#[cfg_attr(not(windows), allow(dead_code))] -fn labeled_wsl_version(text: &str) -> Option { - let line = text.lines().find(|line| { - let head = line - .split(':') - .next() - .unwrap_or("") - .trim() - .to_ascii_lowercase(); - head.starts_with("wsl") && !head.starts_with("wslg") - })?; - extract_version(line) -} - -#[cfg_attr(not(windows), allow(dead_code))] -fn sole_version(text: &str) -> Option { - let mut found = None; - for token in text.split(|c: char| !(c.is_ascii_digit() || c == '.')) { - if token.is_empty() { - continue; - } - let Some(version) = extract_version(token) else { - continue; - }; - if found.is_some() { - return None; - } - found = Some(version); - } - found -} - -/// Pull the first `x.y.z` / `x.y.z.w` from command output (fourth component dropped). -pub fn extract_version(text: &str) -> Option { - for token in text.split(|c: char| !(c.is_ascii_digit() || c == '.')) { - if token.is_empty() { - continue; - } - let parts: Vec<&str> = token.split('.').collect(); - if parts.len() >= 3 - && parts - .iter() - .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit())) - { - return Some(format!("{}.{}.{}", parts[0], parts[1], parts[2])); + let Some(sv) = SemVer::parse(raw) else { + return WslVersionVerdict::Missing; + }; + let normalized = sv.to_string(); + if sv >= MIN_WSL_VERSION { + WslVersionVerdict::Ok(normalized) + } else { + WslVersionVerdict::TooOld { + found: normalized, } } - None } /// Prefer exact host match, else newest package with the same major as the -/// host, else the newest published package (SDK must not be older than WSL). +/// host, else the newest published package. pub fn pick_nuget_version(host: &str, available: &[String]) -> Option { let mut parsed: Vec<(SemVer, &String)> = available .iter() @@ -165,10 +87,8 @@ pub fn pick_nuget_version(host: &str, available: &[String]) -> Option { /// Whether a cached NuGet SDK can serve this host without re-downloading. /// -/// Host probe strings can drift (`wslc` vs `wsl --version`, or `unknown` on a -/// failed probe). Do not require an exact stamp match — only re-download when -/// the cached package is clearly too old (major behind the host), or when the -/// user forces Retry. +/// Re-download only when the cached package major is behind the host, or when +/// the user forces Retry. pub fn cache_compatible(host: &str, nuget_version: &str) -> bool { let Some(host_ver) = SemVer::parse(host) else { return true; @@ -179,184 +99,39 @@ pub fn cache_compatible(host: &str, nuget_version: &str) -> bool { nuget_ver.major >= host_ver.major } -fn normalize_command_text(text: &str) -> String { - let stripped: String = text.chars().filter(|c| *c != '\0').collect(); - if looks_like_spaced_utf16(&stripped) { - let collapsed = collapse_ascii_gaps(&stripped); - if collapsed != stripped { - return collapsed; - } - } - stripped -} - -fn decode_utf16(bytes: &[u8]) -> Option { - if bytes.starts_with(&[0xFF, 0xFE]) { - return Some(utf16_units_to_string(&bytes[2..], true)); - } - if bytes.starts_with(&[0xFE, 0xFF]) { - return Some(utf16_units_to_string(&bytes[2..], false)); - } - if looks_like_utf16_le(bytes) { - return Some(utf16_units_to_string(bytes, true)); - } - None -} - -fn utf16_units_to_string(bytes: &[u8], little_endian: bool) -> String { - let units: Vec = bytes - .chunks(2) - .map(|chunk| { - if chunk.len() == 2 { - if little_endian { - u16::from_le_bytes([chunk[0], chunk[1]]) - } else { - u16::from_be_bytes([chunk[0], chunk[1]]) - } - } else if little_endian { - chunk[0] as u16 - } else { - (chunk[0] as u16) << 8 - } - }) - .collect(); - String::from_utf16_lossy(&units) - .trim_start_matches('\u{feff}') - .to_string() -} - -/// ASCII-heavy UTF-16LE: the high byte of most units is NUL. -fn looks_like_utf16_le(bytes: &[u8]) -> bool { - if bytes.len() < 8 || !bytes.len().is_multiple_of(2) { - return false; - } - let mut odd_zeros = 0usize; - let mut even_zeros = 0usize; - for pair in bytes.as_chunks::<2>().0 { - if pair[0] == 0 { - even_zeros += 1; - } - if pair[1] == 0 { - odd_zeros += 1; - } - } - let pairs = bytes.len() / 2; - odd_zeros * 2 > pairs && odd_zeros > even_zeros.saturating_mul(4) -} - -fn plausible_text(text: &str) -> bool { - let total = text.chars().count(); - if total == 0 { - return false; - } - let printable = text - .chars() - .filter(|c| !c.is_control() || matches!(*c, '\n' | '\r' | '\t')) - .count(); - printable * 4 >= total * 3 -} - -/// Fallback when the buffer is not even-length UTF-16 but still has -/// `ASCII, NUL` pairs (a truncated or mixed capture). -fn strip_nuls_between_ascii(bytes: &[u8]) -> Option { - let mut pairs = 0usize; - let mut i = 0usize; - while i + 1 < bytes.len() { - if bytes[i] != 0 && bytes[i].is_ascii() && bytes[i + 1] == 0 { - pairs += 1; - i += 2; - } else { - i += 1; - } - } - if pairs < 3 { - return None; - } - let stripped: Vec = bytes.iter().copied().filter(|b| *b != 0).collect(); - let text = String::from_utf8_lossy(&stripped).into_owned(); - if text.trim().is_empty() { - None - } else { - Some(text) - } -} - -fn looks_like_spaced_utf16(text: &str) -> bool { - let chars: Vec = text.chars().collect(); - let mut hits = 0usize; - for window in chars.windows(3) { - if window[1] == ' ' && is_gap_glue(window[0]) && is_gap_glue(window[2]) { - hits += 1; - } - } - hits >= 3 -} - -fn is_gap_glue(c: char) -> bool { - c.is_ascii_alphanumeric() || c == '.' -} - -/// Join ASCII that was split by the NUL bytes of UTF-16LE (`3 . 0 . 1` → `3.0.1`). -/// Newlines stay put so a trailing digit cannot merge into the next line. -fn collapse_ascii_gaps(text: &str) -> String { - let mut out = String::new(); - let mut chars = text.chars().peekable(); - while let Some(c) = chars.next() { - if c == '\n' || c == '\r' { - out.push(c); - continue; - } - if c.is_whitespace() { - let prev = match out.chars().last() { - Some('\n' | '\r') | None => None, - other => other, - }; - let next = chars.peek().copied(); - if let (Some(prev), Some(next)) = (prev, next) - && next != '\n' - && next != '\r' - && is_gap_glue(prev) - && is_gap_glue(next) - { - continue; - } - if !matches!(out.chars().last(), Some(' ' | '\n' | '\r') | None) { - out.push(' '); - } - continue; - } - out.push(c); - } - out -} - #[cfg(test)] mod tests { use super::*; - fn encode_utf16_le(text: &str) -> Vec { - text.encode_utf16() - .flat_map(|unit| unit.to_le_bytes()) - .collect() + #[test] + fn parses_registry_style_versions() { + assert_eq!(SemVer::parse("2.9.3"), Some(SemVer { major: 2, minor: 9, patch: 3 })); + assert_eq!(SemVer::parse("3.0.1.0"), Some(SemVer { major: 3, minor: 0, patch: 1 })); + assert_eq!(SemVer::parse(" 3.0.1 "), Some(SemVer { major: 3, minor: 0, patch: 1 })); + assert!(SemVer::parse("not-a-version").is_none()); + assert!(SemVer::parse("").is_none()); } - const CHINESE_VERSION: &str = "\ -WSL 版本: 3.0.1.0 -内核版本: 6.18.40.1-1 -WSLg 版本: 1.0.66 -MSRDC 版本: 1.2.6074 -Direct3D 版本: 1.611.1-81528511 -DXCore 版本: 10.0.26100.1-240331-1435.ge-release -Windows 版本: 10.0.26100.3476 -"; - #[test] - fn extracts_version_tokens() { + fn judges_package_version() { + assert_eq!(judge_package_version(None), WslVersionVerdict::Missing); + assert_eq!(judge_package_version(Some(" ")), WslVersionVerdict::Missing); assert_eq!( - extract_version("WSL version: 2.9.3.0\n"), - Some("2.9.3".into()) + judge_package_version(Some("2.9.3")), + WslVersionVerdict::Ok("2.9.3".into()) ); - assert_eq!(extract_version("wslc 3.0.1\n"), Some("3.0.1".into())); + assert_eq!( + judge_package_version(Some("3.0.1.0")), + WslVersionVerdict::Ok("3.0.1".into()) + ); + assert_eq!( + judge_package_version(Some("2.9.2")), + WslVersionVerdict::TooOld { + found: "2.9.2".into() + } + ); + assert!(SemVer::parse("3.0.1.0").unwrap() >= MIN_WSL_VERSION); + assert_eq!(format!("{MIN_WSL_VERSION}"), "2.9.3"); } #[test] @@ -389,7 +164,7 @@ Windows 版本: 10.0.26100.3476 } #[test] - fn cache_compatible_ignores_host_string_drift() { + fn cache_compatible_by_major() { assert!(cache_compatible("unknown", "3.0.1")); assert!(cache_compatible("2.9.3", "2.9.9")); assert!(cache_compatible("2.9.5", "3.0.1")); @@ -397,106 +172,4 @@ Windows 版本: 10.0.26100.3476 assert!(!cache_compatible("3.0.1", "2.9.9")); assert!(!cache_compatible("3.0.0", "not-a-version")); } - - #[test] - fn judges_wsl_version_output() { - assert_eq!(judge_wsl_version(None), WslVersionVerdict::Missing); - assert_eq!(judge_wsl_version(Some(" ")), WslVersionVerdict::Missing); - assert_eq!( - judge_wsl_version(Some("2.9.3")), - WslVersionVerdict::Ok("2.9.3".into()) - ); - assert_eq!( - judge_wsl_version(Some("2.10.0")), - WslVersionVerdict::Ok("2.10.0".into()) - ); - assert_eq!( - judge_wsl_version(Some("2.9.2")), - WslVersionVerdict::TooOld { - found: "2.9.2".into() - } - ); - - let english = "\ -WSL version: 2.4.13.0 -Kernel version: 5.15.167.4-1 -WSLg version: 1.0.65 -"; - assert_eq!( - judge_wsl_version(Some(english)), - WslVersionVerdict::TooOld { - found: "2.4.13".into() - } - ); - - let chinese = "WSL 版本: 2.9.3.0\n内核版本: 5.15.167.4-1\n"; - assert_eq!( - judge_wsl_version(Some(chinese)), - WslVersionVerdict::Ok("2.9.3".into()) - ); - - let unlabeled = "Kernel version: 5.15.167.4-1\nWSLg version: 1.0.65\n"; - assert_eq!( - judge_wsl_version(Some(unlabeled)), - WslVersionVerdict::Missing - ); - assert_eq!(format!("{}", MIN_WSL_VERSION), "2.9.3"); - } - - #[test] - fn utf16_le_chinese_3_0_1_0_meets_minimum() { - let bytes = encode_utf16_le(CHINESE_VERSION); - let text = decode_command_output(&bytes); - assert_eq!(text.lines().next(), Some("WSL 版本: 3.0.1.0")); - assert_eq!( - judge_wsl_version(Some(&text)), - WslVersionVerdict::Ok("3.0.1".into()) - ); - assert!(SemVer::parse("3.0.1.0").unwrap() >= MIN_WSL_VERSION); - - let mut bom = vec![0xFF, 0xFE]; - bom.extend(encode_utf16_le( - "WSL version: 3.0.1.0\r\nKernel version: 6.18.40.1-1\r\n", - )); - let from_stderr = decode_command_output(&bom); - assert_eq!( - judge_wsl_version(Some(&from_stderr)), - WslVersionVerdict::Ok("3.0.1".into()) - ); - } - - #[test] - fn nul_and_spaced_misread_still_accepts_3_0_1_0() { - let bytes = encode_utf16_le(CHINESE_VERSION); - let lossy = String::from_utf8_lossy(&bytes); - assert!(lossy.contains('\0')); - assert_eq!( - judge_wsl_version(Some(&lossy)), - WslVersionVerdict::Ok("3.0.1".into()) - ); - - let spaced = lossy.replace('\0', " "); - assert!( - spaced.contains("Hr,g"), - "版本 misread as Latin-1 should surface as Hr,g, got {spaced:?}" - ); - assert!(spaced.contains("3 . 0 . 1 . 0"), "got {spaced:?}"); - assert_eq!( - judge_wsl_version(Some(&spaced)), - WslVersionVerdict::Ok("3.0.1".into()) - ); - } - - #[test] - fn nul_strip_fallback_when_utf16_length_is_odd() { - let mut bytes = encode_utf16_le("WSL version: 3.0.1.0\n"); - bytes.push(b'A'); - assert_eq!(bytes.len() % 2, 1); - assert!(super::decode_utf16(&bytes).is_none()); - let text = decode_command_output(&bytes); - assert_eq!( - judge_wsl_version(Some(&text)), - WslVersionVerdict::Ok("3.0.1".into()) - ); - } } diff --git a/src/wslc/wsl_probe.rs b/src/wslc/wsl_probe.rs index 9c8e4d3..3a605fe 100644 --- a/src/wslc/wsl_probe.rs +++ b/src/wslc/wsl_probe.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Result}; -use super::sdk_version::extract_version; +use super::sdk_version::SemVer; /// Installed WSL package version (`x.y.z`), if the MSI registry value is present. pub fn package_version() -> Option { @@ -16,9 +16,9 @@ pub fn package_version() -> Option { r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\MSI", ] { if let Some(raw) = read_hk_lm_string(subkey, "Version") - && let Some(v) = extract_version(&raw) + && let Some(v) = SemVer::parse(&raw) { - return Some(v); + return Some(v.to_string()); } } None From 751ac33dd541bc730718ec22da085d3043d0397b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 11:53:32 +0000 Subject: [PATCH 5/5] chore: remove unused session_stats path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop SessionStats/Cmd::Stats and the unused host_version wrapper. Refresh SDK notes and silence Linux-only dead_code false positives. Co-authored-by: 绿岸公式 --- CHANGELOG.md | 2 ++ src/wslc/live.rs | 28 +--------------------------- src/wslc/mod.rs | 20 +------------------- src/wslc/sdk_fetch.rs | 10 ++++++---- src/wslc/sdk_version.rs | 2 ++ src/wslc/types.rs | 15 ++------------- src/wslc/wsl_probe.rs | 12 ------------ wix/sdk/README.md | 2 +- 8 files changed, 15 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d997283..dd7ae53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - Split SDK install into probe / cache / fetch modules so the download path stays short. - 删除面向 `wsl --version` 的 UTF-16 / 多行文本解析;`sdk_version` 只保留 SemVer 与 NuGet 匹配。 - Drop CLI text/UTF-16 parsing; `sdk_version` keeps SemVer + NuGet matching only. +- 移除未使用的 `session_stats` / `SessionStats` / `Cmd::Stats` 路径。 + - Remove unused session-stats plumbing. ## [0.1.2] - 2026-10-11 diff --git a/src/wslc/live.rs b/src/wslc/live.rs index 77bd5fb..e7f9065 100644 --- a/src/wslc/live.rs +++ b/src/wslc/live.rs @@ -12,7 +12,7 @@ use super::catalog::{Catalog, PersistedContainer}; use super::prefs::SessionPrefs; use super::{ split_ref, BackendKind, ContainerInfo, CreateContainerRequest, EngineStatus, ImageInfo, - PortMapping, PullEvent, SessionStats, VolumeInfo, VolumeMapping, + PortMapping, PullEvent, VolumeInfo, VolumeMapping, }; const SESSION_NAME: &str = "wslc-desktop"; @@ -21,7 +21,6 @@ const STOP_TIMEOUT: Duration = Duration::from_secs(10); pub enum Cmd { Status(Sender), - Stats(Sender), ListContainers { all: bool, reply: Sender>>, @@ -409,28 +408,6 @@ impl LiveState { } } - fn stats(&self) -> SessionStats { - let running = self - .containers - .iter() - .filter(|c| { - matches!( - c.handle.state(), - Ok(wslc::ContainerState::Running) - ) - }) - .count(); - let images = self.session.list_images().map(|v| v.len()).unwrap_or(0); - SessionStats { - cpu_count: self.prefs.cpu_count, - memory_mb: self.prefs.memory_mb, - containers: self.containers.len(), - running, - images, - volumes: self.volumes.len(), - } - } - fn find_container_mut(&mut self, key: &str) -> Result<&mut TrackedContainer> { self.containers .iter_mut() @@ -923,9 +900,6 @@ fn live_worker(mut state: LiveState, rx: mpsc::Receiver) { Cmd::Status(reply) => { let _ = reply.send(state.status()); } - Cmd::Stats(reply) => { - let _ = reply.send(state.stats()); - } Cmd::ListContainers { all, reply } => { let _ = reply.send(state.list_containers(all)); } diff --git a/src/wslc/mod.rs b/src/wslc/mod.rs index f90388b..71c4572 100644 --- a/src/wslc/mod.rs +++ b/src/wslc/mod.rs @@ -36,6 +36,7 @@ pub fn min_wsl_version_text() -> String { sdk_version::MIN_WSL_VERSION.to_string() } +#[cfg_attr(not(windows), allow(dead_code))] // production use is in `live` (Windows) pub(super) fn split_ref(reference: &str) -> (String, String) { if let Some((repo, tag)) = reference.rsplit_once(':') { if !tag.contains('/') { @@ -428,25 +429,6 @@ impl WslcClient { } } - #[cfg_attr(not(windows), allow(dead_code))] - #[allow(dead_code)] // reserved for richer Overview refresh - pub fn session_stats(&self) -> SessionStats { - match &self.inner.backend { - #[cfg(windows)] - Backend::Live { tx, .. } => { - let (reply_tx, reply_rx) = std::sync::mpsc::channel(); - if tx.send(live::Cmd::Stats(reply_tx)).is_ok() { - if let Ok(stats) = reply_rx.recv() { - return stats; - } - } - SessionStats::default() - } - #[cfg(not(windows))] - Backend::Unsupported => SessionStats::default(), - } - } - pub fn list_volumes(&self) -> Result> { match &self.inner.backend { #[cfg(windows)] diff --git a/src/wslc/sdk_fetch.rs b/src/wslc/sdk_fetch.rs index 5b05012..b91d8c8 100644 --- a/src/wslc/sdk_fetch.rs +++ b/src/wslc/sdk_fetch.rs @@ -45,13 +45,15 @@ pub fn ensure_sdk_dir( emit(FetchEvent::Status("Detecting WSL version…".into())); emit(FetchEvent::Progress(5.0)); - let host = match wsl_probe::host_version() { - Ok(v) => { + let host = match wsl_probe::package_version() { + Some(v) => { emit(FetchEvent::Log(format!("Host version: {v}"))); v } - Err(err) => { - emit(FetchEvent::Log(format!("Could not read WSL version: {err:#}"))); + None => { + emit(FetchEvent::Log( + "WSL package version not found in registry (Lxss\\Msi\\Version)".into(), + )); emit(FetchEvent::Log( "Install or update WSL (`wsl --install` / `wsl --update`), then retry.".into(), )); diff --git a/src/wslc/sdk_version.rs b/src/wslc/sdk_version.rs index a95ab49..646b10e 100644 --- a/src/wslc/sdk_version.rs +++ b/src/wslc/sdk_version.rs @@ -64,6 +64,7 @@ pub fn judge_package_version(version: Option<&str>) -> WslVersionVerdict { /// Prefer exact host match, else newest package with the same major as the /// host, else the newest published package. +#[cfg_attr(not(windows), allow(dead_code))] // used from `sdk_fetch` on Windows pub fn pick_nuget_version(host: &str, available: &[String]) -> Option { let mut parsed: Vec<(SemVer, &String)> = available .iter() @@ -89,6 +90,7 @@ pub fn pick_nuget_version(host: &str, available: &[String]) -> Option { /// /// Re-download only when the cached package major is behind the host, or when /// the user forces Retry. +#[cfg_attr(not(windows), allow(dead_code))] // used from `sdk_cache` on Windows pub fn cache_compatible(host: &str, nuget_version: &str) -> bool { let Some(host_ver) = SemVer::parse(host) else { return true; diff --git a/src/wslc/types.rs b/src/wslc/types.rs index 961ed5c..83071a5 100644 --- a/src/wslc/types.rs +++ b/src/wslc/types.rs @@ -127,9 +127,9 @@ pub enum SetupStepId { #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(not(windows), allow(dead_code))] pub enum SetupFault { - /// `wsl --version` missing, failed, or unreadable. + /// Registry `Lxss\Msi\Version` missing or unreadable. WslMissing, - /// Parsed version is below 2.9.3. + /// Package version is below 2.9.3. WslTooOld { found: String }, /// This build is not running on Windows. NotWindows, @@ -164,14 +164,3 @@ pub enum SdkSetupEvent { Finished(Result), } -/// Snapshot used by Overview "simple stats". -#[derive(Debug, Clone, PartialEq, Eq, Default)] -#[allow(dead_code)] -pub struct SessionStats { - pub cpu_count: u32, - pub memory_mb: u32, - pub containers: usize, - pub running: usize, - pub images: usize, - pub volumes: usize, -} diff --git a/src/wslc/wsl_probe.rs b/src/wslc/wsl_probe.rs index 3a605fe..a7454a7 100644 --- a/src/wslc/wsl_probe.rs +++ b/src/wslc/wsl_probe.rs @@ -5,8 +5,6 @@ #![cfg(windows)] -use anyhow::{anyhow, Result}; - use super::sdk_version::SemVer; /// Installed WSL package version (`x.y.z`), if the MSI registry value is present. @@ -24,16 +22,6 @@ pub fn package_version() -> Option { None } -/// Host version for NuGet package selection. -pub fn host_version() -> Result { - package_version().ok_or_else(|| { - anyhow!( - "WSL package version not found in registry (Lxss\\Msi\\Version); \ - install or update WSL with `wsl --update`" - ) - }) -} - fn read_hk_lm_string(subkey: &str, value_name: &str) -> Option { use windows_sys::Win32::Foundation::{ERROR_MORE_DATA, ERROR_SUCCESS}; use windows_sys::Win32::System::Registry::{ diff --git a/wix/sdk/README.md b/wix/sdk/README.md index ac0fc8e..00b024f 100644 --- a/wix/sdk/README.md +++ b/wix/sdk/README.md @@ -2,7 +2,7 @@ The MSI does **not** embed `wslcsdk.dll`. At runtime the app: -1. Reads `wslc version` / `wsl --version` +1. Reads the WSL package version from `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Msi\Version` 2. Picks a matching [Microsoft.WSL.Containers](https://www.nuget.org/packages/Microsoft.WSL.Containers) release 3. Caches `wslcsdk.dll` under `%LOCALAPPDATA%\WSLcDesktop\sdk\`