Skip to content

Commit f74df5f

Browse files
merge: stage1-hostcalls into master
Merge stage1-hostcalls branch introducing: - PipelineRecord event handling in repl and web client - Tab completion support (TabExpansion2) - Enhanced hostcall types and handlers - Information record message formatting with ANSI colors - New psrp_record module for debug/verbose/warning/progress records - License files for ironposh-web Resolved conflicts: - Kept version 0.3.2 (HEAD) - Kept multi-line formatting style - Fixed duplicate module declarations
2 parents 045c2ea + 47f0d51 commit f74df5f

File tree

22 files changed

+3755
-367
lines changed

22 files changed

+3755
-367
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ jobs:
7171
rustup target add wasm32-unknown-unknown
7272
7373
- name: Install wasm-pack
74-
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
74+
run: |
75+
cargo install wasm-pack --locked --version 0.13.1
7576
7677
- name: Build WASM Package
7778
working-directory: crates/ironposh-web
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# ironwinrm Web(WASM)记录流测试矩阵
2+
3+
目标:验证 `crates/ironposh-web` 在 Web 端不会因为 PSRP record(Warning/Verbose/Debug/Progress/Information 等)而 panic,并且 JS 能拿到足够上下文(pipelineId/stream/messageType 等)用于定位。
4+
5+
> 说明:此矩阵以 CI 的构建方式为准(见 `.github/workflows/ci.yml`)。
6+
7+
---
8+
9+
## 构建/产物检查
10+
11+
|| 命令/动作 | 期望 | 结果 |
12+
|---|---|---|---|
13+
| WASM 构建 | `cd D:\ironwinrm\crates\ironposh-web; wasm-pack build --target web --scope devolutions` | 生成 `crates/ironposh-web/pkg``.d.ts``PipelineRecord/WasmPsrpRecord` | PASS |
14+
| 组件构建 | `cd D:\ironwinrm\web\powershell-terminal-component; npm run build:component` | TS 构建通过,Vite 打包成功 | PASS |
15+
| App 构建 | `cd D:\ironwinrm\web\powershell-terminal-app; npm run build` | `tsc && vite build` 通过 | PASS |
16+
17+
---
18+
19+
## 端到端(手动 Playwright)覆盖
20+
21+
环境:
22+
- 使用 `web/powershell-terminal-app` 的页面表单(值来自 `.env` / VITE_*),连接到正在运行的 gateway + WinRM 目标。
23+
- 通过 Playwright 自动输入命令,并在 xterm DOM 中等待 marker 出现(确保事件链路打通且会话未崩)。
24+
25+
| Case | PowerShell 命令 | 期望 Web 端行为 | 结果 |
26+
|---|---|---|---|
27+
| WarningRecord | `Write-Warning '__E2E_WARN__'` | 不 panic;终端可见 marker;应走 `PipelineRecord(kind=warning)` 或等价输出 | PASS |
28+
| VerboseRecord | `Write-Verbose '__E2E_VERBOSE__' -Verbose` | 不 panic;终端可见 marker;应走 `PipelineRecord(kind=verbose)` | PASS |
29+
| DebugRecord | `Write-Debug '__E2E_DEBUG__' -Debug` | 不 panic;终端可见 marker;应走 `PipelineRecord(kind=debug)` | PASS |
30+
| InformationRecord | `Write-Information '__E2E_INFO__'` | 不 panic;终端可见 marker;应走 `PipelineRecord(kind=information)` | PASS |
31+
| ProgressRecord + 完成标记 | `1..5 | % { Write-Progress -Activity 'E2E' -Status $_ -PercentComplete ($_*20); Start-Sleep -Milliseconds 50 }; Write-Output '__E2E_PROGRESS_DONE__'` | 不 panic;终端可见 DONE marker;progress 记录应走 `PipelineRecord(kind=progress)` | PASS |
32+
| 会话继续可用 | `Write-Output '__E2E_AFTER__'` | 前面 record 出现后仍可继续执行命令 | PASS |
33+
34+
---
35+
36+
## 未覆盖/待补充
37+
38+
| Case | 原因 | 建议补充方式 |
39+
|---|---|---|
40+
| `PipelineRecord(kind=unsupported)` | 正常 PowerShell 脚本难以稳定触发“未知 message_type” | 在测试环境注入/回放一段包含未知 message_type 的 PSRP 数据,或增加专用“回放模式”集成测试 |
41+

IRONWINRM_WEB_TDD_PLAN.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# ironwinrm(Rust + WASM + Web)端到端计划与 TDD 矩阵
2+
3+
本文档聚焦:`crates/ironposh-web`(WASM 包)在 Web 端的可用性与可维护性,尤其是 HostCall/PSRP records 的类型安全与“遇到未知/异常数据不崩”的鲁棒性。
4+
5+
## 目标
6+
- Web 端不会因为某个 PSRP message / record 解析失败而“卡死”(stream 不再推进)。
7+
- JS/TS 能拿到足够上下文定位问题来源(哪条 pipeline、最后一个事件、失败的 record 类型等)。
8+
- HostCall/Record 的跨边界数据结构尽量强类型(避免 `any`),字段命名约定一致(建议 camelCase)。
9+
- 对于不可完全解析/不支持的消息:必须走降级路径(lossy / unsupported),而不是 panic/unwrap。
10+
11+
## 端到端落地步骤(建议顺序)
12+
1. **边界类型梳理**:列出 WASM 暴露给 TS 的所有 event/record/hostcall 类型;确认哪些字段是 snake_case、哪些是 camelCase。
13+
2. **失败可观测性**
14+
- JS 侧:对 `stream.next()` 增加超时与“最后事件摘要”错误信息(pipelineCreated/finished/record.kind 等)。
15+
- Rust 侧:解析失败必须 `warn!(...)`(结构化字段),并继续流转。
16+
3. **Records 的降级策略**
17+
-`Write-Information``MessageData: object` 的场景:允许 message 不是字符串,使用 `to_string`/属性 fallback。
18+
- 对未知 message_type:映射为 `Unsupported` record(携带可读 preview)。
19+
4. **HostCalls 全覆盖**:按 HostCall 分类(HostInfo/Input/Output/RawUI)逐个实现/对齐 tokio client 行为;每加一个,补一条 E2E。
20+
5. **发布与回归**:每次变更后本地 `wasm-pack build` + WebTerminal Playwright E2E 全跑;保证回归可复现。
21+
22+
---
23+
24+
## TDD 矩阵(Rust 侧)
25+
> 目标:保证解析、降级、以及“不会卡死”这些关键语义在 Rust 层就可验证。
26+
27+
### R1:InformationRecord 解析(严格)
28+
- 输入:`InformationalRecord_Message``string`
29+
- 期望:`InformationRecord::try_from` 成功,字段 roundtrip
30+
- 类型:单元测试(`crates/ironposh-psrp/src/messages/information_record.rs`
31+
32+
### R2:InformationRecord 解析(MessageData 非 string)
33+
- 输入:`InformationalRecord_Message` 是非字符串(例如复杂对象/数值)
34+
- 期望:严格解析返回 Err;上层必须能走 lossy(不 panic)
35+
- 类型:新增单元测试(psrp 层)+ 新增单元测试(client-core 的 lossy 路径)
36+
37+
### R3:RunspacePool 接收 InformationRecord(lossy 不终止会话)
38+
- 输入:构造一个“无法严格解析”的 InformationRecord message
39+
- 期望:
40+
- 产生 `PsrpRecord::Information`(message_data 为 fallback string)或 `Unsupported`
41+
- 不返回 Err,不中断消息循环
42+
- 类型:单元/集成测试(`crates/ironposh-client-core`
43+
44+
### R4:未知/不支持 message_type
45+
- 输入:未知 message_type 的 payload
46+
- 期望:映射为 `PsrpRecord::Unsupported`,且 pipeline 仍能 `Finished`
47+
- 类型:集成测试(client-core)
48+
49+
---
50+
51+
## TDD 矩阵(WASM/TS 侧)
52+
> 目标:确保浏览器侧不会 hang、不会吞错误,并且输出/hostcall 行为可回归。
53+
54+
### W1:Records(Warning/Verbose/Debug/Information/Progress)
55+
- 执行:分别运行 `Write-Warning/Write-Verbose/Write-Debug/Write-Information/Write-Progress`
56+
- 期望:
57+
- `runCommand()` 返回(不 hang)
58+
- 输出包含 marker
59+
- 会话仍可继续执行下一条命令
60+
- 类型:Playwright E2E(推荐使用真实 gateway)
61+
62+
### W2:Host UI hostcalls
63+
- 执行:`$Host.UI.Write*` 系列(Warning/Error/Verbose/Debug/Write/WriteLine)
64+
- 期望:浏览器侧 hostcall handler 都能接到对应方法名;不 crash
65+
- 类型:Playwright E2E
66+
67+
### W3:RawUI(Clear-Host)
68+
- 执行:`Clear-Host`
69+
- 期望:触发 `ClearScreen` 或 buffer ops(`SetBufferContents*`),且会话仍可继续
70+
- 类型:Playwright E2E
71+
72+
### W4:错误可定位
73+
- 执行:构造会导致解析失败/卡死风险的命令
74+
- 期望:JS 抛出的错误包含:超时、最后事件摘要(last event summary)、命令片段
75+
- 类型:Playwright E2E(断言错误文本)
76+
77+
---
78+
79+
## 错误信息是否足够(给 JS 定位)
80+
最低要求:
81+
- JS 侧 `runCommand()` 在等待 `stream.next()` 时设置超时;超时错误包含:`lastEventSummary`、是否收到 `PipelineCreated`、是否已 `kill`、以及命令片段。
82+
- Rust 侧对无法解析的 record 走 lossy,并用结构化日志记录:`target``error``command_id/pipeline_id`(不要记录敏感信息)。

WASM_PSRP_PROD_PLAN.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# WASM/PSRP 稳定性与类型安全:端到端计划 + TDD 矩阵
2+
3+
> 目标:让 `crates/ironposh-web` 在生产环境中遇到 **任何** PSRP/Host 输出时都“不断线、不 panic、可观测、类型安全”。
4+
> 当前已知痛点:某些 PSRP record(例如 `WarningRecord`)会触发 wasm `panic -> RuntimeError: unreachable`,导致会话中断。
5+
6+
---
7+
8+
## 范围与定义
9+
10+
### 范围(本计划覆盖)
11+
- Rust(协议/会话核心):`crates/ironposh-client-core``crates/ironposh-psrp`(必要时也涉及 `crates/ironposh-terminal`)。
12+
- WASM API:`crates/ironposh-web`(wasm-bindgen 导出、事件回调、HostCall 交互)。
13+
- TS/JS(类型):由 `tsify` 生成或手写的 `.d.ts` / hostcall 对象模型(目标是消灭 `any``format!` 拼装 payload)。
14+
15+
### 不在范围(但会被依赖/影响)
16+
- 具体 UI(demo/终端渲染组件)的样式问题、`Write-Host` 特殊行为(除非它触发协议崩溃)。
17+
18+
### Done(“能在 prod thrive” 的最低标准)
19+
- **不崩溃**:任何 PSRP message/record 都不会触发 panic(尤其是 wasm `unreachable`)。
20+
- **不丢会话**:遇到未知/暂不支持的 message 类型也能继续执行后续命令。
21+
- **可观测**:未知类型会被结构化记录(Rust `tracing`)且可选择上报给前端。
22+
- **类型安全**:HostCall payload/返回值在 Rust 与 TS 两端都强类型;公共 API 不暴露 `any`
23+
24+
---
25+
26+
## 端到端计划(聚焦 1/2/3)
27+
28+
### 里程碑 1:止血(Rust/WASM 不再 panic)
29+
**目标**:把所有 “Unhandled message_type/record -> panic” 改为可恢复路径(降级/转发事件)。
30+
31+
1. **复现与枚举**
32+
- 建立最小脚本集合,分别触发:
33+
- Warning(`Write-Warning``$Host.UI.WriteWarningLine`
34+
- Verbose(`Write-Verbose -Verbose`
35+
- Debug(`Write-Debug -Debug`
36+
- Progress(`Write-Progress`
37+
- Information(`Write-Information` / `Write-Host` 相关路径)
38+
- 记录每个脚本在 wasm 下的行为:是否 panic、是否丢输出、是否中断会话。
39+
40+
2. **替换 panic 为“降级处理”**
41+
-`crates/ironposh-client-core`(例如 `runspace_pool/pool.rs` 的 message 分发):
42+
-`panic!/unreachable!/unwrap` 等致命路径替换为:
43+
- `SessionEvent::RecordUnsupported { message_type, stream, command_id, raw_summary }`(建议)
44+
- 或最简单的:把内容降级为 `stdout/stderr` 文本输出(保证不断线)
45+
- Rust 侧统一使用 `tracing` 结构化日志(不要 log secret):
46+
- `warn!(message_type = %..., stream = %..., command_id = ?..., "unsupported PSRP record; downgraded");`
47+
48+
3. **验收**
49+
- 在 wasm 环境下执行上述最小脚本:不 panic、不掉线,至少能继续执行下一条命令。
50+
51+
---
52+
53+
### 里程碑 2:统一 “Record → Event” 模型(减少 TS 猜测)
54+
**目标**:前端只负责渲染,不参与协议语义推断;所有 record/stream 都通过统一事件输出。
55+
56+
1. **Rust 事件模型(建议形态)**
57+
- `SessionEvent`(或类似名字)新增/稳定化以下事件(按需精简):
58+
- `OutputText { stream: OutputStream, text: String }`
59+
- `RecordText { level: RecordLevel, stream: OutputStream, text: String, command_id: Option<Uuid> }`
60+
- `Progress { activity_id: i32, parent_activity_id: i32, activity: String, status: String, percent: Option<i32> }`
61+
- `Unsupported { message_type: u32, stream: OutputStream, command_id: Option<Uuid>, summary: String }`
62+
- 关键原则:
63+
- **永不 panic**:未知类型必须走 `Unsupported`
64+
- **可节流**:Progress/频繁事件需要可控频率(避免 UI 卡顿)。
65+
66+
2. **WASM 导出**
67+
- wasm-bindgen 导出一个事件回调或事件队列 API:
68+
- `on_session_event((event: JsSessionEvent) => void)`
69+
- `JsSessionEvent` 必须是稳定 schema(serde + tsify),避免字符串拼装。
70+
71+
3. **TS 绑定与渲染策略**
72+
- 统一在一个地方把 `JsSessionEvent` 映射到:
73+
- xterm 输出(stdout/stderr)
74+
- progress 区域(可选)
75+
- toast/日志(Unsupported)
76+
- TS 不应该自行判断 “PSRP WarningRecord 应该如何解析”;应由 Rust 产生 `RecordText/Unsupported`
77+
78+
4. **验收**
79+
- 用最小脚本触发 Warning/Progress 等:
80+
- 前端能收到事件(至少 `RecordText``Unsupported`),并且会话不中断。
81+
82+
---
83+
84+
### 里程碑 3:HostCall 全面强类型化(Rust structs + tsify + TS union)
85+
**目标**:公共 API 不再出现 `any`,HostCall payload 不再由 `format!`/拼字符串生成。
86+
87+
1. **Rust:把 payload 全部改成结构体**
88+
- 对每个 HostCall payload / result:
89+
- 定义 `struct` + 统一宏属性:
90+
- `#[tsify(into_wasm_abi, from_wasm_abi)]`
91+
- `#[serde(rename_all = "camelCase")]`
92+
- 规则:
93+
- 不允许 `format!` 生成 JSON/字段名/协议数据(除非纯日志字符串)。
94+
95+
2. **TS:可判别联合(discriminated union)**
96+
- `JsHostCall` 定义为:
97+
- `{ kind: "WriteLine2", params: ... } | { kind: "Prompt", params: ... } | ...`
98+
- Handler 定义为:
99+
- `type TypedHostCallHandler = (call: JsHostCall) => HostCallResultMap[call.kind] | Promise<...>`
100+
- 结果映射 `HostCallResultMap``tsify` 输出/或集中维护,避免散落的 module augmentation。
101+
102+
3. **验收**
103+
- TS 编译期可以:
104+
- 通过 `call.kind` 自动收窄类型
105+
- 对每个 hostcall 的返回值类型做静态检查
106+
- Public API(例如 `connect(...)`)不再接受/返回 `any`
107+
108+
---
109+
110+
## TDD 矩阵(Rust 侧)
111+
112+
> 目标:用测试把 “不 panic + 事件正确 + 可继续执行” 固化下来。
113+
114+
### A. 单元测试(优先)
115+
| 场景 | crate/位置建议 | 测试类型 | 输入 | 期望 |
116+
|---|---|---|---|---|
117+
| 未知 message_type 不 panic | `crates/ironposh-client-core`(分发/解析模块) | unit | 构造未知类型 record | 返回 `SessionEvent::Unsupported`,且不中断状态机 |
118+
| WarningRecord → RecordText/Unsupported | `crates/ironposh-client-core` | unit | 构造 WarningRecord | 产生可显示文本事件;不 panic |
119+
| Verbose/Debug/Information 同上 | `crates/ironposh-client-core` | unit | 各 record | 同上 |
120+
| ProgressRecord 节流策略 | `crates/ironposh-client-core` | unit | 高频 progress 序列 | 输出事件数量受控(固定窗口/间隔) |
121+
122+
### B. 集成测试(协议/会话层)
123+
| 场景 | crate/位置建议 | 测试类型 | 输入 | 期望 |
124+
|---|---|---|---|---|
125+
| “遇到 Warning 后继续执行下一条命令” | `crates/ironposh-client-core/tests/` | integration | 两条命令:先触发 warning,再输出 marker | 两条都完成;事件包含 marker |
126+
| 混合 stream:stdout + stderr + record | 同上 | integration | 触发多 stream | 前端事件顺序可解释(至少不丢/不乱序到崩) |
127+
| 长输出 + record 插入 | 同上 | integration | 大量输出插入 record | 不 OOM、不死锁、不 panic |
128+
129+
### C. 质量门槛
130+
- `cargo test -p ironposh-client-core`
131+
- `cargo test -p ironposh-psrp`
132+
- `cargo clippy --all-targets --all-features -- -D warnings`
133+
134+
---
135+
136+
## TDD 矩阵(WASM 侧:ironposh-web)
137+
138+
### A. wasm-bindgen-test(Node/Headless)
139+
| 场景 | crate/位置建议 | 测试类型 | 输入 | 期望 |
140+
|---|---|---|---|---|
141+
| JS 端能订阅 session events | `crates/ironposh-web/tests/``src``#[cfg(test)]` | wasm test | 注册回调并触发模拟事件 | 回调收到 `JsSessionEvent`(schema 正确) |
142+
| Unsupported 事件 schema 稳定 | 同上 | wasm test | 构造 Unsupported | TS 侧可解析字段(camelCase) |
143+
| Progress 事件节流在 JS 可承受 | 同上 | wasm test | 高频 progress 注入 | JS 收到事件数量受控 |
144+
145+
建议命令(示例):
146+
- `cd D:\\ironwinrm\\crates\\ironposh-web; wasm-pack test --node`
147+
148+
### B. 端到端(web demo / Playwright)
149+
| 场景 | 位置建议 | 测试类型 | 输入 | 期望 |
150+
|---|---|---|---|---|
151+
| 触发 Warning/Progress 不崩溃 | `web/` demo + Playwright | e2e | 运行脚本触发记录 | 页面不中断;后续命令仍可执行;console 无 wasm panic |
152+
| HostCall Prompt 输入不吞字符 | 同上 | e2e | `Read-Host` + 先输入别的命令 | prompt 读取值准确;前一次命令不会泄漏 |
153+
154+
---
155+
156+
## 实施顺序建议(最小可交付)
157+
1. Rust:先把 `WarningRecord` 这条 panic 链路修到 **永不 panic**(即便先降级为 Unsupported 文本)。
158+
2. Rust:补齐 Progress/Verbose/Debug/Information 的降级处理。
159+
3. WASM:导出统一事件模型(最小:OutputText + Unsupported)。
160+
4. HostCall:逐个替换 `format!`,确保 tsify + camelCase,并收紧 TS handler 类型。
161+
5. 用 E2E 恢复/新增覆盖(warning/progress),作为发布门槛。
162+
163+
---
164+
165+
## 备注:关于 “让它在 prod thrive”
166+
- 真正的生产稳定性依赖于:**协议层永不 panic + 事件模型稳定 + 类型系统不漏 any**
167+
- UI/terminal 的“显示效果”可以迭代,但协议层的“不断线/不中断”必须第一优先级。
168+

crates/ironposh-client-core/src/connector/active_session.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
use ironposh_psrp::{ErrorRecord, PipelineOutput, PsPrimitiveValue, PsValue};
1212
use tracing::{error, info, instrument, warn};
1313

14-
#[expect(clippy::large_enum_variant)]
14+
#[allow(clippy::large_enum_variant)]
1515
#[derive(Debug, PartialEq, Eq)]
1616
pub enum UserEvent {
1717
PipelineCreated {
@@ -28,6 +28,10 @@ pub enum UserEvent {
2828
error_record: ErrorRecord,
2929
handle: PipelineHandle,
3030
},
31+
PipelineRecord {
32+
pipeline: PipelineHandle,
33+
record: crate::psrp_record::PsrpRecord,
34+
},
3135
}
3236

3337
impl UserEvent {
@@ -44,11 +48,12 @@ impl UserEvent {
4448
..
4549
} => powershell.id(),
4650
Self::ErrorRecord { handle, .. } => handle.id(),
51+
Self::PipelineRecord { pipeline, .. } => pipeline.id(),
4752
}
4853
}
4954
}
5055

51-
#[expect(clippy::large_enum_variant)]
56+
#[allow(clippy::large_enum_variant)]
5257
#[derive(Debug)]
5358
pub enum ActiveSessionOutput {
5459
SendBack(Vec<TrySend>),
@@ -344,6 +349,12 @@ impl ActiveSession {
344349
handle,
345350
}));
346351
}
352+
AcceptResponsResult::PipelineRecord { record, handle } => {
353+
outs.push(ActiveSessionOutput::UserEvent(UserEvent::PipelineRecord {
354+
pipeline: handle,
355+
record,
356+
}));
357+
}
347358
}
348359
}
349360

crates/ironposh-client-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod credentials;
55
pub mod host;
66
pub mod pipeline;
77
pub mod powershell;
8+
pub mod psrp_record;
89
pub mod runspace;
910
pub mod runspace_pool;
1011

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use ironposh_psrp::{InformationRecord, MessageType, ProgressRecord};
2+
use uuid::Uuid;
3+
4+
#[derive(Debug, Clone, PartialEq, Eq)]
5+
pub struct PsrpRecordMeta {
6+
pub message_type: MessageType,
7+
pub message_type_value: u32,
8+
pub stream: String,
9+
pub command_id: Option<Uuid>,
10+
pub data_len: usize,
11+
}
12+
13+
#[derive(Debug, Clone, PartialEq, Eq)]
14+
pub enum PsrpRecord {
15+
Debug {
16+
meta: PsrpRecordMeta,
17+
message: String,
18+
},
19+
Verbose {
20+
meta: PsrpRecordMeta,
21+
message: String,
22+
},
23+
Warning {
24+
meta: PsrpRecordMeta,
25+
message: String,
26+
},
27+
Information {
28+
meta: PsrpRecordMeta,
29+
record: InformationRecord,
30+
},
31+
Progress {
32+
meta: PsrpRecordMeta,
33+
record: ProgressRecord,
34+
},
35+
Unsupported {
36+
meta: PsrpRecordMeta,
37+
data_preview: String,
38+
},
39+
}

0 commit comments

Comments
 (0)