Compare commits

...
8 changed files with 36 additions and 6 deletions

No files matched your search

+5 -1
View File
@@ -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
+6
View File
@@ -17,6 +17,12 @@ ShellRS 的第一个公开版本。
- 外部 CLI:`shellrs` 命令让 AI Agent 用已保存的主机执行命令、传输文件。
- 在线升级:在后台检查并下载新版本,下好后标题栏右上角提示,重启或退出时安装。
## [0.1.0-beta.4]
### 修复
- Windows:SFTP 本地面板的路径不再显示成 `\\?\C:\Users` 这样,而是平常的 `C:\Users`,传输队列的「来源」也一样。以前添加的本地书签如果带着 `\\?\`,删掉重新添加即可;升级前停在一半的上传不能接着续传,需要重新开始。
## [0.1.0-beta.3]
### 改进
+1 -1
View File
@@ -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.<uuid>.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`。
Generated
+2 -1
View File
@@ -7686,7 +7686,7 @@ dependencies = [
[[package]]
name = "shellrs"
version = "0.1.0-beta.3"
version = "0.1.0-beta.4"
dependencies = [
"alacritty_terminal",
"anyhow",
@@ -7695,6 +7695,7 @@ dependencies = [
"chrono",
"clap",
"dirs 6.0.0",
"dunce",
"futures",
"gpui-kit",
"keyring",
+3 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "shellrs"
version = "0.1.0-beta.3"
version = "0.1.0-beta.4"
edition = "2024"
publish = false
description = "SSH session manager with local terminal support"
@@ -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"
+4 -1
View File
@@ -694,7 +694,10 @@ impl LocalDirectoryProvider for SystemLocalDirectoryProvider {
.collect()
}
fn list(&self, path: &Path) -> Result<DirectoryListing> {
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?;
+13
View File
@@ -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() {
+2 -1
View File
@@ -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 {