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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1069,10 +1069,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-2022']
runs-on: '${{ matrix.os }}'
timeout-minutes: 45
permissions:
contents: 'read'
Expand Down Expand Up @@ -1126,13 +1130,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'

Expand All @@ -1150,6 +1154,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'
1 change: 1 addition & 0 deletions packages/desktop-shell/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/desktop-shell/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
20 changes: 4 additions & 16 deletions packages/desktop-shell/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -307,22 +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::<ApplicationState>();
let canonical = match fs::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,
Comment thread
yiliang114 marked this conversation as resolved.
Err(error) => {
emit_runtime_failure(
&app,
generation,
format!("Failed to open workspace {}: {error}", workspace.display()),
);
emit_runtime_failure(&app, generation, error);
return;
}
};
Expand Down
79 changes: 71 additions & 8 deletions packages/desktop-shell/src-tauri/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -44,13 +44,13 @@ impl DesktopRuntime {
pub fn start(app: &AppHandle, workspace: &Path, log_path: &Path) -> Result<Self, String> {
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())
Expand Down Expand Up @@ -176,13 +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<PathBuf, String> {
let workspace = fs::canonicalize(configured).map_err(|error| {
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 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()
));
}
Ok(())
}

pub(crate) fn resolve_workspace(configured: &Path) -> Result<PathBuf, String> {
// 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() {
Comment thread
yiliang114 marked this conversation as resolved.
Ok(workspace)
} else {
Expand Down Expand Up @@ -459,13 +474,61 @@ fn runtime_arguments(workspace: &Path) -> Vec<OsString> {
#[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;
#[cfg(windows)]
use std::path::PathBuf;
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()));
Comment thread
yiliang114 marked this conversation as resolved.
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}"
);
}

#[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 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]
fn parses_loopback_listening_line() {
let url = parse_listening_url(
Expand Down
Loading