expect-pty 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/CONTRIBUTING.md +10 -4
- data/README.md +13 -4
- data/docs/INTERNAL_CONTRACTS.md +21 -0
- data/docs/RELEASING.md +26 -12
- data/docs/VERIFICATION.md +98 -0
- data/examples/kibitz/README.md +28 -20
- data/examples/kibitz/kibitz.rb +1 -0
- data/lib/expect/interaction.rb +2 -0
- data/lib/expect/relay.rb +37 -7
- data/lib/expect/session_resources.rb +39 -14
- data/lib/expect/terminal.rb +1 -1
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +70 -33
- data/script/release.rb +10 -2
- data/test/initialization_failure_test.rb +92 -0
- data/test/integration/README.md +42 -23
- data/test/ownership_sequence_test.rb +208 -0
- data/test/process_interruption_test.rb +296 -0
- data/test/relay_reentrancy_test.rb +159 -0
- data/test/release_test.rb +47 -0
- data/test/write_contract_test.rb +105 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85c1290ea232f7bf915d4c0d95a50f1c8fd2e4ac89ce423ee46cdc66f3632ff5
|
|
4
|
+
data.tar.gz: f2db309b7f7543a82736dd39b216de1b003d75c92c044582f95999b010077677
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f26e3067e300d27354ffb3251474c7644d87df6b8efa72615ff4e6695437de65a395fc7654fbb06e21dfb52ca744131110e2b14590250b930d7492ef71b29ca3
|
|
7
|
+
data.tar.gz: 1b86664a4d28868d44a4bd5c513a1c26e07483b0aae23562cd41c62bfc9bfc0a9ecb8cadc9fb44011445174b34524ff3905a34670bd24f7a9675b5d5e9dbb8fc
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.3 - 2026-09-27
|
|
6
|
+
|
|
7
|
+
- **初始化清理**:资源账本建立前的中断也会尝试关闭全部所属 IO,保留借用 IO 与原始异常身份。
|
|
8
|
+
- **进程回收**:等待及关闭在 EINTR 后沿用原预算,终结器继续有限清理;保持软关闭、创建者归属及未知状态约定。
|
|
9
|
+
- **转接边界**:新增 `Expect::ReentrancyError`,阻止活跃 Relay 的同源递归,避免重复交付;保留独立来源嵌套、嵌套匹配和顺序恢复。
|
|
10
|
+
- **写入协议**:拒绝零、负数、非整数和超出本次片段的写入计数,明确 `write_timeout` 为背压期限。
|
|
11
|
+
- **发布文本**:CHANGELOG 与 release notes 显式使用 UTF-8,本地与工作流共用读取入口,非法 UTF-8 明确报错。
|
|
12
|
+
- **内部命名与验证**:简化清理及 EOF 方法名称,补充构造、回收、递归转接、locale 与连续状态回归。
|
|
13
|
+
|
|
14
|
+
## 0.5.2 - 2026-09-27
|
|
15
|
+
|
|
16
|
+
- **文档排版**:整理贡献、发布、Kibitz 示例和 SSH 集成测试说明的换行与表格格式。
|
|
17
|
+
|
|
5
18
|
## 0.5.1 - 2026-09-27
|
|
6
19
|
|
|
7
20
|
- **终端清理**:`stty` 读取失败或中断后也会清理辅助管道和进程,保留原始异常,避免影响主会话。
|
data/CONTRIBUTING.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 参与贡献
|
|
2
2
|
|
|
3
|
-
欢迎中文或英文 issue 和 PR。库的公共 API 名称和错误信息使用英文;库内的责任划分、复杂生命周期和协议解释使用中文,RuboCop
|
|
3
|
+
欢迎中文或英文 issue 和 PR。库的公共 API 名称和错误信息使用英文;库内的责任划分、复杂生命周期和协议解释使用中文,RuboCop
|
|
4
|
+
等工具指令保持原格式。文档以现有文件的语言为准,示例中的可编辑命令保持直接清晰。
|
|
4
5
|
|
|
5
6
|
## 本地检查
|
|
6
7
|
|
|
@@ -12,12 +13,17 @@ bundle exec rake # RuboCop 和默认测试
|
|
|
12
13
|
script/ci # 另含示例、Gem 构建及隔离安装验证
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
修改转接或匹配时,先运行对应的 `test/*_test.rb`,再运行 `bundle exec rake`。默认测试不需要 SSH;真实 SSH
|
|
16
|
+
修改转接或匹配时,先运行对应的 `test/*_test.rb`,再运行 `bundle exec rake`。默认测试不需要 SSH;真实 SSH
|
|
17
|
+
集成测试的环境要求见 [测试说明](test/integration/README.md)。发布操作和历史验证分别见 [发布说明](docs/RELEASING.md)
|
|
18
|
+
与 [验证记录](docs/VERIFICATION.md)。
|
|
16
19
|
|
|
17
|
-
RuboCop 对生产代码保留有限的长度、ABC、嵌套、参数数量和分支复杂度门槛。测试及基准的场景编排豁免方法长度与 ABC
|
|
20
|
+
RuboCop 对生产代码保留有限的长度、ABC、嵌套、参数数量和分支复杂度门槛。测试及基准的场景编排豁免方法长度与 ABC
|
|
21
|
+
计数;协议状态机的分支豁免只放在对应方法,并注明原因。阈值用于提示复核,职责拆分仍应保持状态归属和异常恢复逻辑完整。
|
|
18
22
|
|
|
19
23
|
## 提交问题或改动
|
|
20
24
|
|
|
21
|
-
问题报告请附 Ruby
|
|
25
|
+
问题报告请附 Ruby
|
|
26
|
+
版本、操作系统、最小复现代码、预期与实际结果,以及必要的异常信息;日志中请删去凭据和会话敏感内容。修复行为缺陷时,先加入能在旧实现复现问题的回归测试。涉及缓冲、转义、背压或进程清理的改动,请对照 [内部状态与数据归属](docs/INTERNAL_CONTRACTS.md)
|
|
27
|
+
,说明超时、EOF 和失败后的恢复行为。
|
|
22
28
|
|
|
23
29
|
PR 请说明行为变化、运行过的命令及结果。不要把本机生成的 `tmp/`、日志或凭据加入提交。
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提
|
|
|
16
16
|
项目和仓库名为 `expect-ruby`,Gem 名为 `expect-pty`。在应用的 Gemfile 中添加以下内容,然后运行 `bundle install`:
|
|
17
17
|
|
|
18
18
|
```ruby
|
|
19
|
-
gem "expect-pty", "~> 0.5.
|
|
19
|
+
gem "expect-pty", "~> 0.5.2", require: "expect/pty"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
也可直接执行 `gem install expect-pty`。需要跟随开发分支时,可从 GitHub 安装:
|
|
@@ -29,8 +29,8 @@ gem "expect-pty", git: "https://github.com/gatework/expect-ruby.git", branch: "m
|
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
31
|
mkdir -p tmp
|
|
32
|
-
gem build expect-pty.gemspec --output tmp/expect-pty-0.5.
|
|
33
|
-
gem install ./tmp/expect-pty-0.5.
|
|
32
|
+
gem build expect-pty.gemspec --output tmp/expect-pty-0.5.2.gem
|
|
33
|
+
gem install ./tmp/expect-pty-0.5.2.gem
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
```ruby
|
|
@@ -241,11 +241,15 @@ ready = Expect.readable_sessions(first, second, timeout: 5)
|
|
|
241
241
|
`send_slow` 在每次写入后只检查已经可读的回复,不附加固定等待;返回时不保证收齐最后一个字符引发的回复,完整对话请继续使用
|
|
242
242
|
`expect`。
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
大块写入遇到背压时同时读取输出,避免双向传输互相阻塞。背压等待超过 `write_timeout` 抛出 `Expect::WriteTimeout`,
|
|
245
245
|
`error.bytes_written` 给出本次 `write` 已被底层接受的字节数;这些字节不回滚,不要从头重发整个命令。写入、等待和背压读取中的
|
|
246
246
|
`EINTR` 均保留原期限重试。控制字符可直接发送,例如 `session.write("\x03")`,其信号作用取决于终端设置。`send`、`public_send`、
|
|
247
247
|
`__send__` 保留 Ruby 反射语义。
|
|
248
248
|
|
|
249
|
+
`write_timeout` 是背压相关期限:持续成功的正数短写不会因为总耗时超过它而失败,也不会强行打断同步用户代码。
|
|
250
|
+
它与匹配的 `timeout/deadline`、整次 `interconnect` 的总 `timeout` 分别计算。非空写入要求底层返回实际接受的正整数字节数,
|
|
251
|
+
且不能超过本次片段长度;非法计数立即抛出 `IOError`,空写入仍返回 0。
|
|
252
|
+
|
|
249
253
|
`stty` 需要系统命令位于 `PATH`;缺失时抛出带安装提示的 `IOError`,原始 `Errno::ENOENT` 保留在 `cause`。窗口尺寸和人工接管的终端恢复使用
|
|
250
254
|
Ruby `io/console`。
|
|
251
255
|
|
|
@@ -338,6 +342,11 @@ IO 期限不会强行中断这些代码。普通 `expect` 的同步日志和监
|
|
|
338
342
|
后改用该值。正则及其锚点作用于当前历史窗口,超过窗口的跨读取正则无法匹配,已实时转发的前缀也无法撤回;零长度正则匹配抛出
|
|
339
343
|
`ArgumentError`。日志包括被转接过滤的转义,显式启用 `redact` 时遮盖注册秘密;在 `expect` / `interconnect` 之间切换不会重复记录。
|
|
340
344
|
|
|
345
|
+
一次转接尚未返回时,递归 `interconnect` 的来源若与活跃来源重叠,会在移动缓冲和修改发送游标前抛出
|
|
346
|
+
`Expect::ReentrancyError`。完全独立的来源仍可嵌套转接;`on_sequence` 中的嵌套 `expect/expect_result` 及返回后再次转接仍受支持。
|
|
347
|
+
自定义 `write` 若已产生副作用却抛错、未返回计数,库无法推断已接受的字节数,此时不能保证恢复交付恰好一次。
|
|
348
|
+
这一保护不代表所有会话 API 都可以跨线程并发调用。
|
|
349
|
+
|
|
341
350
|
`interact` 会自动设置并恢复本地输入终端模式,同时保留输出换行处理;输入会话的 `raw_terminal = false` 将设置交给调用方。通用的
|
|
342
351
|
`interconnect` 只负责字节转发,由调用方管理终端模式。`interact` 还会恢复临时监听组、日志开关和转义设置,包括超时和异常路径。
|
|
343
352
|
|
data/docs/INTERNAL_CONTRACTS.md
CHANGED
|
@@ -47,6 +47,11 @@ EOF,不再制造一次超时事件。EOF 或超时回调重置期限后恢复
|
|
|
47
47
|
|
|
48
48
|
转义扫描只在一轮内复用 `history + buffer`,只含字面规则时不构造它。回调继续后重新读取规则、历史和缓冲;不能跨回调保存文本快照。字面转义的潜在前缀暂存,完整前缀交付后才执行转义回调。
|
|
49
49
|
|
|
50
|
+
Relay 在移动缓冲、恢复写入期限之前检查全部来源的 `relay_owner`,有重叠时抛出 `ReentrancyError`。
|
|
51
|
+
重复来源先去重,标记检查和登记只使用短临界区;不跨 IO 或用户回调持锁。退出及准备失败时只归还已转移的缓冲,
|
|
52
|
+
只释放当前调用的 token,不能清除外层标记。不同来源的嵌套转接、转义回调中的 Matcher 和转接返回后的顺序恢复保持可用。
|
|
53
|
+
自定义目标先产生副作用再抛错而不返回计数时,库不能猜测进度;该协议边界不承诺恰好一次,也不承诺通用线程安全。
|
|
54
|
+
|
|
50
55
|
## 关闭与所有权
|
|
51
56
|
|
|
52
57
|
SessionResources 保存创建者 PID、所属句柄、直属子进程和所属日志。借用 IO 不关闭,fork 后的非创建者不发送信号或回收父进程的孩子。soft_close
|
|
@@ -55,9 +60,20 @@ SessionResources 保存创建者 PID、所属句柄、直属子进程和所属
|
|
|
55
60
|
工厂和构造器在校验之前登记资源归属,失败时沿用同一清理流程;启动成功必须显式记录,不能用 PID 非空推断整个工厂调用已经成功。即使
|
|
56
61
|
exec 成功后诊断输出失败,也要立即回收未交付给调用方的子进程。
|
|
57
62
|
|
|
63
|
+
资源账本构造本身也可能被中断。账本尚未发布时,`cleanup_session` 仅按局部 `own` 关闭真实 IO,按对象身份去重,
|
|
64
|
+
一端失败仍尝试其他端;借用 IO 保持打开。账本存在后仍调用统一的 `close`,不维护第二份生命周期状态。
|
|
65
|
+
`cleanup(failed:)` 的关键字表示本次作用域是否已有异常;只有已有主异常时才抑制常规清理错误,保留同一异常对象。
|
|
66
|
+
内部动作使用 `close_resources`、`close_child`、`mark_eof` 等直接名称,不保留旧私有方法别名。
|
|
67
|
+
|
|
58
68
|
显式关闭遇到 IOError/SystemCallError 时,继续尝试其他句柄、交互包装器、日志和子进程清理,最后传播首个清理错误;已经在传播的其他异常保留。失败资源继续持有,后续关闭可重试。GC
|
|
59
69
|
终结器独立尝试句柄、日志、非阻塞回收,常规清理错误不向外传播。终结器不能强引用会话本身。
|
|
60
70
|
|
|
71
|
+
`SessionResources#reap` 只做一次非阻塞系统调用,EINTR 交给所属流程决定:主会话 `process_status` 返回当前未知/缓存状态,
|
|
72
|
+
`wait_for_child` 沿用阶段开始时的绝对期限,并在重试间休眠。自然等待、TERM 等待分别使用调用方预算,硬关闭的 KILL 阶段保留 1 秒预算;
|
|
73
|
+
零预算也先尝试一次,信号 EINTR 不重新计时。soft_close 不发 KILL,ECHILD 清空 PID,ESRCH 后仍尝试回收,预算耗尽也返回已取得的状态。
|
|
74
|
+
GC 只做一次非阻塞回收、最多两次 KILL 尝试及一次 detach;回收/信号 EINTR 不跳过后续阶段,不执行用户日志回调。
|
|
75
|
+
detach 成功才移交 PID,失败则保留未知 PID/status。每个操作仍受创建者归属约束;持续系统调用失败只保证有限尝试,不保证返回前退出。
|
|
76
|
+
|
|
61
77
|
是否保留原始异常由当前构造、块或关闭作用域显式记录,不能直接读取调用者 rescue 中的 `$!`。`Interrupt` 和 `SystemExit`
|
|
62
78
|
同样先清理再传播;`break` / `throw` 不是异常,此时清理失败仍应抛出。
|
|
63
79
|
|
|
@@ -83,6 +99,11 @@ ESRCH 后仍尝试回收。三个阶段结束或系统调用失败后仍有 PID
|
|
|
83
99
|
|
|
84
100
|
未设置总期限时,`timeout: 0` 保持现有缓冲匹配和首次非阻塞轮询语义。期限检查是协作式的,不强行打断单次正则、日志、同步监听器或用户回调;正则执行限时由调用方的 `Regexp` 实例控制。
|
|
85
101
|
|
|
102
|
+
直接写入及 Relay 目标的 `write_timeout` 用于背压等待和中断重试,不是持续成功短写的总耗时限制。
|
|
103
|
+
匹配 `deadline`、写入期限和 Relay 总 `timeout` 分别管理,不自动把匹配期限传给回调中的写入。
|
|
104
|
+
直接写入必须先验证计数为正整数且不超过本次 chunk,再推进 offset;`:wait_writable` 仍进入背压路径,空输入返回 0。
|
|
105
|
+
`WriteTimeout#bytes_written` 只计已确认字节;读取输出引发嵌套写入超时时,对外保留当前写入进度,原异常留在 cause。
|
|
106
|
+
|
|
86
107
|
## 诊断与脱敏归属
|
|
87
108
|
|
|
88
109
|
`logging.rb` 分开处理接收日志、诊断和协议转发。`diagnostic_output` 借用 Logger、可写对象或回调,不进入资源账本;默认沿用 stderr。回调接收冻结的事件 Hash 和 message 字符串,不含会话对象。诊断失败与其他同步 IO 错误同样保留已读取的原始输入。
|
data/docs/RELEASING.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 发布版本
|
|
2
2
|
|
|
3
|
-
项目名为 `expect-ruby`,RubyGems 名为 `expect-pty`。使用 `script/release.rb --rubygems-only` 可仅发布 RubyGems;不加该选项时还会创建
|
|
3
|
+
项目名为 `expect-ruby`,RubyGems 名为 `expect-pty`。使用 `script/release.rb --rubygems-only` 可仅发布 RubyGems;不加该选项时还会创建
|
|
4
|
+
GitHub Release,附带同一个 Gem 和 `SHA256SUMS`。生成文件统一放在已被 Git 忽略的 `tmp/` 下。
|
|
4
5
|
|
|
5
6
|
## 准备版本
|
|
6
7
|
|
|
@@ -12,7 +13,8 @@
|
|
|
12
13
|
|
|
13
14
|
## 仅发布 RubyGems
|
|
14
15
|
|
|
15
|
-
本地已登录 `gem` 时,脚本直接使用已有凭据。如果 RubyGems 要求一次性验证码,`gem push` 会提示输入。此模式不调用 `gh`,不要求推送到
|
|
16
|
+
本地已登录 `gem` 时,脚本直接使用已有凭据。如果 RubyGems 要求一次性验证码,`gem push` 会提示输入。此模式不调用 `gh`,不要求推送到
|
|
17
|
+
GitHub,也不创建标签或 GitHub Release。
|
|
16
18
|
|
|
17
19
|
```sh
|
|
18
20
|
bundle install
|
|
@@ -20,9 +22,13 @@ ruby script/release.rb --rubygems-only --dry-run
|
|
|
20
22
|
ruby script/release.rb --rubygems-only
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
默认执行 `script/ci` 的检查、完整测试、构建和隔离安装验证,构建包保存在 `tmp/ci/expect-pty-版本号.gem`。随后复制到
|
|
25
|
+
默认执行 `script/ci` 的检查、完整测试、构建和隔离安装验证,构建包保存在 `tmp/ci/expect-pty-版本号.gem`。随后复制到
|
|
26
|
+
`tmp/release/版本号/candidate-*/` 的独占目录,核对包内文件并生成发布说明和校验文件。该副本贯穿后续发布,目录会保留供失败重试。正式发布前再次确认源码未变,上传
|
|
27
|
+
RubyGems 后下载远端包核对 SHA256。
|
|
24
28
|
|
|
25
|
-
正式发布逐个核对包内普通文件在目标 Git 提交中的存在性、原始字节和执行权限,同时核对 gemspec
|
|
29
|
+
正式发布逐个核对包内普通文件在目标 Git 提交中的存在性、原始字节和执行权限,同时核对 gemspec 的安装元数据及主页。
|
|
30
|
+
`git status` 为空不能替代这一检查:忽略规则可能隐藏被 glob 收入包的本地文件,`assume-unchanged` 和 `core.filemode`
|
|
31
|
+
也可能隐藏差异。候选包必须与已提交源码一致。
|
|
26
32
|
|
|
27
33
|
`--dry-run` 只做本地验证,可在提交前使用。它仍要求版本号和发布说明完整。
|
|
28
34
|
|
|
@@ -35,11 +41,13 @@ ruby script/release.rb --dry-run
|
|
|
35
41
|
ruby script/release.rb
|
|
36
42
|
```
|
|
37
43
|
|
|
38
|
-
正式发布先创建 GitHub Release,再上传 RubyGems,最后下载 RubyGems 上的包核对 SHA256。GitHub 上还没有标签时,会为当前提交创建
|
|
44
|
+
正式发布先创建 GitHub Release,再上传 RubyGems,最后下载 RubyGems 上的包核对 SHA256。GitHub 上还没有标签时,会为当前提交创建
|
|
45
|
+
`v版本号` 标签。
|
|
39
46
|
|
|
40
47
|
## GitHub Actions 发布
|
|
41
48
|
|
|
42
|
-
GitHub Runner 不会继承本机的 Gem 登录状态。要在 Actions 发布 RubyGems,需在仓库的 Settings → Secrets and variables →
|
|
49
|
+
GitHub Runner 不会继承本机的 Gem 登录状态。要在 Actions 发布 RubyGems,需在仓库的 Settings → Secrets and variables →
|
|
50
|
+
Actions 中配置 `RUBYGEMS_API_KEY`,使用具有 `Push rubygem` 权限的发布 Key。
|
|
43
51
|
|
|
44
52
|
```sh
|
|
45
53
|
git tag -a v0.3.3 -m 'Release v0.3.3'
|
|
@@ -49,22 +57,28 @@ gh workflow run release.yml --ref v0.3.3 --repo gatework/expect-ruby
|
|
|
49
57
|
|
|
50
58
|
也可以在 Actions → Release → Run workflow 选择对应版本标签。工作流仅支持手动触发,避免本地发布时出现第二次并发上传。
|
|
51
59
|
|
|
52
|
-
发布作业先验证标签与版本号一致,再复用 CI 的 Linux/macOS、Ruby 3.2/3.3/3.4/4.0 共 8 个环境。全部通过后,下载 Ubuntu / Ruby
|
|
60
|
+
发布作业先验证标签与版本号一致,再复用 CI 的 Linux/macOS、Ruby 3.2/3.3/3.4/4.0 共 8 个环境。全部通过后,下载 Ubuntu / Ruby
|
|
61
|
+
4.0 作业验证过的 Gem,交给同一个发布脚本;发布阶段不重新构建。
|
|
53
62
|
|
|
54
63
|
未配置 `RUBYGEMS_API_KEY` 时,GitHub Release 仍会创建,RubyGems 步骤会明确失败;此时可以下载 Release 中的原包,在本地使用已有登录状态完成上传。
|
|
55
64
|
|
|
56
65
|
## 失败后继续
|
|
57
66
|
|
|
58
|
-
保留脚本输出的 `Artifact` 路径,用该候选 Gem 重试发布;更换 RubyGems 工具版本或重新构建可能得到不同字节,同一个版本不得覆盖已有内容。也可以直接指定从
|
|
67
|
+
保留脚本输出的 `Artifact` 路径,用该候选 Gem 重试发布;更换 RubyGems 工具版本或重新构建可能得到不同字节,同一个版本不得覆盖已有内容。也可以直接指定从
|
|
68
|
+
CI 或 Release 下载的原包:
|
|
59
69
|
|
|
60
70
|
```sh
|
|
61
71
|
ruby script/release.rb --rubygems-only --artifact tmp/ci/expect-pty-0.3.3.gem
|
|
62
72
|
```
|
|
63
73
|
|
|
64
|
-
将示例路径替换为实际输出的 `Artifact` 路径。`--artifact` 会跳过构建和测试,但仍核对包与当前源码是否一致;需要同时恢复
|
|
74
|
+
将示例路径替换为实际输出的 `Artifact` 路径。`--artifact` 会跳过构建和测试,但仍核对包与当前源码是否一致;需要同时恢复
|
|
75
|
+
GitHub Release 时去掉 `--rubygems-only`。
|
|
65
76
|
|
|
66
|
-
工作流失败时优先使用 Re-run failed jobs,继续使用本次 CI 保存的产物。需要在本地恢复时,检出发布标签对应的干净源码,下载该
|
|
77
|
+
工作流失败时优先使用 Re-run failed jobs,继续使用本次 CI 保存的产物。需要在本地恢复时,检出发布标签对应的干净源码,下载该
|
|
78
|
+
Release 的 Gem,再通过 `--artifact` 指定它。
|
|
67
79
|
|
|
68
|
-
脚本会校验现有 RubyGems 版本和 GitHub Release 附件的 SHA256;一致时复用,不一致时中止。已有 GitHub Release
|
|
80
|
+
脚本会校验现有 RubyGems 版本和 GitHub Release 附件的 SHA256;一致时复用,不一致时中止。已有 GitHub Release
|
|
81
|
+
缺少附件时会补传,已有版本和附件不会被覆盖。
|
|
69
82
|
|
|
70
|
-
上传中断若留下 `starter` 状态的空附件,脚本会明确指出附件名称。先确认没有其他发布或上传在运行,再在 GitHub Release
|
|
83
|
+
上传中断若留下 `starter` 状态的空附件,脚本会明确指出附件名称。先确认没有其他发布或上传在运行,再在 GitHub Release
|
|
84
|
+
中删除该失败附件,使用原包重试;脚本不会自动删除可能仍在上传的附件。
|
data/docs/VERIFICATION.md
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
1
1
|
# 验证记录
|
|
2
2
|
|
|
3
|
+
## v0.5.2 改进计划 T00–T07(2026-09-27,本地实施阶段)
|
|
4
|
+
|
|
5
|
+
以下是版本归档前的本地验证记录,对应改动随后归入 0.5.3。发布状态以 GitHub Release、对应 CI 和 RubyGems 为准。
|
|
6
|
+
|
|
7
|
+
基线为 `ac583532b5f4c946856e79ecd7350eb298019081`,开始时主工作区干净且 HEAD 等于本地 v0.5.2 标签。
|
|
8
|
+
本轮保留同步 IO、匹配优先级、默认 nil 期限、二进制缓冲及现有运行时依赖。版本仍为 0.5.2,变更记入 Unreleased;
|
|
9
|
+
没有提交、推送、创建标签、调用发布接口或登录真实 SSH。
|
|
10
|
+
|
|
11
|
+
T00 在 macOS 26.6.2 arm64、Ruby 4.0.6、Bundler 4.0.17、Minitest 5.27.0 上执行原始 `bundle exec rake`:
|
|
12
|
+
61 个文件 lint 通过,346 项 / 6,604 断言,退出码 0,无失败、错误或跳过。
|
|
13
|
+
系统默认 Ruby 为 2.6.10,因此下列本机命令统一使用 Homebrew Ruby,不改全局配置:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
export PATH="/opt/homebrew/opt/ruby/bin:$PATH"
|
|
17
|
+
export BUNDLER_VERSION=4.0.17
|
|
18
|
+
bundle exec rake
|
|
19
|
+
bundle exec rake test TESTOPTS='--seed=20260927'
|
|
20
|
+
bundle exec rake test TESTOPTS='--seed=1'
|
|
21
|
+
bundle exec rake test TESTOPTS='--seed=42'
|
|
22
|
+
script/ci
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
计划中的 `TESTOPTS='--seed 20260927'` 在锁定的 Rake 13.4.2 上实际失败:加载器把独立的数字参数当作文件。
|
|
26
|
+
同一 `rake test` 入口改用 `--seed=...` 后,输出确认采用指定 seed;未修改加载器、依赖或测试门槛。
|
|
27
|
+
|
|
28
|
+
### 按任务核对红灯与绿灯
|
|
29
|
+
|
|
30
|
+
从基线建立独立 detached worktree `tmp/improvement-052/baseline`,只复制对应的新回归测试,逐文件重放旧行为。
|
|
31
|
+
下表红灯为该基线的真实失败,绿灯为修复后同一测试文件的独立运行;全部使用 seed 20260927。
|
|
32
|
+
|
|
33
|
+
| 任务 | 修改与回归文件 | 旧版失败证据 | 修复后结果(测试 / 断言) |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| T01 / F01 | `lib/expect.rb`、`session_resources.rb`、`test/initialization_failure_test.rb` | 6 项失败:PTY 未关闭、open 用 NoMethodError 覆盖原中断 | 6 / 39,通过 |
|
|
36
|
+
| T02 / F02 | 上述生命周期文件、`test/process_interruption_test.rb` | 3 项失败、4 项错误:EINTR 提前终止 wait/close,finalizer 未 detach,归属变化后仍 detach | 12 / 56,通过 |
|
|
37
|
+
| T03 / F03 | `lib/expect.rb`、`interaction.rb`、`relay.rb`、`test/relay_reentrancy_test.rb` | 5 项失败:`abc` 变为 `abcabc`、重叠来源未拒绝、准备异常丢失缓冲 | 8 / 47,通过 |
|
|
38
|
+
| T04 / F04 | `script/release.rb`、发布 workflow、`test/release_test.rb` | 2 项失败:中文说明在 US-ASCII 下无法解析,非法 UTF-8 缺少明确诊断 | 21 / 124,通过(含原有发布拒绝条件) |
|
|
39
|
+
| T05 / C01–C02 | `lib/expect.rb`、`test/write_contract_test.rb` | 2 项失败:零计数依赖 watchdog 才退出,超出当前 chunk 的计数未拒绝 | 6 / 39,通过 |
|
|
40
|
+
| T06 | `test/ownership_sequence_test.rb` | 组合回归,非新增独立缺陷 | 6 / 161,通过 |
|
|
41
|
+
| T07 | README、内部契约、CHANGELOG、本记录 | 保留所有原测试及原有 CI 门禁 | 见下方完整验证 |
|
|
42
|
+
|
|
43
|
+
F01 使用真实 IO/PTY 并注入构造与 close 故障;F02 包含真实孩子的 waitpid 中断、真实 fork 非创建者检查,
|
|
44
|
+
连续中断和 GC 路径另外使用受控时钟与全部系统调用替身。假 PID 不进入真实信号或回收调用。
|
|
45
|
+
F03 使用公开 API、真实管道和自定义 writer 自然复现;F04 使用真实 CLI 子进程和中文文件。
|
|
46
|
+
C02 是非法 IO 适配返回值注入,不能推断正常 Ruby IO 会返回这些值。C01 的持续成功短写特征在旧版及新版均通过。
|
|
47
|
+
|
|
48
|
+
T06 的字节与多目标序列使用固定 seeds 20260927、1、42,核对各目标的完整内容、已确认计数、未处理缓冲、
|
|
49
|
+
转义消费、日志唯一性及原目标续发。真实 pipe EOF 与仍活跃孩子组合验证 IO/PID 相互独立,soft_close 只发 TERM,
|
|
50
|
+
hard_close 后读取真实状态并确认 ECHILD。测试时钟与系统调用替身在 ensure 中恢复,watchdog 仅用于测试防挂起。
|
|
51
|
+
|
|
52
|
+
### 完整验证与发布预演
|
|
53
|
+
|
|
54
|
+
新增 40 项测试,最终本机 `bundle exec rake`:66 个文件 lint 通过,**386 项 / 6,965 断言**,退出码 0。
|
|
55
|
+
三个固定 seed 的默认套件也各为 386 / 6,965,退出码均为 0,无失败、错误或跳过。
|
|
56
|
+
原有 cleanup/process/terminal_cleanup、relay/interact/diagnostics、io/timeout/deadline 定向组合也全部通过。
|
|
57
|
+
|
|
58
|
+
| 环境 | 完整 `script/ci` |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| macOS 26.6.2 arm64,Ruby 4.0.6 | 退出 0;386 / 6,965;66 文件 lint 通过 |
|
|
61
|
+
| Linux aarch64,Ruby 3.2.11,`ruby:3.2` | 退出 0;386 / 6,965;66 文件 lint 通过 |
|
|
62
|
+
| Linux aarch64,Ruby 3.3.12,`ruby:3.3` | 退出 0;386 / 6,965;66 文件 lint 通过 |
|
|
63
|
+
| Linux aarch64,Ruby 3.4.10,`ruby:3.4-slim` | 退出 0;386 / 6,965;66 文件 lint 通过 |
|
|
64
|
+
| Linux aarch64,Ruby 4.0.6,`ruby:4.0` | 退出 0;386 / 6,965;66 文件 lint 通过 |
|
|
65
|
+
|
|
66
|
+
Linux 验证以只读源目录挂载到临时容器,复制到独立 `/work` 后,使用同一 Gemfile.lock、Bundler 4.0.17 与
|
|
67
|
+
`BUNDLE_FROZEN=true` 执行原始 `script/ci`。3.4 slim 的 git 和构建工具仅安装在该临时容器。
|
|
68
|
+
完整脚本包含 dialogue 示例、matching/relay/send_slow/scaling/redactor 五组 smoke、Gem 构建、
|
|
69
|
+
普通 RubyGems 与最小 Bundler 消费方的隔离 PTY/核心契约/运行时依赖检查;所有这些步骤保留并执行。
|
|
70
|
+
|
|
71
|
+
在添加本轮 Unreleased 条目前,用本地完整源码构建的同一个 Gem 实际执行:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
LC_ALL=C LANG=C ruby -EUS-ASCII script/release.rb --dry-run --artifact tmp/improvement-052/expect-pty-0.5.2.gem
|
|
75
|
+
LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 ruby -EUTF-8 script/release.rb --dry-run --artifact tmp/improvement-052/expect-pty-0.5.2.gem
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
两次退出 0、验证相同 Gem 字节,生成的中文 release-notes 逐字节相同。该候选只是中途本地验证物,不是可发布版本。
|
|
79
|
+
新增 CLI 测试另覆盖独立 fixture 项目的非法 UTF-8 拒绝,以及 C/UTF-8 两种外部编码;不会访问发布服务。
|
|
80
|
+
当前 Unreleased 保留本轮变更,正式发布前仍需维护者自行归档并选择版本,原有拒绝条件不绕过。
|
|
81
|
+
最后对当前工作区再次执行 C locale dry-run,按预期退出 1 并提示 `Move Unreleased changes into the versioned changelog before releasing`。
|
|
82
|
+
|
|
83
|
+
### 同类检查、兼容性与剩余范围
|
|
84
|
+
|
|
85
|
+
- 构造所有权:核对 new/open/spawn/stty 四条入口。new/open 补账本之前的兜底;spawn 继续委托 new,stty 保留已有独立兜底。
|
|
86
|
+
- 进程回收:核对主会话、finalizer、stty 正常等待与异常回收。主会话/GC 修复 EINTR;stty 继续使用自身阶段策略,不合并账本。
|
|
87
|
+
- 写入进度:核对直接 write、RelayWriter、logging.emit 三处。后两者已有计数验证,仅补直接 write。
|
|
88
|
+
- 发布文本:本地入口与 workflow 共用 UTF-8 读取,notes 显式 UTF-8 写出;ASCII 校验和与 Git/Gem 二进制比较保持原状。
|
|
89
|
+
- 内部命名:统一为 `cleanup(failed:)`、`close_resources`、`close_child`、`mark_eof`;库、测试和文档无旧私有入口调用或别名。
|
|
90
|
+
- 兼容性:同源活跃 Relay 现在明确报 `ReentrancyError`,异常适配计数报 IOError;普通顺序恢复、嵌套 Matcher、匹配顺序和期限默认值不变。
|
|
91
|
+
|
|
92
|
+
G1–G6 由上述红绿回归与实际 locale dry-run 覆盖;G7–G8 由默认套件及原始 CI 脚本覆盖;G9–G10 由契约回归、
|
|
93
|
+
聚焦 diff 和本记录覆盖。日志位于被忽略的 `tmp/improvement-052/`,含基线、逐项红绿、固定 seed、容器和 dry-run 输出。
|
|
94
|
+
测试迭代中修正了测试 helper 与 Minitest 同名方法/嵌套 stub 的冲突,以及 CLI fixture 必须随脚本根目录复制的问题;
|
|
95
|
+
这些测试支架错误不计入产品红灯证据,表中证据来自修正后的独立基线重放。
|
|
96
|
+
|
|
97
|
+
未执行 macOS Ruby 3.2/3.3/3.4、远端 GitHub Actions 的 ubuntu-24.04/macos-15 矩阵、真实 SSH 或发布。
|
|
98
|
+
本地 Linux arm64 容器不能替代该远端矩阵。T08 的长期压力、高水位及性能对照按计划留作后续测量;
|
|
99
|
+
本轮 smoke 不证明吞吐提升、峰值内存下降或任意系统故障下保证回收成功。
|
|
100
|
+
|
|
3
101
|
## 0.5.0 公共字节过滤器发布前复核(2026-09-27)
|
|
4
102
|
|
|
5
103
|
基线为已发布的 `5a219f7`(0.4.0)。本轮公开 `Expect::Redactor`,新增 11 项回归,覆盖独立加载不引入 PTY、完整文本与流尾部策略、跨分片与重叠秘密、自定义替换标记、空规则、输入复制、无效更新原子性及安全摘要;现有会话日志与诊断继续复用该过滤器。
|
data/examples/kibitz/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# 本地 Kibitz 示例与测试
|
|
2
2
|
|
|
3
|
-
参考 [Expect.pm 的 kibitz](https://github.com/jacoby/expect.pm/tree/master/examples/kibitz),使用本项目的 `Expect.open`
|
|
3
|
+
参考 [Expect.pm 的 kibitz](https://github.com/jacoby/expect.pm/tree/master/examples/kibitz),使用本项目的 `Expect.open`
|
|
4
|
+
块、`listeners=`、`on_sequence` 和 `Expect.interconnect` 连接两端输入与同一个交互进程。参考源码为上游提交
|
|
5
|
+
`2ea0e4ce20a896c95cb4c94e781f1b1f3145150d`。
|
|
4
6
|
|
|
5
7
|
需要 Ruby 3.2+ 和 macOS/Linux。示例和自动脚本只依赖标准库,不需要 SSH 服务或密码。
|
|
6
8
|
|
|
@@ -12,7 +14,8 @@
|
|
|
12
14
|
ruby examples/kibitz/kibitz.rb -- /bin/sh -i
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
程序打印一条完整的 `--join /tmp/expect-kibitz-.../peer.sock` 命令。在本机、同一账户的第二个终端执行这条命令,两端即可操作同一个
|
|
17
|
+
程序打印一条完整的 `--join /tmp/expect-kibitz-.../peer.sock` 命令。在本机、同一账户的第二个终端执行这条命令,两端即可操作同一个
|
|
18
|
+
shell。输入和输出在两端都可见,例如:
|
|
16
19
|
|
|
17
20
|
```sh
|
|
18
21
|
# 第一个终端设置 shell 变量
|
|
@@ -21,7 +24,8 @@ shared_value=42
|
|
|
21
24
|
printf '%s\n' "$shared_value"
|
|
22
25
|
```
|
|
23
26
|
|
|
24
|
-
任一端输入 `exit` 结束共享 shell;Ctrl-C 发给共享进程的前台任务。任一端按 Ctrl-] 结束自己的转接并断开连接,另一端随 EOF
|
|
27
|
+
任一端输入 `exit` 结束共享 shell;Ctrl-C 发给共享进程的前台任务。任一端按 Ctrl-] 结束自己的转接并断开连接,另一端随 EOF
|
|
28
|
+
结束。终端配置在退出时恢复。可以通过 `-- program args...` 运行其他交互程序;不传程序时使用 `$SHELL`,缺省 `/bin/sh`。
|
|
25
29
|
|
|
26
30
|
## 无子进程互传和可选参数
|
|
27
31
|
|
|
@@ -39,9 +43,11 @@ ruby examples/kibitz/kibitz.rb --noescape -- /bin/sh -i
|
|
|
39
43
|
ruby examples/kibitz/kibitz.rb --timeout 30 --log /tmp/my-kibitz-session.log -- /bin/sh -i
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
共享进程模式的日志记录该进程实际返回的字节,包括终端回显;无子进程模式记录对端返回的字节。日志使用 0600
|
|
46
|
+
共享进程模式的日志记录该进程实际返回的字节,包括终端回显;无子进程模式记录对端返回的字节。日志使用 0600
|
|
47
|
+
权限。进程正常结束时,发起端返回该进程的退出码;转义或对端 EOF 返回 0,超时或连接失败返回非零。加入端只能观察连接结束,不取得共享进程的退出码。
|
|
43
48
|
|
|
44
|
-
这是同账户本地示例:连接使用 0700 临时目录内的 0600 Unix socket,接受一位加入者后移除 socket,退出时清理目录。原例的用户邀请、跨主机
|
|
49
|
+
这是同账户本地示例:连接使用 0700 临时目录内的 0600 Unix socket,接受一位加入者后移除 socket,退出时清理目录。原例的用户邀请、跨主机
|
|
50
|
+
rlogin、FIFO 以及转义菜单没有移植;双方终端初始尺寸由发起端决定,未实现运行期间的窗口尺寸同步。
|
|
45
51
|
|
|
46
52
|
## 自动运行同样的两个终端流程
|
|
47
53
|
|
|
@@ -51,21 +57,23 @@ ruby examples/kibitz/test_kibitz.rb
|
|
|
51
57
|
bundle exec rake test:kibitz
|
|
52
58
|
```
|
|
53
59
|
|
|
54
|
-
脚本真正启动两份 `kibitz.rb`,通过两个独立 PTY 模拟两位用户,并执行打印出的 Unix socket 加入流程。共享模式运行真实
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
脚本真正启动两份 `kibitz.rb`,通过两个独立 PTY 模拟两位用户,并执行打印出的 Unix socket 加入流程。共享模式运行真实
|
|
61
|
+
`/bin/sh -i`,没有 mock 转发逻辑。
|
|
62
|
+
|
|
63
|
+
| 场景 | 检查 |
|
|
64
|
+
|-------------------|-------------------------------------------------------------------------|
|
|
65
|
+
| 共享 shell | 输入回显、双方命令、共享变量、两端输出、中文、stderr、失败后继续执行 |
|
|
66
|
+
| Ctrl-C 与进程 EOF | 第二端中断第一端启动的前台任务,最终输出到达两端,进程退出 |
|
|
67
|
+
| `--noproc` | 双向字节互传,发送端无本地回显 |
|
|
68
|
+
| 自定义转义 | `STOP` 分两次发送,前缀不泄漏,转义和尾部不进入对端 |
|
|
69
|
+
| 加入端转义 | Ctrl-] 关闭加入端,发起端响应 EOF |
|
|
70
|
+
| `--noescape` | Ctrl-] 字节被完整转发 |
|
|
71
|
+
| 非零退出 | 共享 shell 返回 7,发起程序也返回 7 |
|
|
72
|
+
| 超时 | 转接到期返回失败,另一端结束 |
|
|
73
|
+
| 每个场景 | 就绪时已关闭本地回显,退出后两端终端配置恢复,socket 清理、日志权限正确 |
|
|
74
|
+
|
|
75
|
+
每轮结果写入 `tmp/kibitz/` 的独立目录,可用 `EXPECT_LOG_DIR` 指定其他目录。`report.json` 包含各场景检查项与整体 `passed`
|
|
76
|
+
,各场景子目录保留 `session.log`。只有全部检查通过才返回 0。默认测试集还覆盖没有第二端加入时的超时清理:
|
|
69
77
|
|
|
70
78
|
```sh
|
|
71
79
|
bundle exec ruby -Itest test/kibitz_test.rb
|
data/examples/kibitz/kibitz.rb
CHANGED
data/lib/expect/interaction.rb
CHANGED
|
@@ -248,6 +248,8 @@ class Expect
|
|
|
248
248
|
attr_accessor :sequences
|
|
249
249
|
# 让同步写入的背压读取遵守当前转接的数据所有权,退出后恢复普通匹配缓冲。
|
|
250
250
|
attr_accessor :interaction_buffer
|
|
251
|
+
# 活跃 Relay 的所有权 token;只限制同源递归转接,不限制转义回调中的 Matcher。
|
|
252
|
+
attr_accessor :relay_owner
|
|
251
253
|
# 只有裁剪、替换和消费才改变代次;同一代次只会追加,供字面扫描复用已排除的前缀。
|
|
252
254
|
attr_reader :buffer_generation
|
|
253
255
|
# 待交付游标随源会话保存,Relay 的超时或异常退出不会丢失各目标已经写出的进度。
|
data/lib/expect/relay.rb
CHANGED
|
@@ -4,21 +4,25 @@ class Expect
|
|
|
4
4
|
# 一次转接的共同读写循环;发送游标留在源会话中,调用结束后仍可继续。
|
|
5
5
|
# 本对象只暂借未处理输入;读缓冲、发送进度与转义回调分开保存,避免重入时重复交付。
|
|
6
6
|
class Relay
|
|
7
|
-
|
|
7
|
+
OWNERSHIP_MUTEX = Mutex.new
|
|
8
|
+
private_constant :OWNERSHIP_MUTEX
|
|
9
|
+
|
|
10
|
+
# 这里只校验参数;run 取得所有来源后才转移缓冲,不关闭或接管任何外部 IO。
|
|
8
11
|
def initialize(sessions, timeout)
|
|
9
12
|
@sessions = sessions.uniq
|
|
10
13
|
@active = @sessions.dup
|
|
11
14
|
period = Expect.duration(timeout)
|
|
12
15
|
@deadline = period && (Expect.monotonic + period)
|
|
13
|
-
@
|
|
14
|
-
@
|
|
16
|
+
@token = Object.new
|
|
17
|
+
@buffers = {}
|
|
18
|
+
@previous = {}
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
# 每轮先处理转义和已排队输出,再共同选择读写;停止返回来源会话,总期限到达返回 nil。
|
|
18
22
|
# 真实 IO 的写入由非阻塞游标推进,用户日志和回调仍同步执行,须由调用方保证及时返回。
|
|
19
23
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 共享读写循环统一维护来源和目标期限。
|
|
20
24
|
def run
|
|
21
|
-
|
|
25
|
+
prepare_sources
|
|
22
26
|
outputs.each(&:restart_timeout)
|
|
23
27
|
polled = false
|
|
24
28
|
loop do
|
|
@@ -89,15 +93,41 @@ class Expect
|
|
|
89
93
|
end
|
|
90
94
|
end
|
|
91
95
|
ensure
|
|
92
|
-
|
|
93
|
-
@previous.each { |session, buffer| session.__send__(:interaction_buffer=, buffer) }
|
|
94
|
-
@buffers.each { |session, buffer| session.__send__(:restore_relay_buffer, buffer) }
|
|
96
|
+
restore_sources
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
98
100
|
|
|
99
101
|
private
|
|
100
102
|
|
|
103
|
+
# 锁仅保护标记检查/登记,不跨 IO 或用户回调;任一来源被占用时整组拒绝,不动缓冲和期限。
|
|
104
|
+
def prepare_sources
|
|
105
|
+
OWNERSHIP_MUTEX.synchronize do
|
|
106
|
+
if @sessions.any? { |session| session.__send__(:relay_owner) }
|
|
107
|
+
raise ReentrancyError, "source session already has an active relay"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@sessions.each { |session| session.__send__(:relay_owner=, @token) }
|
|
111
|
+
end
|
|
112
|
+
@sessions.each do |session|
|
|
113
|
+
@previous[session] = session.__send__(:interaction_buffer)
|
|
114
|
+
@buffers[session] = session.clear_buffer
|
|
115
|
+
session.__send__(:interaction_buffer=, @buffers.fetch(session))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 未处理输入还给会话,已排队字节仍归游标;构造中断也只恢复已转移的缓冲及自己的标记。
|
|
120
|
+
def restore_sources
|
|
121
|
+
@previous.each { |session, buffer| session.__send__(:interaction_buffer=, buffer) }
|
|
122
|
+
@buffers.each { |session, buffer| session.__send__(:restore_relay_buffer, buffer) }
|
|
123
|
+
ensure
|
|
124
|
+
OWNERSHIP_MUTEX.synchronize do
|
|
125
|
+
@sessions.each do |session|
|
|
126
|
+
session.__send__(:relay_owner=, nil) if session.__send__(:relay_owner).equal?(@token)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
101
131
|
# 只收集尚未完成的目标,供共同 select 以及最早写入期限计算使用。
|
|
102
132
|
def outputs = @sessions.flat_map { |session| session.__send__(:relay_outputs) }.reject(&:done?)
|
|
103
133
|
|
|
@@ -20,11 +20,14 @@ class Expect
|
|
|
20
20
|
# 只关闭由本库拥有的 IO;借用的 reader、writer 由调用方管理。
|
|
21
21
|
# 常规关闭错误延后到所有句柄尝试完再抛出,失败句柄仍留在账本内供下一次关闭重试。
|
|
22
22
|
def close_handles
|
|
23
|
-
|
|
23
|
+
self.class.close_handles(reader, writer, slave) if own
|
|
24
|
+
end
|
|
24
25
|
|
|
26
|
+
# 账本尚未建立时也能释放局部 IO;不依赖完整会话,也不调用无效参数上的用户方法。
|
|
27
|
+
def self.close_handles(*handles)
|
|
25
28
|
# 初始化校验失败时可能含无效参数,只关闭真实 IO;PTY 读写端也需要去重。
|
|
26
29
|
failure = nil
|
|
27
|
-
|
|
30
|
+
handles.grep(IO).uniq(&:object_id).each do |io|
|
|
28
31
|
io.close unless io.closed?
|
|
29
32
|
rescue IOError, SystemCallError => error
|
|
30
33
|
failure ||= error
|
|
@@ -59,18 +62,7 @@ class Expect
|
|
|
59
62
|
owned_log.close if owned_log && !owned_log.closed?
|
|
60
63
|
ensure
|
|
61
64
|
# 每个阶段独立收尾;句柄或日志关闭失败不能跳过进程回收。
|
|
62
|
-
|
|
63
|
-
if pid
|
|
64
|
-
begin
|
|
65
|
-
Process.kill("KILL", pid)
|
|
66
|
-
rescue Errno::ESRCH
|
|
67
|
-
# 子进程可能刚好退出,仍需尝试 wait,不能留下僵尸进程。
|
|
68
|
-
nil
|
|
69
|
-
end
|
|
70
|
-
# 将最终 wait 交给后台回收线程,避免在 GC 终结器中阻塞等待。
|
|
71
|
-
Process.detach(pid)
|
|
72
|
-
@pid = nil
|
|
73
|
-
end
|
|
65
|
+
finalize_child
|
|
74
66
|
end
|
|
75
67
|
end
|
|
76
68
|
rescue IOError, SystemCallError
|
|
@@ -81,5 +73,38 @@ class Expect
|
|
|
81
73
|
def self.finalizer(resources)
|
|
82
74
|
proc { resources.finalize }
|
|
83
75
|
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# GC 只做一次非阻塞回收和最多两次信号尝试;失败也把等待交给 detach,不运行用户回调。
|
|
80
|
+
def finalize_child
|
|
81
|
+
begin
|
|
82
|
+
reap
|
|
83
|
+
rescue Errno::EINTR
|
|
84
|
+
# GC 不等待下轮轮询,仍对本进程拥有的子进程执行后续有限清理。
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
return unless pid && owner == Process.pid
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
2.times do
|
|
91
|
+
break unless pid && owner == Process.pid
|
|
92
|
+
|
|
93
|
+
Process.kill("KILL", pid)
|
|
94
|
+
break
|
|
95
|
+
rescue Errno::EINTR
|
|
96
|
+
next
|
|
97
|
+
rescue Errno::ESRCH
|
|
98
|
+
break
|
|
99
|
+
end
|
|
100
|
+
ensure
|
|
101
|
+
if pid && owner == Process.pid
|
|
102
|
+
Process.detach(pid)
|
|
103
|
+
@pid = nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
rescue Errno::ECHILD
|
|
107
|
+
@pid = nil
|
|
108
|
+
end
|
|
84
109
|
end
|
|
85
110
|
end
|
data/lib/expect/terminal.rb
CHANGED
data/lib/expect/version.rb
CHANGED