expect-pty 0.3.2 → 0.3.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/.rubocop.yml +18 -10
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +3 -1
- data/README.md +11 -5
- data/docs/COMPATIBILITY.md +1 -1
- data/docs/INTERNAL_CONTRACTS.md +21 -0
- data/docs/PERFORMANCE.md +16 -0
- data/docs/RELEASING.md +8 -6
- data/docs/VERIFICATION.md +16 -0
- data/lib/expect/interaction.rb +3 -3
- data/lib/expect/logging.rb +96 -0
- data/lib/expect/matcher.rb +11 -2
- data/lib/expect/pattern.rb +6 -2
- data/lib/expect/session_resources.rb +2 -2
- data/lib/expect/terminal.rb +35 -0
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +35 -129
- data/script/release.rb +37 -5
- data/test/cleanup_test.rb +88 -0
- data/test/pattern_offset_test.rb +3 -1
- data/test/process_test.rb +46 -0
- data/test/release_test.rb +94 -16
- data/test/timeout_test.rb +119 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e21e96f8c4a79123cb7eb74606f0b0a8195c855290ba5ca97b43419917b1e36
|
|
4
|
+
data.tar.gz: 5f77fab495dd5d5b10e91e14a6957d3907c261cd9931dd73d1113827c1eabc74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6334c31be027acef464d2d648dec242e43eff2367076a149c64a64a88edd27da2d51a65f3e85afcf2e8d15b2c41439c496b75731cc26db8e816d1fd3296076b
|
|
7
|
+
data.tar.gz: 1437a5c147513aa572b8b0430d8f25f0ed526ba6b14309f261f0ef30e1f0fa7e2b4850c4abd8a6a35f0988307f204870f31961c26db77245a6d35f986e34babe
|
data/.rubocop.yml
CHANGED
|
@@ -7,29 +7,37 @@ AllCops:
|
|
|
7
7
|
- "tmp/**/*"
|
|
8
8
|
- "vendor/**/*"
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# carry local exemptions at the method that needs them.
|
|
10
|
+
# 宽松但有限的规模门槛覆盖生产代码;测试和基准的场景编排不按方法长度及 ABC 计数。
|
|
11
|
+
# 需要更高分支复杂度的协议状态机仅在对应方法说明豁免原因。
|
|
13
12
|
Metrics/AbcSize:
|
|
14
|
-
|
|
13
|
+
Max: 120
|
|
14
|
+
Exclude:
|
|
15
|
+
- "test/**/*"
|
|
16
|
+
- "benchmark/**/*"
|
|
15
17
|
|
|
16
18
|
Metrics/BlockLength:
|
|
17
|
-
|
|
19
|
+
Max: 80
|
|
18
20
|
|
|
19
21
|
Metrics/BlockNesting:
|
|
20
|
-
|
|
22
|
+
Max: 4
|
|
23
|
+
Exclude:
|
|
24
|
+
- "test/**/*"
|
|
21
25
|
|
|
22
26
|
Metrics/ClassLength:
|
|
23
|
-
|
|
27
|
+
Max: 500
|
|
24
28
|
|
|
25
29
|
Metrics/MethodLength:
|
|
26
|
-
|
|
30
|
+
Max: 80
|
|
31
|
+
Exclude:
|
|
32
|
+
- "test/**/*"
|
|
33
|
+
- "benchmark/**/*"
|
|
27
34
|
|
|
28
35
|
Metrics/ModuleLength:
|
|
29
|
-
|
|
36
|
+
Max: 250
|
|
30
37
|
|
|
31
38
|
Metrics/ParameterLists:
|
|
32
|
-
|
|
39
|
+
Max: 6
|
|
40
|
+
CountKeywordArgs: false
|
|
33
41
|
|
|
34
42
|
Metrics/CyclomaticComplexity:
|
|
35
43
|
Max: 13
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.3 - 2026-09-27
|
|
6
|
+
|
|
7
|
+
- **生命周期**:构造及工厂启动失败时统一释放所属 IO 并回收子进程;块或软关闭的原始异常不再被后续 IO 清理错误覆盖,失败的单个句柄不会阻止其他资源收尾。
|
|
8
|
+
- **匹配期限**:EOF 回调保留原期限继续时,不再于期限过后消费其他来源的文本匹配;已知 EOF 仍依次派发,所有来源结束时保持 EOF 结果。
|
|
9
|
+
- **正则性能**:只在需要调整编码时复制输入缓冲,减少大缓冲区多模式扫描的耗时和分配,保持编码及字节偏移语义。
|
|
10
|
+
- **终端错误**:系统缺少 `stty` 时提供明确的 IO 错误和安装提示,并保留原始异常。
|
|
11
|
+
- **发布校验**:拒绝发布与目标 Git 提交的内容、执行权限或包元数据不一致的 Gem,提交前仍可通过 dry-run 验证候选包。
|
|
12
|
+
|
|
5
13
|
## 0.3.2 - 2026-09-26
|
|
6
14
|
|
|
7
15
|
- **匹配与转接**:正则直接使用字节偏移,重复来源在单轮扫描中共享缓冲快照;转义正则复用本轮组合文本,保留回调修改规则后的重新扫描语义。`send_slow` 只读取已经可读的回复。
|
data/CONTRIBUTING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 参与贡献
|
|
2
2
|
|
|
3
|
-
欢迎中文或英文 issue 和 PR
|
|
3
|
+
欢迎中文或英文 issue 和 PR。库的公共 API 名称和错误信息使用英文;库内的责任划分、复杂生命周期和协议解释使用中文,RuboCop 等工具指令保持原格式。文档以现有文件的语言为准,示例中的可编辑命令保持直接清晰。
|
|
4
4
|
|
|
5
5
|
## 本地检查
|
|
6
6
|
|
|
@@ -14,6 +14,8 @@ script/ci # 另含示例、Gem 构建及隔离安装验证
|
|
|
14
14
|
|
|
15
15
|
修改转接或匹配时,先运行对应的 `test/*_test.rb`,再运行 `bundle exec rake`。默认测试不需要 SSH;真实 SSH 集成测试的环境要求见 [测试说明](test/integration/README.md)。发布操作和历史验证分别见 [发布说明](docs/RELEASING.md) 与 [验证记录](docs/VERIFICATION.md)。
|
|
16
16
|
|
|
17
|
+
RuboCop 对生产代码保留有限的长度、ABC、嵌套、参数数量和分支复杂度门槛。测试及基准的场景编排豁免方法长度与 ABC 计数;协议状态机的分支豁免只放在对应方法,并注明原因。阈值用于提示复核,职责拆分仍应保持状态归属和异常恢复逻辑完整。
|
|
18
|
+
|
|
17
19
|
## 提交问题或改动
|
|
18
20
|
|
|
19
21
|
问题报告请附 Ruby 版本、操作系统、最小复现代码、预期与实际结果,以及必要的异常信息;日志中请删去凭据和会话敏感内容。修复行为缺陷时,先加入能在旧实现复现问题的回归测试。涉及缓冲、转义、背压或进程清理的改动,请对照 [内部状态与数据归属](docs/INTERNAL_CONTRACTS.md),说明超时、EOF 和失败后的恢复行为。
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
项目和仓库名为 `expect-ruby`,Gem 名为 `expect-pty`。在应用的 Gemfile 中添加以下内容,然后运行 `bundle install`:
|
|
14
14
|
|
|
15
15
|
```ruby
|
|
16
|
-
gem "expect-pty", "~> 0.3.
|
|
16
|
+
gem "expect-pty", "~> 0.3.3", require: "expect/pty"
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
也可直接执行 `gem install expect-pty`。需要跟随开发分支时,可从 GitHub 安装:
|
|
@@ -26,8 +26,8 @@ gem "expect-pty", git: "https://github.com/gatework/expect-ruby.git", branch: "m
|
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
28
|
mkdir -p tmp
|
|
29
|
-
gem build expect-pty.gemspec --output tmp/expect-pty-0.3.
|
|
30
|
-
gem install ./tmp/expect-pty-0.3.
|
|
29
|
+
gem build expect-pty.gemspec --output tmp/expect-pty-0.3.3.gem
|
|
30
|
+
gem install ./tmp/expect-pty-0.3.3.gem
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
```ruby
|
|
@@ -45,7 +45,7 @@ Expect.spawn("/bin/sh", "-i") do |shell|
|
|
|
45
45
|
end
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
块返回其执行结果,退出时关闭会话并回收子进程,异常和 `break`
|
|
48
|
+
块返回其执行结果,退出时关闭会话并回收子进程,异常和 `break` 也执行清理。构造或启动失败时同样释放已创建的资源;清理中的 IO 错误不会替换正在传播的原始异常。无块形式需用 `ensure` 显式调用 `close`。`Expect.new` 可以先创建 PTY、设置 `slave.echo` / `slave.winsize`,然后调用实例的 `spawn`。
|
|
49
49
|
|
|
50
50
|
多个命令参数原样传给 Ruby `exec`;单个命令字符串使用 Ruby 的 shell 语义。不可信参数应使用独立参数形式。支持 `env: { "NAME" => "value" }` 和 `chdir: "/path"`。同一会话只能启动一次,启动失败抛出 `Expect::SpawnError`。
|
|
51
51
|
|
|
@@ -71,6 +71,8 @@ end
|
|
|
71
71
|
|
|
72
72
|
`configure` 校验后发布冻结的配置对象;块异常不会发布部分修改。并发调用按顺序完成读改写,不会互相覆盖不同属性。配置块在锁内执行,应保持简短,不要在块内再次调用 `configure`(会抛出 `ThreadError`)或等待其他配置线程。每个会话独立持有配置,优先使用构造参数;修改默认值不会改变已有会话,修改一个会话也不会影响其他会话。子类继承父类默认配置,可独立覆盖。
|
|
73
73
|
|
|
74
|
+
全局默认配置通常在应用启动时设置;每次会话的动态差异使用构造参数或会话属性,避免在高频路径反复获取共享配置锁。
|
|
75
|
+
|
|
74
76
|
| 属性 | 默认值 | 行为 |
|
|
75
77
|
| --- | --- | --- |
|
|
76
78
|
| `timeout` | `nil` | 等待匹配的默认超时,秒;`nil` 无限,`0` 非阻塞轮询 |
|
|
@@ -143,7 +145,7 @@ end
|
|
|
143
145
|
|
|
144
146
|
回调通过闭包访问局部变量。无参数声明块在模式构建器中执行;希望保留调用方 `self` 时使用 `do |patterns|`,调用 `patterns.on(...)`。所有模式注册完成后才读取 IO;注册异常或 `break` 不消费输入。块和位置模式不能混用。`expect_result` 支持同样的声明方式。
|
|
145
147
|
|
|
146
|
-
`continue` 继续等待并重新计时;`continue(reset_timeout: false)`
|
|
148
|
+
`continue` 继续等待并重新计时;`continue(reset_timeout: false)` 保留原期限,类和实例均可调用。回调返回后若保留的期限已过,不再扫描新的文本匹配,未消费的输入留给下一次等待。无回调或返回其他值时结束本次匹配。超时回调只有返回重置计时的 `continue` 才再次等待。EOF 回调继续时移除该源并等待其余会话;已知的 EOF 仍依次派发,全部 EOF 时直接返回,期限已过时仅对剩余活跃源触发超时。
|
|
147
149
|
|
|
148
150
|
`eof` / `timeout` 声明会占用模式序号,但事件返回的 `number` 为 `nil`。一个等待只能注册一个超时回调;它接收**所有仍在监听的会话**。不需要回调时,可将 `:eof` / `:timeout` 作为位置事件参数。
|
|
149
151
|
|
|
@@ -193,6 +195,8 @@ ready = Expect.readable_sessions(first, second, timeout: 5)
|
|
|
193
195
|
|
|
194
196
|
大块写入遇到背压时同时读取输出,避免双向传输互相阻塞。超过 `write_timeout` 抛出 `Expect::WriteTimeout`,`error.bytes_written` 给出本次 `write` 已被底层接受的字节数;这些字节不回滚,不要从头重发整个命令。写入、等待和背压读取中的 `EINTR` 均保留原期限重试。控制字符可直接发送,例如 `session.write("\x03")`,其信号作用取决于终端设置。`send`、`public_send`、`__send__` 保留 Ruby 反射语义。
|
|
195
197
|
|
|
198
|
+
`stty` 需要系统命令位于 `PATH`;缺失时抛出带安装提示的 `IOError`,原始 `Errno::ENOENT` 保留在 `cause`。窗口尺寸和人工接管的终端恢复使用 Ruby `io/console`。
|
|
199
|
+
|
|
196
200
|
## 日志与人工交互
|
|
197
201
|
|
|
198
202
|
```ruby
|
|
@@ -210,6 +214,8 @@ session.log_listeners = false
|
|
|
210
214
|
|
|
211
215
|
所有会话默认不输出到 stdout。日志仅记录实际读取的接收字节;写入不重复记录,终端回显可能作为接收内容返回。密码交互应关闭日志、调试,并确保被控程序不回显密码。
|
|
212
216
|
|
|
217
|
+
普通 `expect` 按顺序同步写入日志、stdout 和 `listeners`,这些目标须及时消费数据;匹配的 `timeout` 不会中断阻塞中的输出。需要在慢目标背压时继续处理其他输入,应使用下面的 `interconnect` 非阻塞转接接口并设置期限。
|
|
218
|
+
|
|
213
219
|
```ruby
|
|
214
220
|
session.interact(input: $stdin, escape: "\x1d", output: $stdout) # Ctrl-]
|
|
215
221
|
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
交互能力参考 [jacoby/expect.pm](https://github.com/jacoby/expect.pm),源码基准为版本 1.38、提交 `2ea0e4ce20a896c95cb4c94e781f1b1f3145150d`。此项目独立实现,使用 MIT 许可,没有复制 Perl 实现代码;原项目作者及维护者为 Austin Schutz、Roland Giersig、Dave Jacoby,采用与 Perl 相同的许可。
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
从 0.2.0 起采用 Ruby 原生接口。下表列出迁移关系,**旧方法、别名和参数语法已移除**;历史 0.1.1 安装包的接口见对应版本记录。
|
|
28
28
|
|
|
29
29
|
| 原接口或状态 | 当前 Ruby 接口 |
|
|
30
30
|
| --- | --- |
|
data/docs/INTERNAL_CONTRACTS.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
这些状态供 Matcher、Relay 和会话生命周期协作使用,不是公共接口。
|
|
4
4
|
|
|
5
|
+
## 模块职责
|
|
6
|
+
|
|
7
|
+
| 模块 | 职责 |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `expect.rb` | 会话入口、PTY 启动、缓冲、直接读写和关闭流程 |
|
|
10
|
+
| `configuration.rb` | 配置校验与可复制的默认快照 |
|
|
11
|
+
| `pattern.rb`、`pattern_list.rb`、`result.rb` | 字节定位、声明顺序及原生结果值 |
|
|
12
|
+
| `matcher.rb` | 一次等待的匹配、事件派发和期限 |
|
|
13
|
+
| `session_resources.rb` | IO、日志与直属子进程的所有权账本和 GC 兜底 |
|
|
14
|
+
| `logging.rb` | 日志目标、监听器及同步输出;不持有第二份资源所有权 |
|
|
15
|
+
| `terminal.rb` | `stty` 和窗口尺寸的会话终端接口 |
|
|
16
|
+
| `interaction.rb`、`relay.rb`、`relay_writer.rb` | 人工接管、转义处理、共同调度和各目标发送游标 |
|
|
17
|
+
|
|
18
|
+
这些能力文件延续 `interaction.rb` 的类重开方式,公共入口仍是 `expect/pty`。按职责组织方法,不引入新的继承层级,也不复制会话状态。
|
|
19
|
+
|
|
5
20
|
跨对象调用使用 `__send__` 访问受保护或私有方法。变更以下入口时,应同时检查调用方与失败恢复路径:
|
|
6
21
|
|
|
7
22
|
| 会话内部入口 | 使用方 | 不变量 |
|
|
@@ -22,14 +37,20 @@
|
|
|
22
37
|
|
|
23
38
|
匹配优先级始终是“声明组 → 会话 → 模式”,不是文本位置;多个会话包装同一 IO 时,就绪处理使用对象身份选择首个声明会话。回调前后用于判断缓冲是否变化的快照仍独立保留,不能换成可变内部字符串。零长度或 preserve_buffer 的继续匹配必须遵守 stalled 保护。
|
|
24
39
|
|
|
40
|
+
文本与 EOF 回调遵守相同的继续期限:`continue(reset_timeout: false)` 返回后先检查原期限,再开始下一轮文本匹配。EOF 继续已到期时仍依次派发已知 EOF,不读取新输入;超时仅通知仍活跃的来源,保留未消费缓冲。所有来源都已 EOF 时直接返回 EOF,不再制造一次超时事件。EOF 或超时回调重置期限后恢复普通匹配顺序。
|
|
41
|
+
|
|
25
42
|
转义扫描只在一轮内复用 `history + buffer`,只含字面规则时不构造它。回调继续后重新读取规则、历史和缓冲;不能跨回调保存文本快照。字面转义的潜在前缀暂存,完整前缀交付后才执行转义回调。
|
|
26
43
|
|
|
27
44
|
## 关闭与所有权
|
|
28
45
|
|
|
29
46
|
SessionResources 保存创建者 PID、所属句柄、直属子进程和所属日志。借用 IO 不关闭,fork 后的非创建者不发送信号或回收父进程的孩子。soft_close 最多发送 TERM 并保留未退出 PID;hard_close 可在有限等待后发送 KILL。
|
|
30
47
|
|
|
48
|
+
工厂和构造器在校验之前登记资源归属,失败时沿用同一清理流程;启动成功必须显式记录,不能用 PID 非空推断整个工厂调用已经成功。即使 exec 成功后诊断输出失败,也要立即回收未交付给调用方的子进程。
|
|
49
|
+
|
|
31
50
|
显式关闭遇到 IOError/SystemCallError 时,继续尝试其他句柄、交互包装器、日志和子进程清理,最后传播首个清理错误;已经在传播的其他异常保留。失败资源继续持有,后续关闭可重试。GC 终结器独立尝试句柄、日志、非阻塞回收,常规清理错误不向外传播。终结器不能强引用会话本身。
|
|
32
51
|
|
|
52
|
+
是否保留原始异常由当前构造、块或关闭作用域显式记录,不能直接读取调用者 rescue 中的 `$!`。`Interrupt` 和 `SystemExit` 同样先清理再传播;`break` / `throw` 不是异常,此时清理失败仍应抛出。
|
|
53
|
+
|
|
33
54
|
## 回归入口
|
|
34
55
|
|
|
35
56
|
| 不变量 | 测试 |
|
data/docs/PERFORMANCE.md
CHANGED
|
@@ -35,6 +35,22 @@ bundle exec ruby benchmark/matching.rb --samples 5 --output tmp/benchmark/after.
|
|
|
35
35
|
|
|
36
36
|
没有 Git 的源码归档或安装目录仍可运行;提交和工作区状态记为 `null`,保留源码 SHA-256。
|
|
37
37
|
|
|
38
|
+
## 正则输入复用验证(2026-09-27)
|
|
39
|
+
|
|
40
|
+
`Pattern#locate` 只在正则要求的编码与输入编码不同时复制输入;其他正则复用只读缓冲。Ruby 字符串可能共享底层存储,因此不能把减少一次 `dup` 直接解释为少复制整个缓冲的字节数;这里报告实际对象分配和墙钟样本。
|
|
41
|
+
|
|
42
|
+
macOS arm64、Ruby 4.0.6、Bundler 4.0.17,使用同一套 `benchmark/matching.rb` 和依赖,对修改前后的 `lib` 分别运行 `--samples 5 --iterations 100`。下表为五次样本的中位数,每项均校验匹配结果。此前用 30 次迭代进行的首轮比较方向一致。
|
|
43
|
+
|
|
44
|
+
| 场景 | 修改前耗时 | 修改后耗时 | 修改前分配 | 修改后分配 |
|
|
45
|
+
| --- | --- | --- | --- | --- |
|
|
46
|
+
| 4 KiB、32 个正则、全未命中 | 1.985 ms | 1.415 ms | 6,501 | 3,301 |
|
|
47
|
+
| 64 KiB、32 个正则、全未命中 | 17.068 ms | 11.070 ms | 6,501 | 3,301 |
|
|
48
|
+
| 1 MiB、32 个正则、全未命中 | 257.474 ms | 165.734 ms | 6,501 | 3,301 |
|
|
49
|
+
| 1 MiB、32 个正则、最后命中 | 265.848 ms | 172.001 ms | 7,301 | 4,101 |
|
|
50
|
+
| 1 MiB UTF-8 前缀、固定编码正则 | 78.348 ms | 74.721 ms | 1,201 | 1,201 |
|
|
51
|
+
|
|
52
|
+
固定 UTF-8 正则仍需要编码副本,分配量不变;约 4.6% 的耗时差不能视为稳定收益。单正则大缓冲场景也基本不变。上述结果适用于本次机器与工作负载,不设为 CI 性能门槛。
|
|
53
|
+
|
|
38
54
|
```sh
|
|
39
55
|
bundle exec ruby benchmark/matching.rb --smoke
|
|
40
56
|
bundle exec ruby benchmark/relay.rb --smoke
|
data/docs/RELEASING.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## 准备版本
|
|
6
6
|
|
|
7
|
-
1. 更新 `lib/expect/version.rb` 的 `Expect::VERSION`,例如 `0.3.
|
|
8
|
-
2. 把 `CHANGELOG.md` 的 `Unreleased` 内容移到对应版本标题下,例如 `## 0.3.
|
|
7
|
+
1. 更新 `lib/expect/version.rb` 的 `Expect::VERSION`,例如 `0.3.3`。
|
|
8
|
+
2. 把 `CHANGELOG.md` 的 `Unreleased` 内容移到对应版本标题下,例如 `## 0.3.3 - 2026-09-27`;可以保留空的 `Unreleased` 标题。
|
|
9
9
|
3. 提交源码,发布时工作区必须干净。若同时发布 GitHub Release,还需推送到 `main`,远端 `main` 必须包含该提交,已有同名标签必须指向该提交。
|
|
10
10
|
|
|
11
11
|
发布脚本只接受正式版 `X.Y.Z`;未归档的变更会阻止发布。
|
|
@@ -22,6 +22,8 @@ ruby script/release.rb --rubygems-only
|
|
|
22
22
|
|
|
23
23
|
默认执行 `script/ci` 的检查、完整测试、构建和隔离安装验证,构建包保存在 `tmp/ci/expect-pty-版本号.gem`。随后复制到 `tmp/release/版本号/candidate-*/` 的独占目录,核对包内文件并生成发布说明和校验文件。该副本贯穿后续发布,目录会保留供失败重试。正式发布前再次确认源码未变,上传 RubyGems 后下载远端包核对 SHA256。
|
|
24
24
|
|
|
25
|
+
正式发布逐个核对包内普通文件在目标 Git 提交中的存在性、原始字节和执行权限,同时核对 gemspec 的安装元数据及主页。`git status` 为空不能替代这一检查:忽略规则可能隐藏被 glob 收入包的本地文件,`assume-unchanged` 和 `core.filemode` 也可能隐藏差异。候选包必须与已提交源码一致。
|
|
26
|
+
|
|
25
27
|
`--dry-run` 只做本地验证,可在提交前使用。它仍要求版本号和发布说明完整。
|
|
26
28
|
|
|
27
29
|
## 同时发布 GitHub Release
|
|
@@ -40,9 +42,9 @@ ruby script/release.rb
|
|
|
40
42
|
GitHub Runner 不会继承本机的 Gem 登录状态。要在 Actions 发布 RubyGems,需在仓库的 Settings → Secrets and variables → Actions 中配置 `RUBYGEMS_API_KEY`,使用具有 `Push rubygem` 权限的发布 Key。
|
|
41
43
|
|
|
42
44
|
```sh
|
|
43
|
-
git tag -a v0.3.
|
|
44
|
-
git push origin v0.3.
|
|
45
|
-
gh workflow run release.yml --ref v0.3.
|
|
45
|
+
git tag -a v0.3.3 -m 'Release v0.3.3'
|
|
46
|
+
git push origin v0.3.3
|
|
47
|
+
gh workflow run release.yml --ref v0.3.3 --repo gatework/expect-ruby
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
也可以在 Actions → Release → Run workflow 选择对应版本标签。工作流仅支持手动触发,避免本地发布时出现第二次并发上传。
|
|
@@ -56,7 +58,7 @@ gh workflow run release.yml --ref v0.3.2 --repo gatework/expect-ruby
|
|
|
56
58
|
保留脚本输出的 `Artifact` 路径,用该候选 Gem 重试发布;更换 RubyGems 工具版本或重新构建可能得到不同字节,同一个版本不得覆盖已有内容。也可以直接指定从 CI 或 Release 下载的原包:
|
|
57
59
|
|
|
58
60
|
```sh
|
|
59
|
-
ruby script/release.rb --rubygems-only --artifact tmp/ci/expect-pty-0.3.
|
|
61
|
+
ruby script/release.rb --rubygems-only --artifact tmp/ci/expect-pty-0.3.3.gem
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
将示例路径替换为实际输出的 `Artifact` 路径。`--artifact` 会跳过构建和测试,但仍核对包与当前源码是否一致;需要同时恢复 GitHub Release 时去掉 `--rubygems-only`。
|
data/docs/VERIFICATION.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# 验证记录
|
|
2
2
|
|
|
3
|
+
## 模块审查、资源清理与正则输入复用(2026-09-27)
|
|
4
|
+
|
|
5
|
+
基线为 `b9fc077`,以下为 0.3.3 发布准备前的源码审查记录。覆盖会话、配置、模式声明与定位、匹配结果及期限、资源所有权、日志、终端、人工接管、转接恢复、示例、发布和打包模块。修复工厂/构造失败后的子进程回收、部分句柄清理、原始异常保留、EOF 继续期限和发布包提交来源验证;新行为均有先失败后通过的回归。EOF 补充多源同时结束和重置期限用例,保留已知 EOF 的派发顺序。
|
|
6
|
+
|
|
7
|
+
日志与终端方法拆入独立能力文件,公共入口保持 `expect/pty`;打包检查确认两文件随 Gem 分发。正则输入复用通过冻结输入、编码分片、二进制、字节偏移及匹配/转接回归,实测数据见 [性能基准](PERFORMANCE.md#正则输入复用验证2026-09-27)。
|
|
8
|
+
|
|
9
|
+
| 当前运行环境 | 完整 `bash script/ci` |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| macOS arm64,Ruby 4.0.6,Bundler 4.0.17,原工作区 | 269 项 / 1,356 断言,0 失败、错误、跳过;RuboCop 51 文件无违规 |
|
|
12
|
+
| macOS arm64,同一运行时,隔离 worktree | 同上;从基线加本轮补丁及新增文件重建 |
|
|
13
|
+
| Linux aarch64,Ruby 3.2.11,Bundler 4.0.17,独立容器副本 | 同上;冻结开发锁文件,容器独立安装依赖 |
|
|
14
|
+
|
|
15
|
+
三次均完成对话示例、三组基准 smoke、Gem 构建、普通 RubyGems 与最小 Bundler 应用的隔离安装及真实 PTY 对话。另检查包内 69 个文件及新增模块,无构建目录或日志;工作流 actionlint 通过(未启用 shellcheck),发布与示例 `--help` 正常。macOS 构建仍有宿主 RDoc 7/8 重复常量警告,退出状态为 0。
|
|
16
|
+
|
|
17
|
+
发布测试使用临时 Git 仓库,验证合法包通过,忽略文件、`assume-unchanged` 隐藏内容、忽略执行权限变化及主页元数据不一致均被拒绝,未连接发布服务。审查时版本仍为 0.3.2,改动位于 `Unreleased`,这些本地检查不代替发布验证;当时未运行真实 SSH、远端 CI 或 Ruby 3.3/3.4,也未提交、推送或发布。0.3.3 的远端平台结果以发布提交对应的 GitHub Actions 为准,以下历史结果不作为本轮验证证据。
|
|
18
|
+
|
|
3
19
|
## 配置并发与模块审查改进(2026-09-26)
|
|
4
20
|
|
|
5
21
|
当前工作树在既有未提交的匹配、转接和清理改动上,增加 `configure` 的串行读改写与回归测试:旧实现的并发用例先失败,修复后通过;嵌套调用显式抛出 `ThreadError`,不会陷入死锁或发布部分配置。`read_available` 的裁剪选择改为显式参数,转接调用明确禁止裁剪其待处理缓冲。RuboCop 对生产代码启用分支复杂度检查,既有状态机只在对应方法局部豁免;补充内部调用契约、贡献指南、issue/PR 模板与安全说明。
|
data/lib/expect/interaction.rb
CHANGED
|
@@ -8,7 +8,7 @@ class Expect
|
|
|
8
8
|
REGEXP_ESCAPE_HISTORY_LIMIT = 65_536
|
|
9
9
|
private_constant :REGEXP_ESCAPE_HISTORY_LIMIT
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# 在 raw 本地终端显示远端文本,补齐 LF 所需的 CR,同时保留已有 CRLF。
|
|
12
12
|
class InteractOutput
|
|
13
13
|
def initialize(target)
|
|
14
14
|
@target = target
|
|
@@ -196,7 +196,7 @@ class Expect
|
|
|
196
196
|
|
|
197
197
|
private :interact_source, :queue_output
|
|
198
198
|
|
|
199
|
-
#
|
|
199
|
+
# 只有 interact 知道哪个流是本地键盘;通用 interconnect 不修改终端模式。
|
|
200
200
|
def prepare_interact_terminal(source)
|
|
201
201
|
return unless source.raw_terminal? && source.tty?
|
|
202
202
|
|
|
@@ -204,7 +204,7 @@ class Expect
|
|
|
204
204
|
state = [io, io.console_mode]
|
|
205
205
|
io.raw!
|
|
206
206
|
state
|
|
207
|
-
rescue Exception # rubocop:disable Lint/RescueException --
|
|
207
|
+
rescue Exception # rubocop:disable Lint/RescueException -- 中断时也恢复可能已部分修改的终端。
|
|
208
208
|
restore_interact_terminal(state)
|
|
209
209
|
raise
|
|
210
210
|
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# 管理日志目标、监听器及同步输出;资源所有权仍由会话的 SessionResources 统一保存。
|
|
4
|
+
class Expect
|
|
5
|
+
# 读取当前日志目标,可能为库打开的文件、借用的 IO、回调或 nil。
|
|
6
|
+
attr_reader :log_output
|
|
7
|
+
|
|
8
|
+
# 替换借用的日志目标或停止日志;先校验新目标,失败时保留旧目标。
|
|
9
|
+
def log_output=(target)
|
|
10
|
+
unless target.nil? || target.respond_to?(:write) || target.respond_to?(:call)
|
|
11
|
+
raise ArgumentError, "log output must support write or call, or be nil"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
replace_log(target)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 打开追加/覆盖日志文件,或注册接收字节的日志块;同一次只能指定一种目标。
|
|
18
|
+
def log_to(target = nil, mode: "a", &block)
|
|
19
|
+
raise ArgumentError, "provide a log target or a block, not both" if block && target
|
|
20
|
+
|
|
21
|
+
target = block if block
|
|
22
|
+
if target.respond_to?(:to_path) || target.is_a?(String)
|
|
23
|
+
raise ArgumentError, "log mode must be a or w" unless %w[a w].include?(mode)
|
|
24
|
+
|
|
25
|
+
# 库打开的文件由 SessionResources 持有,替换日志或关闭会话时释放;外部 IO 只借用。
|
|
26
|
+
replace_log(File.open(target, "#{mode}b", 0o600), owned: true)
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError, "provide a log target or a block" unless target
|
|
29
|
+
|
|
30
|
+
self.log_output = target
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 向当前日志目标补写内容,支持 IO 和回调,不发送给子进程或监听器。
|
|
35
|
+
def write_log(*objects)
|
|
36
|
+
target = log_output
|
|
37
|
+
return unless target
|
|
38
|
+
|
|
39
|
+
data = objects.map { |object| object.to_s.b }.join
|
|
40
|
+
target.respond_to?(:call) ? target.call(data) : emit(target, data)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 返回监听器列表副本,避免外部原地修改转发关系。
|
|
44
|
+
def listeners = @listeners.dup
|
|
45
|
+
|
|
46
|
+
# 校验所有监听器均可写后一次性替换列表,外部数组后续修改不会影响会话。
|
|
47
|
+
def listeners=(outputs)
|
|
48
|
+
outputs = Array(outputs)
|
|
49
|
+
raise ArgumentError, "listeners must support write" unless outputs.all? { |output| output.respond_to?(:write) }
|
|
50
|
+
|
|
51
|
+
@listeners = outputs.dup
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# 按各自开关将接收字节转发到 stdout 和监听器,不重复写日志。
|
|
57
|
+
def propagate(data)
|
|
58
|
+
emit($stdout, data) if log_stdout?
|
|
59
|
+
@listeners.each { |listener| emit(listener, data) } if log_listeners?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 向目标写入并在支持时立即 flush,使日志和终端输出及时可见。
|
|
63
|
+
def emit(target, data)
|
|
64
|
+
offset = 0
|
|
65
|
+
while offset < data.bytesize
|
|
66
|
+
count = target.write(data.byteslice(offset..))
|
|
67
|
+
unless count.is_a?(Integer) && count.positive? && count <= data.bytesize - offset
|
|
68
|
+
raise IOError, "write must return the number of accepted bytes"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
offset += count
|
|
72
|
+
end
|
|
73
|
+
target.flush if target.respond_to?(:flush)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 按诊断级别向 stderr 输出会话标识和消息。
|
|
77
|
+
def trace(message, level: 1)
|
|
78
|
+
warn("#{inspect}: #{message}") if debug_level >= level
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 交接日志目标和所有权,只关闭库拥有的旧文件;失败时释放新打开的文件。
|
|
82
|
+
def replace_log(target, owned: false)
|
|
83
|
+
previous = log_output
|
|
84
|
+
# 重复赋值同一目标时保留原所有权,防止将库打开的文件误变成借用资源。
|
|
85
|
+
return target if previous.equal?(target)
|
|
86
|
+
|
|
87
|
+
previous.close if @resources.owned_log && previous && !previous.closed?
|
|
88
|
+
# 终结器只持有所属文件;借用回调可能捕获会话,不能让它经资源对象成为 GC 根。
|
|
89
|
+
@resources.owned_log = owned ? target : nil
|
|
90
|
+
@log_output = target
|
|
91
|
+
target
|
|
92
|
+
rescue Exception # rubocop:disable Lint/RescueException -- 替换失败时仍释放刚打开的文件。
|
|
93
|
+
target.close if owned && target && !target.closed?
|
|
94
|
+
raise
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/expect/matcher.rb
CHANGED
|
@@ -12,6 +12,7 @@ class Expect
|
|
|
12
12
|
@handled_eof = []
|
|
13
13
|
@stalled_matches = {}
|
|
14
14
|
@polled = false
|
|
15
|
+
@expired_eof_continuation = false
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# 运行匹配状态机;内部 :retry 表示继续循环,最终返回一个 Result。
|
|
@@ -30,10 +31,12 @@ class Expect
|
|
|
30
31
|
end
|
|
31
32
|
loop do
|
|
32
33
|
# 先消费已缓冲的匹配,再处理 EOF,最后读取;避免进程退出时丢失最后一个匹配。
|
|
33
|
-
result = if (matched = find_match)
|
|
34
|
+
result = if !@expired_eof_continuation && (matched = find_match)
|
|
34
35
|
handle_match(*matched)
|
|
35
36
|
elsif (session = unhandled_eof)
|
|
36
37
|
handle_eof(session)
|
|
38
|
+
elsif @expired_eof_continuation
|
|
39
|
+
handle_timeout
|
|
37
40
|
else
|
|
38
41
|
read_next
|
|
39
42
|
end
|
|
@@ -109,7 +112,12 @@ class Expect
|
|
|
109
112
|
return result unless actions.any? { |action| continuing?(action) }
|
|
110
113
|
|
|
111
114
|
@deadline = next_deadline if actions.include?(CONTINUE)
|
|
112
|
-
@sessions.all? { |candidate| @handled_eof.include?(candidate) }
|
|
115
|
+
return result if @sessions.all? { |candidate| @handled_eof.include?(candidate) }
|
|
116
|
+
|
|
117
|
+
# 期限已过时不再扫描文本,但先派发已知 EOF;最后一个源结束不能被误报为超时。
|
|
118
|
+
@expired_eof_continuation = !actions.include?(CONTINUE) && expired?
|
|
119
|
+
|
|
120
|
+
:retry
|
|
113
121
|
end
|
|
114
122
|
|
|
115
123
|
# 在剩余期限内等待可读 IO;零超时仍允许首次非阻塞轮询,EINTR 重试不重新计时。
|
|
@@ -182,6 +190,7 @@ class Expect
|
|
|
182
190
|
|
|
183
191
|
@deadline = next_deadline
|
|
184
192
|
@polled = false
|
|
193
|
+
@expired_eof_continuation = false
|
|
185
194
|
:retry
|
|
186
195
|
end
|
|
187
196
|
end
|
data/lib/expect/pattern.rb
CHANGED
|
@@ -18,8 +18,12 @@ class Expect
|
|
|
18
18
|
offset = buffer.index(value)
|
|
19
19
|
return [offset, value.bytesize, []] if offset
|
|
20
20
|
when Regexp
|
|
21
|
-
|
|
22
|
-
text.
|
|
21
|
+
# 正则只读取输入;只有编码标记不同才复制,避免每个模式额外分配缓冲对象。
|
|
22
|
+
text = if value.fixed_encoding? && value.encoding != buffer.encoding
|
|
23
|
+
buffer.dup.force_encoding(value.encoding)
|
|
24
|
+
else
|
|
25
|
+
buffer
|
|
26
|
+
end
|
|
23
27
|
unless text.valid_encoding?
|
|
24
28
|
# 不完整的尾字符可能改变锚点或前瞻结果,必须等字符收齐后再匹配。
|
|
25
29
|
validate_incomplete_suffix!(text, final: final)
|
|
@@ -19,9 +19,9 @@ class Expect
|
|
|
19
19
|
def close_handles
|
|
20
20
|
return unless own
|
|
21
21
|
|
|
22
|
-
# PTY
|
|
22
|
+
# 初始化校验失败时可能含无效参数,只关闭真实 IO;PTY 读写端也需要去重。
|
|
23
23
|
failure = nil
|
|
24
|
-
[reader, writer, slave].
|
|
24
|
+
[reader, writer, slave].grep(IO).uniq.each do |io|
|
|
25
25
|
io.close unless io.closed?
|
|
26
26
|
rescue IOError, SystemCallError => error
|
|
27
27
|
failure ||= error
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
# 会话终端的模式和窗口尺寸接口;人工接管期间的临时恢复由 interaction.rb 负责。
|
|
6
|
+
class Expect
|
|
7
|
+
# 查询可恢复的终端模式字符串,或通过系统 stty 设置模式;参数按数组传递,不经 shell。
|
|
8
|
+
def stty(*modes)
|
|
9
|
+
return "" unless tty?
|
|
10
|
+
|
|
11
|
+
modes = modes.flat_map { |mode| Shellwords.split(mode.to_s) }
|
|
12
|
+
modes = ["-g"] if modes.empty?
|
|
13
|
+
reader, sink = IO.pipe
|
|
14
|
+
child = Process.spawn("stty", *modes, in: to_io, out: sink, err: sink)
|
|
15
|
+
sink.close
|
|
16
|
+
output = reader.read
|
|
17
|
+
_, status = Process.waitpid2(child)
|
|
18
|
+
raise IOError, "stty failed: #{output.strip}" unless status.success?
|
|
19
|
+
|
|
20
|
+
output.strip
|
|
21
|
+
rescue Errno::ENOENT
|
|
22
|
+
raise IOError, "stty executable not found in PATH; install the system terminal utilities"
|
|
23
|
+
ensure
|
|
24
|
+
reader&.close unless reader&.closed?
|
|
25
|
+
sink&.close unless sink&.closed?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 读取终端的 [行数, 列数]。
|
|
29
|
+
def winsize = to_io.winsize
|
|
30
|
+
|
|
31
|
+
# 更新终端尺寸,由内核通知前台进程。
|
|
32
|
+
def winsize=(size)
|
|
33
|
+
to_io.winsize = size
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/expect/version.rb
CHANGED