From c2c7c01e24653d12a409c5d549c7e8623c8e5d22 Mon Sep 17 00:00:00 2001 From: Aibeto Date: Tue, 13 Oct 2026 16:41:56 +0800 Subject: [PATCH 1/2] fix(wine): make setup detect a provisioned prefix Two defects made `uur setup` fail on a fully installed prefix. 1. The service check matched `gameviewerserver.exe`, but the Windows service NetEase registers uses `GameViewerService.exe` as its ImagePath. `GameViewerServer.exe` is the streaming server and never appears in the service registration, so `client_service_registered` and `query_client_service` always returned false. A completed install was reported as "GameViewerService has no valid registered executable" and setup re-downloaded and re-installed every run. Both checks now share one `SERVICE_IMAGE` constant, and the `system.reg` parse is factored into a pure `service_image_path` helper so it is testable without a Wine prefix. 2. `wineboot --init` returns before wineserver flushes `system.reg`/`user.reg` to disk (measured ~7s on Wine 11), so a freshly created prefix was misread as uninitialized and setup aborted with "Wine prefix initialization failed (exit status: 0)". Added a bounded `flush_prefix` barrier that waits for `wineserver -w` to return, then polls for the registry files. Verified on Wine 11.19: a clean install into an isolated prefix succeeds end to end, re-running setup takes the already-provisioned fast path, and `setup --repair` verifies without reinstalling. --- src/wine.rs | 119 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 109 insertions(+), 10 deletions(-) diff --git a/src/wine.rs b/src/wine.rs index 80912ba..ad2f7b8 100644 --- a/src/wine.rs +++ b/src/wine.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; use crate::config::Config; @@ -59,6 +60,12 @@ pub fn setup(accept_eula: bool, repair: bool) -> Result<()> { .arg(if repair { "--update" } else { "--init" }) .status() .context("running wineboot")?; + // `wineboot --init` exits before wineserver flushes system.reg/user.reg + // to disk (measured ~7s on Wine 11), so a freshly created prefix looks + // uninitialized to the check below even though wineboot succeeded. Wait + // for the server to go idle, which is the point at which the registry + // files are actually on disk. + flush_prefix(&prefix); if !status.success() || !prefix_initialized(&prefix) { anyhow::bail!("{}", t!("setup.prefix_failed", status = status)); } @@ -136,12 +143,16 @@ pub fn webview2_installed(prefix: &Path) -> bool { .is_some_and(|path| std::fs::metadata(path).is_ok_and(|metadata| metadata.len() > 0)) } -/// Read the persisted Wine service registration without starting Wine or -/// showing component installers from `uur doctor`. -pub fn client_service_registered(prefix: &Path) -> bool { - let Ok(registry) = std::fs::read_to_string(prefix.join("system.reg")) else { - return false; - }; +/// The Windows service NetEase's installer registers points at +/// `GameViewerService.exe`. `GameViewerServer.exe` is a different program — +/// the streaming/terminal server — and never appears in the service +/// ImagePath, so matching on it made every provisioning check fail and left +/// `uur setup` re-downloading and re-installing on an already complete prefix. +const SERVICE_IMAGE: &str = "gameviewerservice.exe"; + +/// Extract the `ImagePath` recorded for the UU Windows service from the raw +/// `system.reg` text. Kept pure so it can be tested without a Wine prefix. +fn service_image_path(registry: &str) -> Option { let mut service_section = false; for line in registry.lines() { if let Some(section) = line.strip_prefix('[').and_then(|line| line.split_once(']')) { @@ -149,11 +160,22 @@ pub fn client_service_registered(prefix: &Path) -> bool { .0 .to_ascii_lowercase() .ends_with(r"\\services\\gameviewerservice"); - } else if service_section && line.starts_with("\"ImagePath\"=") { - return line.to_ascii_lowercase().contains("gameviewerserver.exe"); + } else if service_section { + if let Some(value) = line.trim_start().strip_prefix("\"ImagePath\"=") { + return Some(value.to_ascii_lowercase()); + } } } - false + None +} + +/// Read the persisted Wine service registration without starting Wine or +/// showing component installers from `uur doctor`. +pub fn client_service_registered(prefix: &Path) -> bool { + let Ok(registry) = std::fs::read_to_string(prefix.join("system.reg")) else { + return false; + }; + service_image_path(®istry).is_some_and(|image| image.contains(SERVICE_IMAGE)) } fn query_client_service(prefix: &Path) -> Result { @@ -171,10 +193,13 @@ fn query_client_service(prefix: &Path) -> Result { .stdin(Stdio::null()) .output() .context("checking the UU Windows service registration")?; + // Wine may record the service under `ControlSet001` and only alias it as + // `CurrentControlSet`, and `reg query` output is CRLF; matching the + // executable name case-insensitively anywhere in stdout covers both. Ok(output.status.success() && String::from_utf8_lossy(&output.stdout) .to_ascii_lowercase() - .contains("gameviewerserver.exe")) + .contains(SERVICE_IMAGE)) } /// Registry compatibility profile, ported from the field-tested reference @@ -330,6 +355,42 @@ fn stop_wineserver(prefix: &Path) { .status(); } +/// Block until wineserver has flushed the prefix registry to disk. +/// +/// `wineboot` returns as soon as it has handed work to wineserver, but +/// `system.reg`/`user.reg` only appear once the server shuts down. Without +/// this barrier a freshly provisioned prefix is misread as broken. Both waits +/// are bounded so a stray Wine process cannot hang provisioning. +fn flush_prefix(prefix: &Path) { + if let Ok(mut child) = Command::new("wineserver") + .env("WINEPREFIX", prefix) + .arg("-w") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + { + let deadline = Instant::now() + Duration::from_secs(60); + loop { + match child.try_wait() { + Ok(Some(_)) => break, + Ok(None) if Instant::now() < deadline => { + std::thread::sleep(Duration::from_millis(50)); + } + _ => { + let _ = child.kill(); + let _ = child.wait(); + break; + } + } + } + } + let deadline = Instant::now() + Duration::from_secs(30); + while !prefix_initialized(prefix) && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(100)); + } +} + /// Locate the installed client directory regardless of how NetEase spells /// the vendor folder ("Netease", "NetEase", ...). Case matters on Linux. pub fn find_client_dir(prefix: &Path) -> Option { @@ -517,8 +578,46 @@ fn purge_wine_shortcuts() { mod tests { use super::log_pixels_command; use super::registry_compat_entries; + use super::service_image_path; use std::path::Path; + /// Real `system.reg` excerpt written by the NetEase installer, escaped + /// exactly as Wine stores it. The service ImagePath names + /// GameViewerService.exe, not GameViewerServer.exe. + const INSTALLED_SYSTEM_REG: &str = concat!( + "[System\\\\ControlSet001\\\\Services\\\\GameViewerService] 1791878366\n", + "#time=1dd5ae8c44b323c\n", + "\"DependOnService\"=str(7):\"RpcSs\\0Tcpip\\0NDIS\\0AFD\\0\"\n", + "\"DisplayName\"=\"GameViewerService\"\n", + "\"ImagePath\"=str(2):\"\\\"C:\\\\Program Files\\\\Netease\\\\GameViewer", + "\\\\GameViewerService.exe\\\" --service\"\n", + "\"ObjectName\"=\"LocalSystem\"\n", + "\"Start\"=dword:00000002\n", + ); + + #[test] + fn service_registration_matches_the_image_the_installer_registers() { + // Regression: the check looked for gameviewerserver.exe, which never + // appears in the service ImagePath, so a complete install was always + // reported as missing and `uur setup` reinstalled on every run. + let image = service_image_path(INSTALLED_SYSTEM_REG).expect("ImagePath parsed"); + assert!(image.contains("gameviewerservice.exe")); + assert!(!image.contains("gameviewerserver.exe")); + } + + #[test] + fn missing_service_section_is_not_registered() { + let registry = "[Software\\\\Wine\\\\DllOverrides] 1\n\"winemenubuilder\"=\"\"\n"; + assert!(service_image_path(registry).is_none()); + } + + #[test] + fn service_section_without_image_path_is_not_registered() { + let registry = "[System\\\\ControlSet001\\\\Services\\\\GameViewerService] 1\n\ + \"Start\"=dword:00000002\n"; + assert!(service_image_path(registry).is_none()); + } + #[test] fn bundled_cpp_runtime_is_preferred_for_both_uu_processes() { let entries = registry_compat_entries(); From 349ec274d6d2a59c940c60e56f8d77ef66e358ae Mon Sep 17 00:00:00 2001 From: Xuc Pan Date: Thu, 15 Oct 2026 14:26:03 +0800 Subject: [PATCH 2/2] fix(wine): bound prefix readiness waits without blocking on services --- src/wine.rs | 64 +++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/src/wine.rs b/src/wine.rs index ad2f7b8..8b7bf0a 100644 --- a/src/wine.rs +++ b/src/wine.rs @@ -60,15 +60,10 @@ pub fn setup(accept_eula: bool, repair: bool) -> Result<()> { .arg(if repair { "--update" } else { "--init" }) .status() .context("running wineboot")?; - // `wineboot --init` exits before wineserver flushes system.reg/user.reg - // to disk (measured ~7s on Wine 11), so a freshly created prefix looks - // uninitialized to the check below even though wineboot succeeded. Wait - // for the server to go idle, which is the point at which the registry - // files are actually on disk. - flush_prefix(&prefix); - if !status.success() || !prefix_initialized(&prefix) { + if !status.success() { anyhow::bail!("{}", t!("setup.prefix_failed", status = status)); } + flush_prefix(&prefix).context("waiting for Wine to persist the initialized prefix")?; if find_client_dir(&prefix).is_some() && query_client_service(&prefix)? { finish_setup(&prefix)?; @@ -355,40 +350,47 @@ fn stop_wineserver(prefix: &Path) { .status(); } -/// Block until wineserver has flushed the prefix registry to disk. -/// -/// `wineboot` returns as soon as it has handed work to wineserver, but -/// `system.reg`/`user.reg` only appear once the server shuts down. Without -/// this barrier a freshly provisioned prefix is misread as broken. Both waits -/// are bounded so a stray Wine process cannot hang provisioning. -fn flush_prefix(prefix: &Path) { - if let Ok(mut child) = Command::new("wineserver") +/// Wait for a new prefix's registry files without waiting for running services +/// to exit. Existing prefixes can be queried through Wine immediately. +fn flush_prefix(prefix: &Path) -> Result<()> { + if prefix_initialized(prefix) { + return Ok(()); + } + let mut child = Command::new("wineserver") .env("WINEPREFIX", prefix) .arg("-w") .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() - { - let deadline = Instant::now() + Duration::from_secs(60); - loop { - match child.try_wait() { - Ok(Some(_)) => break, - Ok(None) if Instant::now() < deadline => { - std::thread::sleep(Duration::from_millis(50)); - } - _ => { - let _ = child.kill(); - let _ = child.wait(); - break; - } + .context("starting wineserver registry wait")?; + let deadline = Instant::now() + Duration::from_secs(30); + let result = loop { + if prefix_initialized(prefix) { + break Ok(()); + } + match child.try_wait() { + Ok(Some(status)) if !status.success() => { + break Err(anyhow::anyhow!( + "wineserver registry wait failed ({status})" + )); } + Err(error) => break Err(error).context("checking wineserver registry wait"), + _ => {} + } + if Instant::now() >= deadline { + break Err(anyhow::anyhow!( + "Wine did not create non-empty system.reg and user.reg within 30 seconds" + )); } - } - let deadline = Instant::now() + Duration::from_secs(30); - while !prefix_initialized(prefix) && Instant::now() < deadline { std::thread::sleep(Duration::from_millis(100)); + }; + // This is only the `-w` waiter, not the prefix's server or its processes. + if child.try_wait().ok().flatten().is_none() { + let _ = child.kill(); + let _ = child.wait(); } + result } /// Locate the installed client directory regardless of how NetEase spells