From 4e0f651fba137d95123bf5d8bfdc631cf6142e76 Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Thu, 6 Aug 2026 10:46:03 +0800 Subject: [PATCH 1/4] fix(desktop): strip Windows verbatim prefix from workspace paths On Windows, std::fs::canonicalize returns extended-length verbatim paths (\\?\C:\dev\MathDesk). The desktop shell persisted that form to desktop-state.json and passed it as the bundled Node runtime's cwd and --workspace argument, and Node's bootstrap (resolveMainPath -> realpathSync) crashed with EISDIR: lstat 'C:', so Desktop 0.1.0 failed to start at all. Because the verbatim path is persisted, every subsequent launch crashed the same way. Use dunce::canonicalize at both canonicalization sites (start_runtime_async, resolve_workspace). It behaves like fs::canonicalize but simplifies the result to a plain drive-letter path whenever that is safe, and delegates to fs::canonicalize unchanged on non-Windows platforms. Since start_runtime_async re-canonicalizes the persisted workspace on every launch, affected installs recover on their first launch after this fix, and the simplified form is written back to desktop-state.json. dunce is already in Cargo.lock transitively via the tauri plugins, so no new code is compiled. Adds a regression test asserting resolve_workspace never returns a \\?-prefixed path. Fixes #8615 --- packages/desktop-shell/src-tauri/Cargo.lock | 1 + packages/desktop-shell/src-tauri/Cargo.toml | 1 + packages/desktop-shell/src-tauri/src/main.rs | 6 +++++- .../desktop-shell/src-tauri/src/runtime.rs | 21 ++++++++++++++++--- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/desktop-shell/src-tauri/Cargo.lock b/packages/desktop-shell/src-tauri/Cargo.lock index 8ec31e4efd2..75373b6e282 100644 --- a/packages/desktop-shell/src-tauri/Cargo.lock +++ b/packages/desktop-shell/src-tauri/Cargo.lock @@ -2730,6 +2730,7 @@ name = "qwen-code-desktop" version = "0.0.1" dependencies = [ "command-group", + "dunce", "open", "rand", "serde", diff --git a/packages/desktop-shell/src-tauri/Cargo.toml b/packages/desktop-shell/src-tauri/Cargo.toml index 3495c0256de..be904ebcb24 100644 --- a/packages/desktop-shell/src-tauri/Cargo.toml +++ b/packages/desktop-shell/src-tauri/Cargo.toml @@ -12,6 +12,7 @@ tauri-build = { version = "2.4.1", features = [] } [dependencies] command-group = "5.0.1" +dunce = "1.0.5" open = "5.4.0" rand = "0.9.2" serde = { version = "1.0", features = ["derive"] } diff --git a/packages/desktop-shell/src-tauri/src/main.rs b/packages/desktop-shell/src-tauri/src/main.rs index 91bfd575920..7562528f01e 100755 --- a/packages/desktop-shell/src-tauri/src/main.rs +++ b/packages/desktop-shell/src-tauri/src/main.rs @@ -307,7 +307,11 @@ fn start_runtime_async(app: AppHandle, workspace: PathBuf) { let _ = app.emit("runtime-starting", workspace.to_string_lossy().into_owned()); tauri::async_runtime::spawn_blocking(move || { let state = app.state::(); - let canonical = match fs::canonicalize(&workspace) { + // dunce::canonicalize strips the Windows `\\?\` verbatim prefix that + // fs::canonicalize produces: the bundled Node runtime mis-resolves a + // verbatim cwd/workspace during bootstrap (EISDIR lstat 'C:'), and the + // persisted path would re-crash every subsequent launch (#8615). + let canonical = match dunce::canonicalize(&workspace) { Ok(path) if path.is_dir() => path, Ok(path) => { emit_runtime_failure( diff --git a/packages/desktop-shell/src-tauri/src/runtime.rs b/packages/desktop-shell/src-tauri/src/runtime.rs index b559df10aa1..0405f15515e 100644 --- a/packages/desktop-shell/src-tauri/src/runtime.rs +++ b/packages/desktop-shell/src-tauri/src/runtime.rs @@ -177,7 +177,9 @@ fn require_file(path: &Path, description: &str) -> Result<(), String> { } fn resolve_workspace(configured: &Path) -> Result { - let workspace = fs::canonicalize(configured).map_err(|error| { + // dunce::canonicalize keeps the Windows `\\?\` verbatim prefix out of the + // child cwd and `--workspace` argument (#8615). + let workspace = dunce::canonicalize(configured).map_err(|error| { format!( "Failed to resolve desktop workspace {}: {error}", configured.display() @@ -459,13 +461,26 @@ fn runtime_arguments(workspace: &Path) -> Vec { #[cfg(test)] mod tests { use super::{ - append_failure_output, parse_listening_url, runtime_arguments, DesktopRuntime, - RuntimeStopped, FAILURE_OUTPUT_LIMIT, + append_failure_output, parse_listening_url, resolve_workspace, runtime_arguments, + DesktopRuntime, RuntimeStopped, FAILURE_OUTPUT_LIMIT, }; use std::path::Path; use std::sync::Mutex; use url::Url; + #[test] + fn resolve_workspace_strips_windows_verbatim_prefix() { + let dir = std::env::temp_dir().join(format!("qwen-desktop-ws-{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp workspace"); + let resolved = resolve_workspace(&dir).expect("resolve workspace"); + std::fs::remove_dir_all(&dir).expect("cleanup temp workspace"); + let resolved = resolved.to_string_lossy(); + assert!( + !resolved.starts_with("\\\\?\\"), + "workspace keeps the verbatim prefix: {resolved}" + ); + } + #[test] fn parses_loopback_listening_line() { let url = parse_listening_url( From 38a11e74ccd38108a6f310c16eaec908f8a9926b Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Thu, 6 Aug 2026 20:35:00 +0800 Subject: [PATCH 2/4] fix(desktop): reject residual verbatim paths --- .github/workflows/ci.yml | 14 +++++--- packages/desktop-shell/src-tauri/src/main.rs | 24 +++---------- .../desktop-shell/src-tauri/src/runtime.rs | 35 ++++++++++++++++--- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cca172bea1..984382bccdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1065,10 +1065,14 @@ jobs: # catches compile failures (e.g. a moved-value error); fmt/clippy are not run # here because the release pipeline does not gate on them either. desktop_shell: - name: 'Desktop Shell (ubuntu-22.04)' + name: 'Desktop Shell (${{ matrix.os }})' needs: 'classify_pr' if: "${{ !cancelled() && github.event_name != 'push' && needs.classify_pr.outputs.skip_ci != 'true' }}" - runs-on: 'ubuntu-22.04' + strategy: + fail-fast: false + matrix: + os: ['ubuntu-22.04', 'windows-latest'] + runs-on: '${{ matrix.os }}' timeout-minutes: 45 permissions: contents: 'read' @@ -1122,13 +1126,13 @@ jobs: # cargo test links the Tauri/wry webview, so the WebKit/GTK dev headers # must be present (mirrors the Linux build job in desktop-release.yml). - name: 'Install Linux dependencies' - if: "${{ steps.filter.outputs.changed == 'true' }}" + if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}" run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libatk-bridge2.0-0 at-spi2-core dbus-x11 patchelf libfuse2 xdg-utils - uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 - if: "${{ steps.filter.outputs.changed == 'true' }}" + if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}" with: node-version: '22.x' @@ -1146,6 +1150,6 @@ jobs: run: 'cargo test --manifest-path src-tauri/Cargo.toml' - name: 'Run desktop release tests' - if: "${{ steps.filter.outputs.changed == 'true' }}" + if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}" working-directory: 'packages/desktop-shell' run: 'node scripts/test-release.js' diff --git a/packages/desktop-shell/src-tauri/src/main.rs b/packages/desktop-shell/src-tauri/src/main.rs index 7562528f01e..bf7af37ed89 100755 --- a/packages/desktop-shell/src-tauri/src/main.rs +++ b/packages/desktop-shell/src-tauri/src/main.rs @@ -4,7 +4,7 @@ mod desktop_state; mod runtime; use desktop_state::{default_window_size, restore_window, SettingsStore}; -use runtime::DesktopRuntime; +use runtime::{resolve_workspace, DesktopRuntime}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; @@ -307,26 +307,10 @@ fn start_runtime_async(app: AppHandle, workspace: PathBuf) { let _ = app.emit("runtime-starting", workspace.to_string_lossy().into_owned()); tauri::async_runtime::spawn_blocking(move || { let state = app.state::(); - // dunce::canonicalize strips the Windows `\\?\` verbatim prefix that - // fs::canonicalize produces: the bundled Node runtime mis-resolves a - // verbatim cwd/workspace during bootstrap (EISDIR lstat 'C:'), and the - // persisted path would re-crash every subsequent launch (#8615). - let canonical = match dunce::canonicalize(&workspace) { - Ok(path) if path.is_dir() => path, - Ok(path) => { - emit_runtime_failure( - &app, - generation, - format!("Workspace is not a directory: {}", path.display()), - ); - return; - } + let canonical = match resolve_workspace(&workspace) { + Ok(path) => path, Err(error) => { - emit_runtime_failure( - &app, - generation, - format!("Failed to open workspace {}: {error}", workspace.display()), - ); + emit_runtime_failure(&app, generation, error); return; } }; diff --git a/packages/desktop-shell/src-tauri/src/runtime.rs b/packages/desktop-shell/src-tauri/src/runtime.rs index 0405f15515e..6274d7f2973 100644 --- a/packages/desktop-shell/src-tauri/src/runtime.rs +++ b/packages/desktop-shell/src-tauri/src/runtime.rs @@ -3,7 +3,7 @@ use rand::RngCore; use std::ffi::OsString; use std::fs::{self, File}; use std::io::{BufRead, BufReader, Read, Write}; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::{ atomic::{AtomicBool, Ordering}, @@ -176,15 +176,28 @@ fn require_file(path: &Path, description: &str) -> Result<(), String> { Err(format!("{description} is missing at {}", path.display())) } -fn resolve_workspace(configured: &Path) -> Result { - // dunce::canonicalize keeps the Windows `\\?\` verbatim prefix out of the - // child cwd and `--workspace` argument (#8615). +fn ensure_supported_workspace_path(path: &Path) -> Result<(), String> { + if matches!( + path.components().next(), + Some(Component::Prefix(prefix)) if prefix.kind().is_verbatim() + ) { + return Err(format!( + "Desktop workspace path requires an unsupported Windows verbatim form: {}. Choose a shorter local path.", + path.display() + )); + } + Ok(()) +} + +pub(crate) fn resolve_workspace(configured: &Path) -> Result { + // dunce::canonicalize strips the Windows `\\?\` prefix when safe (#8615). let workspace = dunce::canonicalize(configured).map_err(|error| { format!( "Failed to resolve desktop workspace {}: {error}", configured.display() ) })?; + ensure_supported_workspace_path(&workspace)?; if workspace.is_dir() { Ok(workspace) } else { @@ -481,6 +494,20 @@ mod tests { ); } + #[cfg(windows)] + #[test] + fn rejects_residual_windows_verbatim_workspace_paths() { + for path in [ + r"\\?\C:\workspace", + r"\\?\UNC\server\share", + r"\\?\GLOBALROOT\Device\HarddiskVolume1", + ] { + let error = super::ensure_supported_workspace_path(Path::new(path)) + .expect_err("reject residual verbatim path"); + assert!(error.contains("unsupported Windows verbatim form")); + } + } + #[test] fn parses_loopback_listening_line() { let url = parse_listening_url( From 9a60c06cd148aa9a8e59fd9f5dc8c0a1ff45d25b Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Thu, 6 Aug 2026 23:23:40 +0800 Subject: [PATCH 3/4] fix(desktop): cover residual Windows workspace paths --- .github/workflows/ci.yml | 2 +- .../desktop-shell/src-tauri/src/runtime.rs | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16848fffbae..03e04e28dc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1075,7 +1075,7 @@ jobs: strategy: fail-fast: false matrix: - os: ['ubuntu-22.04', 'windows-latest'] + os: ['ubuntu-22.04', 'windows-2022'] runs-on: '${{ matrix.os }}' timeout-minutes: 45 permissions: diff --git a/packages/desktop-shell/src-tauri/src/runtime.rs b/packages/desktop-shell/src-tauri/src/runtime.rs index 6274d7f2973..6963cf2f6e3 100644 --- a/packages/desktop-shell/src-tauri/src/runtime.rs +++ b/packages/desktop-shell/src-tauri/src/runtime.rs @@ -182,7 +182,7 @@ fn ensure_supported_workspace_path(path: &Path) -> Result<(), String> { Some(Component::Prefix(prefix)) if prefix.kind().is_verbatim() ) { return Err(format!( - "Desktop workspace path requires an unsupported Windows verbatim form: {}. Choose a shorter local path.", + "Desktop workspace path uses an unsupported Windows extended-length form: {}. Choose a local drive path; network (UNC) shares, paths over 260 characters, and names ending in a dot or space are not supported.", path.display() )); } @@ -478,6 +478,8 @@ mod tests { DesktopRuntime, RuntimeStopped, FAILURE_OUTPUT_LIMIT, }; use std::path::Path; + #[cfg(windows)] + use std::path::PathBuf; use std::sync::Mutex; use url::Url; @@ -504,8 +506,27 @@ mod tests { ] { let error = super::ensure_supported_workspace_path(Path::new(path)) .expect_err("reject residual verbatim path"); - assert!(error.contains("unsupported Windows verbatim form")); + assert!(error.contains("unsupported Windows extended-length form")); + } + } + + #[cfg(windows)] + #[test] + fn resolve_workspace_rejects_residual_verbatim_paths() { + use std::os::windows::ffi::OsStrExt; + + let base = + std::env::temp_dir().join(format!("qwen-desktop-long-ws-{}", std::process::id())); + let mut workspace = PathBuf::from(format!(r"\\?\{}", base.display())); + while workspace.as_os_str().encode_wide().count() <= 270 { + workspace.push("long-workspace-component"); } + std::fs::create_dir_all(&workspace).expect("create long workspace"); + let result = resolve_workspace(&workspace); + std::fs::remove_dir_all(PathBuf::from(format!(r"\\?\{}", base.display()))) + .expect("cleanup long workspace"); + let error = result.expect_err("reject long workspace"); + assert!(error.contains("unsupported Windows extended-length form")); } #[test] From 16726db90590731d32c004c178d2aa36f8dfc478 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Fri, 7 Aug 2026 01:29:12 +0000 Subject: [PATCH 4/4] refactor(desktop): resolve workspace once per runtime start --- packages/desktop-shell/src-tauri/src/runtime.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop-shell/src-tauri/src/runtime.rs b/packages/desktop-shell/src-tauri/src/runtime.rs index 6963cf2f6e3..1231c1e3738 100644 --- a/packages/desktop-shell/src-tauri/src/runtime.rs +++ b/packages/desktop-shell/src-tauri/src/runtime.rs @@ -44,13 +44,13 @@ impl DesktopRuntime { pub fn start(app: &AppHandle, workspace: &Path, log_path: &Path) -> Result { let id = NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed); let layout = RuntimeLayout::resolve(app)?; - let workspace = resolve_workspace(workspace)?; + // Callers pass a workspace already resolved by resolve_workspace. let token = random_token(); let mut command = Command::new(&layout.node); command .arg(&layout.entry) - .args(runtime_arguments(&workspace)) - .current_dir(&workspace) + .args(runtime_arguments(workspace)) + .current_dir(workspace) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped())