From e887ffc66bc2236e20db7632333bdb600ba22325 Mon Sep 17 00:00:00 2001 From: Xuz Date: Thu, 1 Oct 2026 21:23:45 +0800 Subject: [PATCH] =?UTF-8?q?Windows=20=E4=B8=8A=20SFTP=20=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E7=9A=84=E8=B7=AF=E5=BE=84=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=B8=A6=20`\\=3F\`=20=E5=89=8D=E7=BC=80=EF=BC=9A=E8=AF=BB?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=92=8C=E4=B8=8A=E4=BC=A0=E7=9A=84=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=E6=94=B9=E7=94=A8=20dunce=20=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E5=8C=96=EF=BC=8CWindows=20CI=20=E5=8A=A0=E8=B7=91=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E8=AF=BB=E7=9B=AE=E5=BD=95=E7=9A=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/windows.yml | 6 +++++- CLAUDE.md | 2 +- Cargo.lock | 1 + Cargo.toml | 2 ++ src/sftp/model.rs | 5 ++++- src/sftp/tests.rs | 13 +++++++++++++ src/sftp/upload.rs | 3 ++- 7 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index e184767..0448cf9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,6 +1,7 @@ # The Windows build, which the Macs this is developed on cannot compile: # the whole crate through clippy, the external CLI's tests, including the -# named pipe between two processes, and the updater's. +# named pipe between two processes, the updater's, and the local SFTP pane's +# paths (std canonicalizes to `\\?\C:\…` there). # # A push runs it only when it touches code that differs on Windows (the # files with `cfg(windows)` and friends), the build script or the @@ -21,6 +22,8 @@ on: - "src/bin/**" - "src/ssh/connection.rs" - "src/sftp/download.rs" + - "src/sftp/model.rs" + - "src/sftp/upload.rs" - "src/credential/credential_dialog.rs" - "src/settings/settings_panel.rs" - "tests/cli_bin.rs" @@ -48,4 +51,5 @@ jobs: - run: cargo clippy --all-targets -- -D warnings - run: "cargo test --lib cli::" - run: "cargo test --lib update::" + - run: "cargo test --lib sftp::tests::local_listing" - run: cargo test --test cli_bin diff --git a/CLAUDE.md b/CLAUDE.md index fa8b2f8..1a0a72f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,7 +133,7 @@ cargo test -- --ignored # 会读写真实钥匙串的测试 - 用同步的 Win32 管道,不用 tokio 的:请求线程要阻塞的 `File`,而 tokio 的管道是 overlapped 句柄,拿去做同步 IO 时 std 会中止进程。 - 监听线程先建好下一个实例再把已连接的交给请求线程,否则那一刻来的客户端会看到「没运行」。请求线程结束时直接关句柄,**不要** `DisconnectNamedPipe`(会丢掉客户端还没读的数据)也不要 `FlushFileBuffers`(客户端挂住时会一直阻塞)。停止时置标志、反复以客户端身份连一下唤醒 `ConnectNamedPipe`,直到线程结束。 - **Windows 的 PATH 是副本加用户 PATH。** `IntegrationPaths::system()` 在 Windows 上:`exe` 是 `shellrs.exe` 旁边的 `shellrs-cli.exe`,`bin_link` 是 `%LOCALAPPDATA%\ShellRS\bin\shellrs.exe`(副本,不是链接),`user_path` 是 `UserPath::Registry`(`HKCU\Environment\Path`,写回 `REG_EXPAND_SZ` 后广播 `WM_SETTINGCHANGE`;只有之后启动的程序看得到)。副本按长度加修改时间判断是否本版(安装时用 `set_modified` 把时间设成源文件的)。正在运行的副本不能覆盖但能改名,所以安装、移除都先把旧的改名成 `shellrs.exe..old` 再尽量删掉。生产环境启动时 `CliIntegration::update_outdated` 在后台更新旧副本,否则 Agent 会一直碰到 `version_mismatch`。测试一律 `user_path: None` 或 `UserPath::File`,绝不碰注册表。开发时 `cargo run` 只构建 `shellrs`,要 `cargo build` 才有 `shellrs-cli.exe`,否则设置页显示找不到。 - - **Windows 只在 CI 上编译和测试**(`.github/workflows/windows.yml`:clippy 全 crate、`cargo test --lib cli::`、`cargo test --lib update::`、`cargo test --test cli_bin`),本机没有 Windows 工具链;改 `pipe_windows.rs`、`install_windows.rs` 的 Windows 部分和 `cfg(windows)` 代码后要看 CI。它不是每次推送都跑:只在推送改到 `paths` 里列的文件(含 `cfg(windows)` 等平台代码的文件、`build.rs`、`Cargo.toml` / `Cargo.lock`)时才跑,tag 不触发;在别的文件里新加 Windows 代码时把文件加进这个列表,或者在 Actions 里手动运行。 + - **Windows 只在 CI 上编译和测试**(`.github/workflows/windows.yml`:clippy 全 crate、`cargo test --lib cli::`、`cargo test --lib update::`、`cargo test --lib sftp::tests::local_listing`(本地面板的路径:std 在 Windows 上把路径规范化成 `\\?\C:\…`,读目录改用 `dunce::canonicalize`)、`cargo test --test cli_bin`),本机没有 Windows 工具链;改 `pipe_windows.rs`、`install_windows.rs` 的 Windows 部分和 `cfg(windows)` 代码后要看 CI。它不是每次推送都跑:只在推送改到 `paths` 里列的文件(含 `cfg(windows)` 等平台代码的文件、`build.rs`、`Cargo.toml` / `Cargo.lock`)时才跑,tag 不触发;在别的文件里新加 Windows 代码时把文件加进这个列表,或者在 Actions 里手动运行。 - **每个请求一个线程**,后端方法都是阻塞的;SSH 句柄绑在创建它的 runtime 上,所以 `ssh::run_command` 和 SFTP 工作线程各自建 current-thread runtime。 - **exec 不许挂住**:`SshPrompts` 非交互;陌生主机在 events 回调里直接返回 `false`(`ask` 没人回答会永远等下去,因为超时不计提示时间),缺凭据由 `MissingCredential` 失败。`exec(true)` 后立刻 `eof()`,远程没有 stdin;读循环读到 `Close` / `None` 为止,退出码可能在 EOF 之后才到。输出回调写失败(Agent 断开)就放弃命令、断开连接。 - **传输无人值守**:`SshCliBackend::transfer` 先等 `Connected` 再发请求;连接前的 `Prompt` 一律回 `Cancel` 并按提示种类给出错误码;`Question` 按固定策略回答(冲突覆盖并勾选全部、续传一律重新开始、出错跳过并记为失败);以 `Idle` 为结束。请求用 `UploadRequest::scp` / `DownloadRequest::scp`:worker 连上后按 scp 规则解析目标(`scp_remote_target` / `scp_local_target`,目标目录必须存在,远程还要 `stat` 确认,因为有的服务器 realpath 不存在的路径也成功),并让新建文件保留源文件的权限位(上传整套 `0o777`,下载只补可执行位);SFTP 标签的请求不受影响。批次准备失败时 worker 也发 `Idle`。 diff --git a/Cargo.lock b/Cargo.lock index 49f609d..da7d11e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7695,6 +7695,7 @@ dependencies = [ "chrono", "clap", "dirs 6.0.0", + "dunce", "futures", "gpui-kit", "keyring", diff --git a/Cargo.toml b/Cargo.toml index bea1cd1..bbd24b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ async-channel = "2" base64 = "0.22" clap = { version = "4", features = ["derive"] } dirs = "6" +# Canonical local paths without the `\\?\` prefix std adds on Windows. +dunce = "1" futures = "0.3" portable-pty = "0.9" rand = "0.9" diff --git a/src/sftp/model.rs b/src/sftp/model.rs index 829d59e..35f6768 100644 --- a/src/sftp/model.rs +++ b/src/sftp/model.rs @@ -694,7 +694,10 @@ impl LocalDirectoryProvider for SystemLocalDirectoryProvider { .collect() } fn list(&self, path: &Path) -> Result { - let path = std::fs::canonicalize(path)?; + // std's canonical form on Windows is `\\?\C:\Users`; the pane shows + // and bookmarks the `C:\Users` people type, wherever that means the + // same. + let path = dunce::canonicalize(path)?; let mut entries = Vec::new(); for entry in std::fs::read_dir(&path)? { let entry = entry?; diff --git a/src/sftp/tests.rs b/src/sftp/tests.rs index 8fb83d0..88bd350 100644 --- a/src/sftp/tests.rs +++ b/src/sftp/tests.rs @@ -895,6 +895,19 @@ fn paths_and_journals_reject_unsafe_identity() { }); } +/// On Windows std canonicalizes to `\\?\C:\…`, which the pane showed as is. +#[test] +fn local_listing_reports_the_path_people_type() { + let tmp = tempfile::tempdir().unwrap(); + let listing = SystemLocalDirectoryProvider.list(tmp.path()).unwrap(); + assert!(!listing.path().starts_with(r"\\?\"), "{}", listing.path()); + // Still the directory asked for, links resolved. + assert_eq!( + std::fs::canonicalize(listing.path()).unwrap(), + std::fs::canonicalize(tmp.path()).unwrap() + ); +} + #[cfg(target_os = "linux")] #[test] fn local_listing_rejects_unrepresentable_names() { diff --git a/src/sftp/upload.rs b/src/sftp/upload.rs index 998faa1..61f8f11 100644 --- a/src/sftp/upload.rs +++ b/src/sftp/upload.rs @@ -51,7 +51,8 @@ impl UploadBatch { let path = if let (Some(parent), Some(name)) = (absolute.parent(), absolute.file_name()) { match tokio::fs::canonicalize(parent).await { - Ok(parent) => parent.join(name), + // Without Windows' `\\?\`, as the queue shows it. + Ok(parent) => dunce::simplified(&parent).join(name), Err(_) => absolute, } } else {