expect-pty 0.3.3 → 0.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e21e96f8c4a79123cb7eb74606f0b0a8195c855290ba5ca97b43419917b1e36
4
- data.tar.gz: 5f77fab495dd5d5b10e91e14a6957d3907c261cd9931dd73d1113827c1eabc74
3
+ metadata.gz: 5f3f00d375bb8a90ef67974bbd7649b031554e15f9d463cf28f1388d5d3e5dc1
4
+ data.tar.gz: 9c9a556719b8c79c43553bc344fa679737abb2330f8ecdec72beabfedb3842d7
5
5
  SHA512:
6
- metadata.gz: b6334c31be027acef464d2d648dec242e43eff2367076a149c64a64a88edd27da2d51a65f3e85afcf2e8d15b2c41439c496b75731cc26db8e816d1fd3296076b
7
- data.tar.gz: 1437a5c147513aa572b8b0430d8f25f0ed526ba6b14309f261f0ef30e1f0fa7e2b4850c4abd8a6a35f0988307f204870f31961c26db77245a6d35f986e34babe
6
+ metadata.gz: 53fa1a25e62c9c16c2ecbafc3efa38ba4acd2c805afa6b1547e4b83cb95c98484bbd27f612a83397a9eb9a0715af857b7f9f4d698f52bdcaa4b7dcbd3a55a42e
7
+ data.tar.gz: 226b72621b685c9fce34097838e0c10a81f616b3ba26a62a90eb6f09c9e3a6e0a3cd5cac331ba79c9a81caf134707821622a3e1ebe85842aff01da79b1d1d554
data/CHANGELOG.md CHANGED
@@ -2,17 +2,33 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.0 - 2026-09-27
6
+
7
+ - **独立脱敏**:新增无需创建会话的 `Expect::Redactor`,支持完整文本和分块字节流过滤,以及自定义替换标记。
8
+ - **过滤规则**:支持空模式列表和复制校验后的规则更新,完整文本仅匹配完整秘密,流结束时默认隐藏未完成的秘密前缀,现有会话脱敏行为保持不变。
9
+
10
+ ## 0.4.0 - 2026-09-27
11
+
12
+ - **缓冲与生命周期**:新增只读 `buffer_discarded_bytes`,区分上限裁剪与正常消费;补齐外部回收进程、借用 IO、重复关闭和失败重试的契约回归。
13
+ - **等待总期限**:类和实例的 `expect` / `expect_result` 支持绝对单调时钟 `deadline:`,接收重置与继续回调不能延长总预算,回调后到期也保留未消费文本和已知 EOF 顺序。
14
+ - **诊断与脱敏**:会话级 `diagnostic_output` 支持 Logger、IO 和结构化回调;`redact` 对日志及收发诊断按字节流过滤,覆盖分片、重叠和流尾部,不改写匹配或协议转发。
15
+ - **匹配性能**:字面模式复用同一缓冲代次中已排除的前缀,消费、替换和裁剪后失效;正则保留完整窗口及原有声明优先级。
16
+ - **验证与维护**:增加持续输出、同时就绪来源和阻塞目标基准,记录 RSS 端点和描述符计数;补齐底层模块中文注释与内部契约。
17
+
5
18
  ## 0.3.3 - 2026-09-27
6
19
 
7
- - **生命周期**:构造及工厂启动失败时统一释放所属 IO 并回收子进程;块或软关闭的原始异常不再被后续 IO 清理错误覆盖,失败的单个句柄不会阻止其他资源收尾。
8
- - **匹配期限**:EOF 回调保留原期限继续时,不再于期限过后消费其他来源的文本匹配;已知 EOF 仍依次派发,所有来源结束时保持 EOF 结果。
20
+ - **生命周期**:构造及工厂启动失败时统一释放所属 IO 并回收子进程;块或软关闭的原始异常不再被后续 IO
21
+ 清理错误覆盖,失败的单个句柄不会阻止其他资源收尾。
22
+ - **匹配期限**:EOF 回调保留原期限继续时,不再于期限过后消费其他来源的文本匹配;已知 EOF 仍依次派发,所有来源结束时保持 EOF
23
+ 结果。
9
24
  - **正则性能**:只在需要调整编码时复制输入缓冲,减少大缓冲区多模式扫描的耗时和分配,保持编码及字节偏移语义。
10
25
  - **终端错误**:系统缺少 `stty` 时提供明确的 IO 错误和安装提示,并保留原始异常。
11
26
  - **发布校验**:拒绝发布与目标 Git 提交的内容、执行权限或包元数据不一致的 Gem,提交前仍可通过 dry-run 验证候选包。
12
27
 
13
28
  ## 0.3.2 - 2026-09-26
14
29
 
15
- - **匹配与转接**:正则直接使用字节偏移,重复来源在单轮扫描中共享缓冲快照;转义正则复用本轮组合文本,保留回调修改规则后的重新扫描语义。`send_slow` 只读取已经可读的回复。
30
+ - **匹配与转接**:正则直接使用字节偏移,重复来源在单轮扫描中共享缓冲快照;转义正则复用本轮组合文本,保留回调修改规则后的重新扫描语义。
31
+ `send_slow` 只读取已经可读的回复。
16
32
  - **配置与清理**:并发 `Expect.configure` 串行发布,避免丢失更新;关闭句柄或日志失败时继续清理其他资源和直属子进程,并保留首个清理错误。转接读取显式保留未交付缓冲。
17
33
  - **维护与验证**:增加状态归属文档、性能基准及 smoke、分支复杂度检查、贡献指南和安全说明;补充匹配、转接、清理及并发配置回归测试。
18
34
 
@@ -24,22 +40,33 @@
24
40
 
25
41
  ## 0.3.0 - 2026-09-26
26
42
 
27
- - **终端模式调整**:通用 `interconnect` 改由调用方设置和恢复终端模式,需要自动切换本地输入终端时使用 `interact`,直接转接终端的现有代码需要相应调整。
28
- - **转接恢复**:真实 IO 的背压受转接总期限和目标写期限约束,超时、短写入或监听器失败后可继续发送未完成的后缀,`pending_output?` 可查询待发送状态。
29
- - **输入与写入**:重复 `interact` 会接续同一输入 IO 的预读尾部,背压输入保留转义过滤,`WriteTimeout#bytes_written` 提供写入进度,临时 `EINTR` 不再中断原操作或重置期限。
30
- - **匹配与编码**:固定 UTF-8 正则等待分片字符收齐,非法或 EOF 截断编码抛出 `EncodingError` 并保留原字节,无进展回调等待新输入,正则转义默认只保留最近 65,536 字节历史。
43
+ - **终端模式调整**:通用 `interconnect` 改由调用方设置和恢复终端模式,需要自动切换本地输入终端时使用 `interact`
44
+ ,直接转接终端的现有代码需要相应调整。
45
+ - **转接恢复**:真实 IO 的背压受转接总期限和目标写期限约束,超时、短写入或监听器失败后可继续发送未完成的后缀,
46
+ `pending_output?` 可查询待发送状态。
47
+ - **输入与写入**:重复 `interact` 会接续同一输入 IO 的预读尾部,背压输入保留转义过滤,`WriteTimeout#bytes_written`
48
+ 提供写入进度,临时 `EINTR` 不再中断原操作或重置期限。
49
+ - **匹配与编码**:固定 UTF-8 正则等待分片字符收齐,非法或 EOF 截断编码抛出 `EncodingError` 并保留原字节,无进展回调等待新输入,正则转义默认只保留最近
50
+ 65,536 字节历史。
31
51
  - **日志与回收**:新建日志默认权限为 `0600`,日志异常后接收数据仍可恢复,捕获会话的日志回调不再阻止子进程的 GC 回收。
32
- - **重复接管与回调**:切换退出正则不再匹配已转发的旧输入,相同终端的跨次 CRLF 保持原样,转义回调可用 `expect` 消费预读尾部和新回复,嵌套输出超时保留调用方的写入进度。
52
+ - **重复接管与回调**:切换退出正则不再匹配已转发的旧输入,相同终端的跨次 CRLF 保持原样,转义回调可用 `expect`
53
+ 消费预读尾部和新回复,嵌套输出超时保留调用方的写入进度。
33
54
 
34
55
  ## 0.2.0 - 2026-09-13
35
56
 
36
- - **不兼容变更**:接口统一采用 Ruby 属性、谓词、关键字参数、原生正则和块回调,移除 `exp_*`、`get/set_accum`、位置超时和数组回调等旧入口,不提供兼容别名,迁移方式见 `docs/COMPATIBILITY.md`。
37
- - **配置与日志**:通过 `Expect.configure` 设置默认值,每个会话独立管理缓冲、日志、终端模式和超时策略,默认关闭 stdout 输出,日志入口统一为 `log_output`、`log_to` 和 `write_log`。
38
- - **匹配与多会话**:`expect` 返回模式序号,`expect_result` 返回七字段 Struct,错误保留为 `:timeout`、`:eof` 或原始 IO 异常,`from:` 可选择会话,超时块接收全部活跃会话。
39
- - **输入与转接**:`write`、`puts` 和 `<<` 遵循 Ruby IO 语义,`send` 保留反射用途,`send_slow(delay:)` 支持逐字符发送,`on_sequence` 使用闭包和 Ruby 真值控制转接。
40
- - **进程清理**:`soft_close` 收集尾部输出并最多发送 TERM,`hard_close` 必要时发送 KILL,两者返回进程状态,块和 `close(graceful:)` 在异常路径也完成资源回收。
57
+ - **不兼容变更**:接口统一采用 Ruby 属性、谓词、关键字参数、原生正则和块回调,移除 `exp_*`、`get/set_accum`
58
+ 、位置超时和数组回调等旧入口,不提供兼容别名,迁移方式见 `docs/COMPATIBILITY.md`。
59
+ - **配置与日志**:通过 `Expect.configure` 设置默认值,每个会话独立管理缓冲、日志、终端模式和超时策略,默认关闭 stdout
60
+ 输出,日志入口统一为 `log_output`、`log_to` 和 `write_log`。
61
+ - **匹配与多会话**:`expect` 返回模式序号,`expect_result` 返回七字段 Struct,错误保留为 `:timeout`、`:eof` 或原始 IO 异常,
62
+ `from:` 可选择会话,超时块接收全部活跃会话。
63
+ - **输入与转接**:`write`、`puts` 和 `<<` 遵循 Ruby IO 语义,`send` 保留反射用途,`send_slow(delay:)` 支持逐字符发送,
64
+ `on_sequence` 使用闭包和 Ruby 真值控制转接。
65
+ - **进程清理**:`soft_close` 收集尾部输出并最多发送 TERM,`hard_close` 必要时发送 KILL,两者返回进程状态,块和
66
+ `close(graceful:)` 在异常路径也完成资源回收。
41
67
  - **SSH 与双终端示例**:提供自动执行后切换人工交互的 SSH 示例和 Kibitz 双终端共享示例,支持转义退出、终端恢复和会话日志。
42
- - **安装与运行**:通过 `gem install expect-pty` 安装,使用 `require "expect/pty"` 加载,支持 Linux/macOS 与 Ruby 3.2 及以上版本,运行时仅依赖标准库。
68
+ - **安装与运行**:通过 `gem install expect-pty` 安装,使用 `require "expect/pty"` 加载,支持 Linux/macOS 与 Ruby 3.2
69
+ 及以上版本,运行时仅依赖标准库。
43
70
 
44
71
  ## 0.1.1
45
72
 
data/Gemfile CHANGED
@@ -16,6 +16,7 @@ group :development, :test do
16
16
  gem "etc", require: false
17
17
  gem "fileutils", require: false
18
18
  gem "json", require: false
19
+ gem "logger", require: false
19
20
  gem "net-http", require: false
20
21
  gem "open3", require: false
21
22
  gem "optparse", require: false
data/README.md CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  [![CI](https://github.com/gatework/expect-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/gatework/expect-ruby/actions/workflows/ci.yml)
4
4
 
5
- 用 Ruby 自动操作交互式程序:启动拥有控制终端的子进程,等待文本或正则,发送输入,处理超时、EOF 和回调,也能接管已有 IO、同时监听多个会话和转接人工交互。交互能力参考 [Expect.pm](https://github.com/jacoby/expect.pm),接口采用 Ruby 的属性、关键字参数和代码块。
5
+ 用 Ruby 自动操作交互式程序:启动拥有控制终端的子进程,等待文本或正则,发送输入,处理超时、EOF 和回调,也能接管已有
6
+ IO、同时监听多个会话和转接人工交互。交互能力参考 [Expect.pm](https://github.com/jacoby/expect.pm),接口采用 Ruby
7
+ 的属性、关键字参数和代码块。
6
8
 
7
- 要求 **Ruby 3.2+、POSIX 系统(Linux/macOS)**。运行时仅使用 Ruby 标准库,其中可独立安装的 gem 已在 gemspec 中声明,由 RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提供独立的 `Expect` 类,不修改标准库的 `IO#expect`。
9
+ 要求 **Ruby 3.2+、POSIX 系统(Linux/macOS)**。运行时仅使用 Ruby 标准库,其中可独立安装的 gem 已在 gemspec 中声明,由
10
+ RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提供独立的 `Expect` 类,不修改标准库的 `IO#expect`。
8
11
 
9
12
  源码中的解释性注释主要使用中文;欢迎用中文或英文提交 issue 和 PR,参与方式见 [贡献指南](CONTRIBUTING.md)。
10
13
 
@@ -13,7 +16,7 @@
13
16
  项目和仓库名为 `expect-ruby`,Gem 名为 `expect-pty`。在应用的 Gemfile 中添加以下内容,然后运行 `bundle install`:
14
17
 
15
18
  ```ruby
16
- gem "expect-pty", "~> 0.3.3", require: "expect/pty"
19
+ gem "expect-pty", "~> 0.5.0", require: "expect/pty"
17
20
  ```
18
21
 
19
22
  也可直接执行 `gem install expect-pty`。需要跟随开发分支时,可从 GitHub 安装:
@@ -26,8 +29,8 @@ gem "expect-pty", git: "https://github.com/gatework/expect-ruby.git", branch: "m
26
29
 
27
30
  ```sh
28
31
  mkdir -p tmp
29
- gem build expect-pty.gemspec --output tmp/expect-pty-0.3.3.gem
30
- gem install ./tmp/expect-pty-0.3.3.gem
32
+ gem build expect-pty.gemspec --output tmp/expect-pty-0.5.0.gem
33
+ gem install ./tmp/expect-pty-0.5.0.gem
31
34
  ```
32
35
 
33
36
  ```ruby
@@ -45,9 +48,12 @@ Expect.spawn("/bin/sh", "-i") do |shell|
45
48
  end
46
49
  ```
47
50
 
48
- 块返回其执行结果,退出时关闭会话并回收子进程,异常和 `break` 也执行清理。构造或启动失败时同样释放已创建的资源;清理中的 IO 错误不会替换正在传播的原始异常。无块形式需用 `ensure` 显式调用 `close`。`Expect.new` 可以先创建 PTY、设置 `slave.echo` / `slave.winsize`,然后调用实例的 `spawn`。
51
+ 块返回其执行结果,退出时关闭会话并回收子进程,异常和 `break` 也执行清理。构造或启动失败时同样释放已创建的资源;清理中的 IO
52
+ 错误不会替换正在传播的原始异常。无块形式需用 `ensure` 显式调用 `close`。`Expect.new` 可以先创建 PTY、设置 `slave.echo` /
53
+ `slave.winsize`,然后调用实例的 `spawn`。
49
54
 
50
- 多个命令参数原样传给 Ruby `exec`;单个命令字符串使用 Ruby 的 shell 语义。不可信参数应使用独立参数形式。支持 `env: { "NAME" => "value" }` 和 `chdir: "/path"`。同一会话只能启动一次,启动失败抛出 `Expect::SpawnError`。
55
+ 多个命令参数原样传给 Ruby `exec`;单个命令字符串使用 Ruby 的 shell 语义。不可信参数应使用独立参数形式。支持
56
+ `env: { "NAME" => "value" }` 和 `chdir: "/path"`。同一会话只能启动一次,启动失败抛出 `Expect::SpawnError`。
51
57
 
52
58
  ## 配置与属性
53
59
 
@@ -69,25 +75,27 @@ Expect.spawn("/bin/sh", "-i", timeout: 3) do |session|
69
75
  end
70
76
  ```
71
77
 
72
- `configure` 校验后发布冻结的配置对象;块异常不会发布部分修改。并发调用按顺序完成读改写,不会互相覆盖不同属性。配置块在锁内执行,应保持简短,不要在块内再次调用 `configure`(会抛出 `ThreadError`)或等待其他配置线程。每个会话独立持有配置,优先使用构造参数;修改默认值不会改变已有会话,修改一个会话也不会影响其他会话。子类继承父类默认配置,可独立覆盖。
78
+ `configure` 校验后发布冻结的配置对象;块异常不会发布部分修改。并发调用按顺序完成读改写,不会互相覆盖不同属性。配置块在锁内执行,应保持简短,不要在块内再次调用
79
+ `configure`(会抛出 `ThreadError`)或等待其他配置线程。每个会话独立持有配置,优先使用构造参数;修改默认值不会改变已有会话,修改一个会话也不会影响其他会话。子类继承父类默认配置,可独立覆盖。
73
80
 
74
81
  全局默认配置通常在应用启动时设置;每次会话的动态差异使用构造参数或会话属性,避免在高频路径反复获取共享配置锁。
75
82
 
76
- | 属性 | 默认值 | 行为 |
77
- | --- | --- | --- |
78
- | `timeout` | `nil` | 等待匹配的默认超时,秒;`nil` 无限,`0` 非阻塞轮询 |
79
- | `write_timeout` | `nil` | 写入遇到背压时的超时,秒 |
80
- | `buffer_limit` | `nil` | 接收缓冲最多保留的字节数;正整数或 `nil`(无限) |
81
- | `debug_level` | `0` | `1` 生命周期和匹配,`2` 加收发内容,`3` 加缓冲内容 |
82
- | `raw_pty` | `false` | spawn 前将 slave 设为 raw,禁用回显和换行转换 |
83
- | `preserve_buffer` | `false` | 匹配后保留完整缓冲 |
84
- | `log_stdout` | `false` | 将接收内容输出到 `$stdout` |
85
- | `log_listeners` | `true` | 将接收内容转发给 `listeners` |
86
- | `raw_terminal` | `true` | `interact` 期间自动设置并恢复输入终端模式,同时保留输出换行处理 |
87
- | `reset_timeout_on_read` | `false` | 每次收到数据时重置匹配期限 |
88
- | `graceful_close` | `false` | `close` 先尝试软关闭,再完成强制清理 |
89
-
90
- 布尔属性均提供 `name`、`name?` 和 `name=`,按 Ruby 真值规则转换:仅 `nil` / `false` 为假,`0` 为真。超时必须有限且非负,`nil` 表示无限;无效赋值不改变原值。`debug_level` 仅接受整数 `0..3`。
83
+ | 属性 | 默认值 | 行为 |
84
+ |-------------------------|---------|-----------------------------------------------------------------|
85
+ | `timeout` | `nil` | 等待匹配的默认超时,秒;`nil` 无限,`0` 非阻塞轮询 |
86
+ | `write_timeout` | `nil` | 写入遇到背压时的超时,秒 |
87
+ | `buffer_limit` | `nil` | 接收缓冲最多保留的字节数;正整数或 `nil`(无限) |
88
+ | `debug_level` | `0` | `1` 生命周期和匹配,`2` 加收发内容,`3` 加缓冲内容 |
89
+ | `raw_pty` | `false` | spawn 前将 slave 设为 raw,禁用回显和换行转换 |
90
+ | `preserve_buffer` | `false` | 匹配后保留完整缓冲 |
91
+ | `log_stdout` | `false` | 将接收内容输出到 `$stdout` |
92
+ | `log_listeners` | `true` | 将接收内容转发给 `listeners` |
93
+ | `raw_terminal` | `true` | `interact` 期间自动设置并恢复输入终端模式,同时保留输出换行处理 |
94
+ | `reset_timeout_on_read` | `false` | 每次收到数据时重置匹配期限 |
95
+ | `graceful_close` | `false` | `close` 先尝试软关闭,再完成强制清理 |
96
+
97
+ 布尔属性均提供 `name`、`name?` 和 `name=`,按 Ruby 真值规则转换:仅 `nil` / `false` 为假,`0` 为真。超时必须有限且非负,`nil`
98
+ 表示无限;无效赋值不改变原值。`debug_level` 仅接受整数 `0..3`。
91
99
 
92
100
  ## 等待和匹配
93
101
 
@@ -100,7 +108,9 @@ session.expect("ready", timeout: 0) # 匹配现有缓冲,并最多轮询读
100
108
  session.expect(timeout: 1) # 仅收集输出,直到超时或 EOF
101
109
  ```
102
110
 
103
- 字符串始终按字面匹配,包括 `"-i"`、`"-re"`、`"timeout"` 和 `"eof"`;正则直接使用 Ruby `Regexp`。按声明顺序选择第一个能匹配的模式,不按它们在文本中的位置排序。返回模式的 **1 起始序号**,超时、EOF 或 IO 错误返回 `nil`。超时使用单调时钟。
111
+ 字符串始终按字面匹配,包括 `"-i"`、`"-re"`、`"timeout"` 和 `"eof"`;正则直接使用 Ruby `Regexp`
112
+ 。按声明顺序选择第一个能匹配的模式,不按它们在文本中的位置排序。返回模式的 **1 起始序号**,超时、EOF 或 IO 错误返回 `nil`
113
+ 。超时使用单调时钟。
104
114
 
105
115
  ```ruby
106
116
  result = session.expect_result(/value=(\d+)/, timeout: 3)
@@ -115,18 +125,31 @@ result.error # nil、:timeout、:eof 或原始 IOError / SystemCallError 对象
115
125
  number, error, match, before, after, session, captures = result.to_a
116
126
  ```
117
127
 
118
- `Result` 使用原生 Ruby `Struct`,支持 `to_a`、`to_h`、模式解构;没有隐式 `to_ary`。会话提供 `last_result`,以及 `match`、`before`、`after`、`match_number`、`captures`、`error` 快捷读取方法。
128
+ `Result` 使用原生 Ruby `Struct`,支持 `to_a`、`to_h`、模式解构;没有隐式 `to_ary`。会话提供 `last_result`,以及 `match`、
129
+ `before`、`after`、`match_number`、`captures`、`error` 快捷读取方法。
119
130
 
120
- 成功匹配后删除匹配内容及其之前的内容,尾部留给下次匹配;超时保留缓冲,EOF 将未匹配内容放入 `before` 并清空缓冲。EOF 与子进程退出是不同事件,使用 `wait` / `process_status` 判断进程结果。底层 IO 错误保留原始异常,回调中的普通异常直接抛出。
131
+ 成功匹配后删除匹配内容及其之前的内容,尾部留给下次匹配;超时保留缓冲,EOF 将未匹配内容放入 `before` 并清空缓冲。EOF
132
+ 与子进程退出是不同事件,使用 `wait` / `process_status` 判断进程结果。底层 IO 错误保留原始异常,回调中的普通异常直接抛出。
121
133
 
122
- 接收缓冲、匹配和捕获值为 `ASCII-8BIT` 字节串,保留控制字符、NUL 和无效 UTF-8。固定 UTF-8 正则会等待读取末尾拆开的字符收齐后再匹配,以免尾部锚点提前命中;显示捕获内容时可 `.force_encoding("UTF-8")`。任意二进制流请用字面字符串或二进制正则 `/.../n`。固定 UTF-8 正则遇到无效数据抛出 `EncodingError`;EOF 时仍未收齐的字符也属于无效编码,匹配缓冲保留原字节供诊断或二进制匹配。缓冲上限按字节截断,应为文本设置足够的上限。
134
+ 接收缓冲、匹配和捕获值为 `ASCII-8BIT` 字节串,保留控制字符、NUL 和无效 UTF-8。固定 UTF-8
135
+ 正则会等待读取末尾拆开的字符收齐后再匹配,以免尾部锚点提前命中;显示捕获内容时可 `.force_encoding("UTF-8")`
136
+ 。任意二进制流请用字面字符串或二进制正则 `/.../n`。固定 UTF-8 正则遇到无效数据抛出 `EncodingError`;EOF
137
+ 时仍未收齐的字符也属于无效编码,匹配缓冲保留原字节供诊断或二进制匹配。缓冲上限按字节截断,应为文本设置足够的上限。
123
138
 
124
139
  正则完全遵循 Ruby:`^` / `$` 是行锚点,`\A` / `\z` 是整个缓冲的锚点,`/m` 让点号匹配换行;不再提供全局正则模式开关。
125
140
 
126
- IO 等待的 `timeout` 不会中断单次正则计算。处理用户提供的正则或不可信长输出时,应使用有限时的正则实例,例如 `Regexp.new('prompt>\\s*', timeout: 0.05)`;该限制同样适用于 `on_sequence`。正则超时原样抛出 `Regexp::TimeoutError`,匹配缓冲保留,库不会修改进程全局 `Regexp.timeout`。`timeout: 0` 仍会匹配已有缓冲,不代表禁止正则计算。长输出可用日志保存全文,按业务需要设置 `buffer_limit` 限制匹配窗口;缩小窗口会改变 `before` 和跨窗口匹配范围。
141
+ IO 等待的 `timeout` 不会中断单次正则计算。处理用户提供的正则或不可信长输出时,应使用有限时的正则实例,例如
142
+ `Regexp.new('prompt>\\s*', timeout: 0.05)`;该限制同样适用于 `on_sequence`。正则超时原样抛出 `Regexp::TimeoutError`
143
+ ,匹配缓冲保留,库不会修改进程全局 `Regexp.timeout`。`timeout: 0` 仍会匹配已有缓冲,不代表禁止正则计算。长输出可用日志保存全文,按业务需要设置
144
+ `buffer_limit` 限制匹配窗口;缩小窗口会改变 `before` 和跨窗口匹配范围。
145
+
146
+ `session.buffer_discarded_bytes` 是只读的会话累计计数,只统计 `buffer_limit` 裁剪掉的字节。
147
+ 读取新数据、赋值 `buffer` 或调低上限触发裁剪时递增;成功匹配、`clear_buffer`、EOF 消费和转接交接均不计入。
148
+ 关闭会话不会清零。日志在实际读取时保存完整接收内容,不受匹配窗口裁剪影响;它可按显式注册的秘密脱敏。
127
149
 
128
150
  ## 回调、事件与多会话
129
151
 
152
+
130
153
  ```ruby
131
154
  session.expect(timeout: 10) do
132
155
  on(/username:\s*/i) do |connection|
@@ -143,11 +166,30 @@ session.expect(timeout: 10) do
143
166
  end
144
167
  ```
145
168
 
146
- 回调通过闭包访问局部变量。无参数声明块在模式构建器中执行;希望保留调用方 `self` 时使用 `do |patterns|`,调用 `patterns.on(...)`。所有模式注册完成后才读取 IO;注册异常或 `break` 不消费输入。块和位置模式不能混用。`expect_result` 支持同样的声明方式。
169
+ 回调通过闭包访问局部变量。无参数声明块在模式构建器中执行;希望保留调用方 `self` 时使用 `do |patterns|`,调用
170
+ `patterns.on(...)`。所有模式注册完成后才读取 IO;注册异常或 `break` 不消费输入。块和位置模式不能混用。`expect_result`
171
+ 支持同样的声明方式。
172
+
173
+ `continue` 继续等待并重新计时;`continue(reset_timeout: false)`
174
+ 保留原期限,类和实例均可调用。回调返回后若保留的期限已过,不再扫描新的文本匹配,未消费的输入留给下一次等待。无回调或返回其他值时结束本次匹配。超时回调只有返回重置计时的
175
+ `continue` 才再次等待。EOF 回调继续时移除该源并等待其余会话;已知的 EOF 仍依次派发,全部 EOF 时直接返回,期限已过时仅对剩余活跃源触发超时。
176
+
177
+ `expect` / `expect_result` 另接受 `deadline:`,值为 `Expect.monotonic` 时钟上的绝对秒数,`nil` 表示不设总期限。总期限与普通
178
+ `timeout` 取较早者,接收重置、文本/EOF 继续及超时回调均不能延长它。同一个 deadline 可用于连续多次等待:
179
+
180
+ ```ruby
181
+ deadline = Expect.monotonic + 60
182
+ session.reset_timeout_on_read = true
183
+ session.expect("ready", timeout: 5, deadline: deadline)
184
+ session.expect("done", timeout: 5, deadline: deadline)
185
+ ```
147
186
 
148
- `continue` 继续等待并重新计时;`continue(reset_timeout: false)` 保留原期限,类和实例均可调用。回调返回后若保留的期限已过,不再扫描新的文本匹配,未消费的输入留给下一次等待。无回调或返回其他值时结束本次匹配。超时回调只有返回重置计时的 `continue` 才再次等待。EOF 回调继续时移除该源并等待其余会话;已知的 EOF 仍依次派发,全部 EOF 时直接返回,期限已过时仅对剩余活跃源触发超时。
187
+ 上例每次等待允许最多 5 秒无新输入,两次等待共用 60 秒总预算。到达总期限后返回超时,不消费已缓冲的文本或读取新数据;已经确认的
188
+ EOF 仍可派发,全部来源结束时返回 EOF。`timeout: 0`
189
+ 在总期限尚未到达时仍保留一次非阻塞轮询。总期限使用协作检查,不中断正在执行的回调、同步日志或单次正则;正则完成后若总期限已到,不消费其匹配结果。它不自动传给回调中的写入或其他操作。
149
190
 
150
- `eof` / `timeout` 声明会占用模式序号,但事件返回的 `number` 为 `nil`。一个等待只能注册一个超时回调;它接收**所有仍在监听的会话**。不需要回调时,可将 `:eof` / `:timeout` 作为位置事件参数。
191
+ `eof` / `timeout` 声明会占用模式序号,但事件返回的 `number` 为 `nil`。一个等待只能注册一个超时回调;它接收
192
+ **所有仍在监听的会话**。不需要回调时,可将 `:eof` / `:timeout` 作为位置事件参数。
151
193
 
152
194
  ```ruby
153
195
  Expect.expect(timeout: 5) do
@@ -158,9 +200,11 @@ end
158
200
  Expect.expect("ready", from: [first, second], timeout: 5)
159
201
  ```
160
202
 
161
- `from:` 指定一个或多个会话;实例块默认当前会话,类方法需提供来源。相邻且来源列表相同的模式组成一组,按组、会话、模式顺序匹配。类方法省略超时使用 `Expect.configuration.timeout`。
203
+ `from:` 指定一个或多个会话;实例块默认当前会话,类方法需提供来源。相邻且来源列表相同的模式组成一组,按组、会话、模式顺序匹配。类方法省略超时使用
204
+ `Expect.configuration.timeout`。
162
205
 
163
- `preserve_buffer = true` 时,继续回调通常应自行消费匹配,例如 `connection.buffer = connection.after`。如果回调没有改变缓冲,当前模式会等待缓冲变化后才重新匹配,避免反复处理同一内容。被信号中断的匹配 select/read 会自动重试,保留原期限。
206
+ `preserve_buffer = true` 时,继续回调通常应自行消费匹配,例如 `connection.buffer = connection.after`
207
+ 。如果回调没有改变缓冲,当前模式会等待缓冲变化后才重新匹配,避免反复处理同一内容。被信号中断的匹配 select/read 会自动重试,保留原期限。
164
208
 
165
209
  ## 已有 IO、写入和终端
166
210
 
@@ -173,29 +217,37 @@ end
173
217
  ready = Expect.readable_sessions(first, second, timeout: 5)
174
218
  ```
175
219
 
176
- `Expect.open` 支持可 `select` 的 File、管道、Socket 和 PTY,`writer:` 可指定独立写端。默认借用 IO,关闭会话不关闭原始 IO;`own: true` 转移关闭责任,初始化失败也会释放接管的 IO。`StringIO` 可以用作日志和监听器,不能用作读取会话。
220
+ `Expect.open` 支持可 `select` 的 File、管道、Socket 和 PTY,`writer:` 可指定独立写端。默认借用 IO,关闭会话不关闭原始 IO;
221
+ `own: true` 转移关闭责任,初始化失败也会释放接管的 IO。`StringIO` 可以用作日志和监听器,不能用作读取会话。
177
222
 
178
- 用于写入或转接的真实 IO 应在首次写入前设置 `io.sync = true`,并由调用方保证没有未刷新的 Ruby 写缓冲;`write_nonblock` 可能先阻塞刷新已有缓冲,这一步不受本库的 IO 等待期限控制。已有缓冲应在交付给本库前由调用方排空,库不会绕过缓冲或改变字节顺序。
223
+ 用于写入或转接的真实 IO 应在首次写入前设置 `io.sync = true`,并由调用方保证没有未刷新的 Ruby 写缓冲;`write_nonblock`
224
+ 可能先阻塞刷新已有缓冲,这一步不受本库的 IO 等待期限控制。已有缓冲应在交付给本库前由调用方排空,库不会绕过缓冲或改变字节顺序。
179
225
 
180
- `readable_sessions` 返回可读的会话对象数组,不消费数据、不包含已关闭会话、同一会话只返回一次。默认 `timeout: 0`;`nil` 无限等待。同一会话应由一个读取者驱动,多会话共同监听使用 `Expect.expect`。
226
+ `readable_sessions` 返回可读的会话对象数组,不消费数据、不包含已关闭会话、同一会话只返回一次。默认 `timeout: 0`;`nil`
227
+ 无限等待。同一会话应由一个读取者驱动,多会话共同监听使用 `Expect.expect`。
181
228
 
182
- | API | 行为 |
183
- | --- | --- |
184
- | `write(*objects)` | 通过 `to_s` 转换并写入所有字节,返回字节数 |
185
- | `puts(*objects)` | 原生 IO 风格的换行、数组递归和 `nil` 返回值 |
186
- | `session << object` | 写入并返回会话,可链式追加 |
187
- | `send_slow(*objects, delay:)` | 每个字符之前等待指定秒数,同时收集返回数据 |
188
- | `buffer` / `buffer=` | 获取副本 / 复制字节并应用上限 |
189
- | `clear_buffer` | 清空缓冲并返回旧内容 |
190
- | `stty("raw -echo")` / `stty` | 修改终端模式 / 获取可恢复的模式字符串 |
191
- | `winsize` / `winsize=` | 读取/修改 `[rows, cols]`,由内核通知前台进程 |
192
- | `slave` / `tty_name` / `to_io` / `writer` / `fileno` / `tty?` | 底层 IO 和终端信息 |
229
+ | API | 行为 |
230
+ |---------------------------------------------------------------|----------------------------------------------|
231
+ | `write(*objects)` | 通过 `to_s` 转换并写入所有字节,返回字节数 |
232
+ | `puts(*objects)` | 原生 IO 风格的换行、数组递归和 `nil` 返回值 |
233
+ | `session << object` | 写入并返回会话,可链式追加 |
234
+ | `send_slow(*objects, delay:)` | 每个字符之前等待指定秒数,同时收集返回数据 |
235
+ | `buffer` / `buffer=` | 获取副本 / 复制字节并应用上限 |
236
+ | `clear_buffer` | 清空缓冲并返回旧内容 |
237
+ | `stty("raw -echo")` / `stty` | 修改终端模式 / 获取可恢复的模式字符串 |
238
+ | `winsize` / `winsize=` | 读取/修改 `[rows, cols]`,由内核通知前台进程 |
239
+ | `slave` / `tty_name` / `to_io` / `writer` / `fileno` / `tty?` | 底层 IO 和终端信息 |
193
240
 
194
- `send_slow` 在每次写入后只检查已经可读的回复,不附加固定等待;返回时不保证收齐最后一个字符引发的回复,完整对话请继续使用 `expect`。
241
+ `send_slow` 在每次写入后只检查已经可读的回复,不附加固定等待;返回时不保证收齐最后一个字符引发的回复,完整对话请继续使用
242
+ `expect`。
195
243
 
196
- 大块写入遇到背压时同时读取输出,避免双向传输互相阻塞。超过 `write_timeout` 抛出 `Expect::WriteTimeout`,`error.bytes_written` 给出本次 `write` 已被底层接受的字节数;这些字节不回滚,不要从头重发整个命令。写入、等待和背压读取中的 `EINTR` 均保留原期限重试。控制字符可直接发送,例如 `session.write("\x03")`,其信号作用取决于终端设置。`send`、`public_send`、`__send__` 保留 Ruby 反射语义。
244
+ 大块写入遇到背压时同时读取输出,避免双向传输互相阻塞。超过 `write_timeout` 抛出 `Expect::WriteTimeout`,
245
+ `error.bytes_written` 给出本次 `write` 已被底层接受的字节数;这些字节不回滚,不要从头重发整个命令。写入、等待和背压读取中的
246
+ `EINTR` 均保留原期限重试。控制字符可直接发送,例如 `session.write("\x03")`,其信号作用取决于终端设置。`send`、`public_send`、
247
+ `__send__` 保留 Ruby 反射语义。
197
248
 
198
- `stty` 需要系统命令位于 `PATH`;缺失时抛出带安装提示的 `IOError`,原始 `Errno::ENOENT` 保留在 `cause`。窗口尺寸和人工接管的终端恢复使用 Ruby `io/console`。
249
+ `stty` 需要系统命令位于 `PATH`;缺失时抛出带安装提示的 `IOError`,原始 `Errno::ENOENT` 保留在 `cause`。窗口尺寸和人工接管的终端恢复使用
250
+ Ruby `io/console`。
199
251
 
200
252
  ## 日志与人工交互
201
253
 
@@ -210,11 +262,50 @@ session.listeners = [output_io, another_session]
210
262
  session.log_listeners = false
211
263
  ```
212
264
 
213
- 日志读取用 `log_output`,设置用 `log_output=`,打开路径或注册日志块用 `log_to`。新建日志权限为 `0600`(仍受 umask 限制),已有文件保留原权限。不能同时提供日志目标与块。`listeners` 返回列表副本,`listeners = []` 清空;替换无效目标不会丢失原目标。
265
+ 日志读取用 `log_output`,设置用 `log_output=`,打开路径或注册日志块用 `log_to`。新建日志权限为 `0600`(仍受 umask
266
+ 限制),已有文件保留原权限。不能同时提供日志目标与块。`listeners` 返回列表副本,`listeners = []` 清空;替换无效目标不会丢失原目标。
214
267
 
215
268
  所有会话默认不输出到 stdout。日志仅记录实际读取的接收字节;写入不重复记录,终端回显可能作为接收内容返回。密码交互应关闭日志、调试,并确保被控程序不回显密码。
216
269
 
217
- 普通 `expect` 按顺序同步写入日志、stdout 和 `listeners`,这些目标须及时消费数据;匹配的 `timeout` 不会中断阻塞中的输出。需要在慢目标背压时继续处理其他输入,应使用下面的 `interconnect` 非阻塞转接接口并设置期限。
270
+ 普通 `expect` 按顺序同步写入日志、stdout 和 `listeners`,这些目标须及时消费数据;匹配的 `timeout`
271
+ 不会中断阻塞中的输出。需要在慢目标背压时继续处理其他输入,应使用下面的 `interconnect` 非阻塞转接接口并设置期限。
272
+
273
+ 诊断与接收字节日志分别设置。`diagnostic_output:` 可作为构造参数,也可通过同名属性修改;接受标准 Logger、可写目标或回调,`nil`
274
+ 使用 stderr。目标均为借用资源,关闭会话不会关闭它们。`debug_level` 仍控制内容:1 为生命周期和匹配,2 增加收发字节,3
275
+ 增加缓冲。Logger 的级别分别使用 `info` 和 `debug`;回调收到冻结的 Hash,包含 `event`、`level`、`pid`、`fd`、`message`,不包含会话对象。
276
+
277
+ ```ruby
278
+ require "logger" # 使用 Logger 的应用需在自己的 Gemfile 声明 logger
279
+ session.diagnostic_output = Logger.new($stderr)
280
+ session.debug_level = 2
281
+ session.redact(password, token) # 在首次通信前注册需要保护的原始字节
282
+ session.log_to("session.log")
283
+ ```
284
+
285
+ `redact` 复制并追加本会话的非空字符串秘密,以 `[FILTERED]` 遮盖 `log_to` / `write_log`
286
+ 及诊断中的对应字节,支持跨读取、跨写入和重叠秘密;匹配缓冲、结果、stdout 显示和 listeners 的转发仍是原始字节。应继续关闭敏感会话的
287
+ `log_stdout`,并自行保护交互显示及协议转发目标。它不推断编码、终端转义、哈希或其他变换后的秘密,也不能删除已输出的日志。
288
+
289
+ 过滤器最多延迟最长秘密长度减一的尾部字节,EOF、目标替换和显式关闭时交付剩余内容;流边界处疑似秘密前缀也会被遮盖。发送与接收诊断各自保留过滤状态;启用脱敏时,level
290
+ 3 的缓冲快照只显示 `[FILTERED]`,避免部分消费或裁剪后剩下的秘密片段绕过过滤。日志回调的分块边界因此可能变化。GC
291
+ 兜底不会调用用户日志回调,需显式关闭会话以交付过滤器尾部。
292
+
293
+ 应用需要过滤自己的日志或错误文本时,可以直接使用独立的字节过滤器,无需打开 PTY 或创建会话:
294
+
295
+ ```ruby
296
+ require "expect/redactor"
297
+
298
+ safe_message = Expect::Redactor.redact(message, [password], replacement: "[REDACTED]")
299
+ filter = Expect::Redactor.new([password], replacement: "[REDACTED]")
300
+ output.write(filter.append(chunk)) # 每个输出流使用独立实例
301
+ output.write(filter.finish)
302
+ ```
303
+
304
+ `patterns=` 用由非空字符串组成的数组替换后续规则,空数组表示不注册秘密;输入数组、字符串和替换标记都会复制,
305
+ 无效更新不改变已有规则或暂存数据。已输出的内容不能撤回,更新后也会保留先前标记为隐藏的尾部区间。
306
+ `redact` 处理完整文本,只匹配完整秘密;`finish` 默认还隐藏未完成的秘密前缀。已确定输入完整的调用方可用
307
+ `finish(partial: false)`。连续或重叠的隐藏区间合并成一个替换标记。过滤不自动识别终端控制符或编码,过滤器本身不持有 IO,
308
+ 也不负责会话作用域或异常对象的安全字段选择。该公共接口从 0.5.0 开始提供。
218
309
 
219
310
  ```ruby
220
311
  session.interact(input: $stdin, escape: "\x1d", output: $stdout) # Ctrl-]
@@ -227,19 +318,31 @@ Expect.open($stdin) do |input|
227
318
  end
228
319
  ```
229
320
 
230
- `on_sequence(sequence) { ... }` 注册字符串、原生正则或 `:eof`,通过闭包传递参数。无回调、返回 `nil` / `false` 停止,其他 Ruby 真值(包括 `0`)继续;字符串 `"EOF"` 按字面匹配。转接返回导致停止的会话,超时或所有 EOF 回调均继续时返回 `nil`。
321
+ `on_sequence(sequence) { ... }` 注册字符串、原生正则或 `:eof`,通过闭包传递参数。无回调、返回 `nil` / `false` 停止,其他 Ruby
322
+ 真值(包括 `0`)继续;字符串 `"EOF"` 按字面匹配。转接返回导致停止的会话,超时或所有 EOF 回调均继续时返回 `nil`。
231
323
 
232
- `interconnect` 统一调度真实 IO 的非阻塞读写;慢目标不会阻止其他源前进,等待同时受总 `timeout` 和目标会话的 `write_timeout` 约束。总期限到达返回 `nil`,目标写期限先到则抛出 `WriteTimeout`。超时后的字面转义前缀只尝试非阻塞发送,不再等待下游。作为写入目标但未显式列出的 Expect 会话,背压期间读取的回复保留在其匹配缓冲;需要同时转发这些回复时,把它也传给 `interconnect`。
324
+ `interconnect` 统一调度真实 IO 的非阻塞读写;慢目标不会阻止其他源前进,等待同时受总 `timeout` 和目标会话的 `write_timeout`
325
+ 约束。总期限到达返回 `nil`,目标写期限先到则抛出 `WriteTimeout`。超时后的字面转义前缀只尝试非阻塞发送,不再等待下游。作为写入目标但未显式列出的
326
+ Expect 会话,背压期间读取的回复保留在其匹配缓冲;需要同时转发这些回复时,把它也传给 `interconnect`。
233
327
 
234
- 每个源独立保存待发送数据以及各目标的发送位置,`source.pending_output?` 表示仍有未交付内容。超时或异常后再次对同一源调用 `interconnect`,会接着发送未完成的后缀,已完成的目标不会重复接收;转义回调在前缀交付后执行。待发送数据与 `buffer` 中尚未处理的输入分开保存,修改 `listeners` 仅影响后续数据,旧数据仍发往原目标。恢复时不要把原始数据再次赋给 `buffer`,也不要在排空旧输出前插入新的直接写入;关闭源会话会放弃其待发送数据。转接保留的输入暂不按 `buffer_limit` 裁剪,下次匹配时重新应用该上限。
328
+ 每个源独立保存待发送数据以及各目标的发送位置,`source.pending_output?` 表示仍有未交付内容。超时或异常后再次对同一源调用
329
+ `interconnect`,会接着发送未完成的后缀,已完成的目标不会重复接收;转义回调在前缀交付后执行。待发送数据与 `buffer`
330
+ 中尚未处理的输入分开保存,修改 `listeners` 仅影响后续数据,旧数据仍发往原目标。恢复时不要把原始数据再次赋给 `buffer`
331
+ ,也不要在排空旧输出前插入新的直接写入;关闭源会话会放弃其待发送数据。转接保留的输入暂不按 `buffer_limit` 裁剪,下次匹配时重新应用该上限。
235
332
 
236
- 自定义写入对象必须及时返回实际接受的字节数,短写入会继续发送后缀,零、负数或非法返回值抛出 `IOError`。对象若先写入再抛错而不报告进度,库无法推断其副作用。日志、用户回调及自定义 `write` / `flush` 同步运行,应由调用方保证它们不会无限阻塞;上述 IO 期限不会强行中断这些代码。普通 `expect` 的同步日志和监听器输出也不受匹配等待期限限制。
333
+ 自定义写入对象必须及时返回实际接受的字节数,短写入会继续发送后缀,零、负数或非法返回值抛出 `IOError`
334
+ 。对象若先写入再抛错而不报告进度,库无法推断其副作用。日志、用户回调及自定义 `write` / `flush` 同步运行,应由调用方保证它们不会无限阻塞;上述
335
+ IO 期限不会强行中断这些代码。普通 `expect` 的同步日志和监听器输出也不受匹配等待期限限制。
237
336
 
238
- 字面转义可以跨读取完整过滤,尾部留给下次调用。正则转义使用历史记录,默认最多保留最近 65,536 字节;设置 `buffer_limit` 后改用该值。正则及其锚点作用于当前历史窗口,超过窗口的跨读取正则无法匹配,已实时转发的前缀也无法撤回;零长度正则匹配抛出 `ArgumentError`。日志始终记录原始接收字节,包括被过滤的转义,在 `expect` / `interconnect` 之间切换也不会重复记录。
337
+ 字面转义可以跨读取完整过滤,尾部留给下次调用。正则转义使用历史记录,默认最多保留最近 65,536 字节;设置 `buffer_limit`
338
+ 后改用该值。正则及其锚点作用于当前历史窗口,超过窗口的跨读取正则无法匹配,已实时转发的前缀也无法撤回;零长度正则匹配抛出
339
+ `ArgumentError`。日志包括被转接过滤的转义,显式启用 `redact` 时遮盖注册秘密;在 `expect` / `interconnect` 之间切换不会重复记录。
239
340
 
240
- `interact` 会自动设置并恢复本地输入终端模式,同时保留输出换行处理;输入会话的 `raw_terminal = false` 将设置交给调用方。通用的 `interconnect` 只负责字节转发,由调用方管理终端模式。`interact` 还会恢复临时监听组、日志开关和转义设置,包括超时和异常路径。
341
+ `interact` 会自动设置并恢复本地输入终端模式,同时保留输出换行处理;输入会话的 `raw_terminal = false` 将设置交给调用方。通用的
342
+ `interconnect` 只负责字节转发,由调用方管理终端模式。`interact` 还会恢复临时监听组、日志开关和转义设置,包括超时和异常路径。
241
343
 
242
- 对同一连接重复传入同一个原始输入 IO 时,`interact` 会复用输入包装器,接续上次预读的尾部。包装器由该连接持有,关闭连接时释放,但不关闭借用的原始 IO;已关闭的输入或包装器不再复用。需要跨连接共享或自行管理输入生命周期时,显式传入 `Expect.open(input)` 创建的会话。
344
+ 对同一连接重复传入同一个原始输入 IO 时,`interact` 会复用输入包装器,接续上次预读的尾部。包装器由该连接持有,关闭连接时释放,但不关闭借用的原始
345
+ IO;已关闭的输入或包装器不再复用。需要跨连接共享或自行管理输入生命周期时,显式传入 `Expect.open(input)` 创建的会话。
243
346
 
244
347
  ## 软关闭、硬关闭与进程状态
245
348
 
@@ -250,21 +353,30 @@ status ||= session.hard_close(timeout: 0.2)
250
353
  session.close(graceful: true) # 先软关闭,必要时继续硬关闭
251
354
  ```
252
355
 
253
- - `soft_close`:等待自然 EOF 并收集尾部输出,然后关闭所属 IO、等待进程退出;超时后最多发送 TERM,**不发送 KILL**。`timeout:` 是自然退出阶段的期限(默认 15 秒),`term_timeout:` 是发 TERM 后的等待时间(默认 1 秒)。未退出返回 `nil`,保留 PID,可继续 `wait` 或 `hard_close`。
254
- - `hard_close`:立即关闭所属 IO,不收集尾部输出;等待 `timeout:`,必要时发送 TERM 再等待同样时长,仍未退出则 KILL 并最多等待 1 秒。默认 `timeout: 0.2`,必须有限。
356
+ - `soft_close`:等待自然 EOF 并收集尾部输出,然后关闭所属 IO、等待进程退出;超时后最多发送 TERM, **不发送 KILL**。`timeout:`
357
+ 是自然退出阶段的期限(默认 15 秒),`term_timeout:` 是发 TERM 后的等待时间(默认 1 秒)。未退出返回 `nil`,保留 PID,可继续
358
+ `wait` 或 `hard_close`。
359
+ - `hard_close`:立即关闭所属 IO,不收集尾部输出;等待 `timeout:`,必要时发送 TERM 再等待同样时长,仍未退出则 KILL 并最多等待
360
+ 1 秒。默认 `timeout: 0.2`,必须有限。
255
361
  - 两者返回已回收的 `Process::Status`,没有子进程或尚未回收时返回 `nil`;重复调用保留已获得的状态。借用 IO 不关闭。
256
362
  - `close(graceful: graceful_close?)`:可选先软关闭,`ensure` 中硬关闭,返回 `nil`。块生命周期使用它完成清理;软关闭发生日志异常时也会回收子进程。
257
- - `wait(timeout: nil)`:等待并回收,返回 `Process::Status`;超时返回 `nil`。`process_status` 非阻塞查询,`exit_code` 读取普通退出码,信号退出看 `process_status.termsig`。
258
- - `closed?` 表示会话 IO 已关闭;`alive?` / `pid` 表示子进程状态。软关闭后可能同时 `closed? == true`、`alive? == true`。成功回收后 PID 为 `nil`。
363
+ - `wait(timeout: nil)`:等待并回收,返回 `Process::Status`;超时返回 `nil`。`process_status` 非阻塞查询,`exit_code`
364
+ 读取普通退出码,信号退出看 `process_status.termsig`。
365
+ - `closed?` 表示会话 IO 已关闭;`alive?` / `pid` 表示子进程状态。软关闭后可能同时 `closed? == true`、`alive? == true`。成功回收后
366
+ PID 为 `nil`。
259
367
 
260
368
  关闭只负责会话直接启动的子进程;垃圾回收提供非阻塞的强制清理兜底,不执行软关闭等待。优先使用块或 `ensure` 管理资源。
261
369
 
262
370
  ## 安全注意事项
263
371
 
264
- - 将不可信命令和参数分别传给 `spawn`,例如 `Expect.spawn("ssh", host)`;单个命令字符串会使用 Ruby 的 shell 语义。`stty` 参数经拆分后作为独立参数传给进程,不拼接 shell 命令。
265
- - 会话日志可能记录密码回显、令牌和其他敏感字节。新日志文件以 `0600` 创建,已有文件保留原权限;请按需关闭 `log_to`、`log_stdout` 和调试输出,并管理日志留存。
266
- - `spawn` 在子进程中使用 `fork` 后的 Ruby 操作与 `exec`。高度多线程的宿主进程,尤其使用第三方 C 扩展时,可能受到 fork 时其他线程持锁的影响;尽量在启动其他线程前创建会话,并在自己的运行环境中验证。
267
- - 不可信正则可能耗费较长时间;使用带 `timeout:` 的 `Regexp` 实例,并为匹配缓冲设置合适的 `buffer_limit`。普通 `expect` 的 IO 期限不打断单次正则或同步回调。
372
+ - 将不可信命令和参数分别传给 `spawn`,例如 `Expect.spawn("ssh", host)`;单个命令字符串会使用 Ruby 的 shell 语义。`stty`
373
+ 参数经拆分后作为独立参数传给进程,不拼接 shell 命令。
374
+ - 会话日志可能记录密码回显、令牌和其他敏感字节。新日志文件以 `0600` 创建,已有文件保留原权限;请按需关闭 `log_to`、
375
+ `log_stdout` 和调试输出,并管理日志留存。
376
+ - `spawn` 在子进程中使用 `fork` 后的 Ruby 操作与 `exec`。高度多线程的宿主进程,尤其使用第三方 C 扩展时,可能受到 fork
377
+ 时其他线程持锁的影响;尽量在启动其他线程前创建会话,并在自己的运行环境中验证。
378
+ - 不可信正则可能耗费较长时间;使用带 `timeout:` 的 `Regexp` 实例,并为匹配缓冲设置合适的 `buffer_limit`。普通 `expect` 的
379
+ IO 期限不打断单次正则或同步回调。
268
380
 
269
381
  ## 示例和验证
270
382
 
@@ -279,28 +391,41 @@ ruby examples/ssh_auto.rb --no-interact
279
391
  ruby examples/ssh_interact.rb --auto
280
392
  ```
281
393
 
282
- 普通测试使用真实 PTY、管道和 socket,无需 SSH 服务或账户。Kibitz 的双终端示例与验证见 [examples/kibitz/](examples/kibitz/README.md)。
394
+ 普通测试使用真实 PTY、管道和 socket,无需 SSH 服务或账户。Kibitz
395
+ 的双终端示例与验证见 [examples/kibitz/](examples/kibitz/README.md)。
283
396
 
284
- [GitHub Actions](https://github.com/gatework/expect-ruby/actions/workflows/ci.yml) 在推送 `main`、推送 `v*` 标签、提交到 `main` 的 Pull Request 或手动触发时运行。流水线覆盖 Ubuntu 24.04 / macOS 15 与 Ruby 3.2、3.3、3.4、4.0 的 8 种组合;每个环境执行 `script/ci`,包括真实 PTY 测试和构建包的隔离安装验证。Ubuntu / Ruby 4.0 作业保留已验证的 Gem 构建产物 14 天,可从该次工作流的 Artifacts 下载。
397
+ [GitHub Actions](https://github.com/gatework/expect-ruby/actions/workflows/ci.yml) 在推送 `main`、推送 `v*` 标签、提交到
398
+ `main` 的 Pull Request 或手动触发时运行。流水线覆盖 Ubuntu 24.04 / macOS 15 与 Ruby 3.2、3.3、3.4、4.0 的 8 种组合;每个环境执行
399
+ `script/ci`,包括真实 PTY 测试和构建包的隔离安装验证。Ubuntu / Ruby 4.0 作业保留已验证的 Gem 构建产物 14 天,可从该次工作流的
400
+ Artifacts 下载。
285
401
 
286
- 运行前先执行 `bundle install`。Gem 库的开发锁文件 `Gemfile.lock` 保留在本地,各 Ruby 环境按 `Gemfile` 解析兼容依赖。生成文件写入已忽略的 `tmp/`,Gem 构建产物位于 `tmp/ci/`,发布候选包位于 `tmp/release/`。安装验证会清除外部 Bundler 环境,分别运行普通 RubyGems 加载和只声明 `expect-pty` 的 Bundler 应用,检查运行时依赖、终端模式、窗口大小及真实 PTY 对话。更新工作流中的 Action 时,应同步更新固定的提交 SHA 和版本注释。
402
+ 运行前先执行 `bundle install`。Gem 库的开发锁文件 `Gemfile.lock` 保留在本地,各 Ruby 环境按 `Gemfile` 解析兼容依赖。生成文件写入已忽略的
403
+ `tmp/`,Gem 构建产物位于 `tmp/ci/`,发布候选包位于 `tmp/release/`。安装验证会清除外部 Bundler 环境,分别运行普通 RubyGems
404
+ 加载和只声明 `expect-pty` 的 Bundler 应用,检查运行时依赖、终端模式、窗口大小及真实 PTY 对话。更新工作流中的 Action
405
+ 时,应同步更新固定的提交 SHA 和版本注释。
287
406
 
288
- 运行时依赖只在 `expect-pty.gemspec` 声明,开发依赖放在 Gemfile 的 `development` / `test` 组;安装或使用本库不会引入 Minitest、Rake、RuboCop 及发布工具的依赖。
407
+ 运行时依赖只在 `expect-pty.gemspec` 声明,开发依赖放在 Gemfile 的 `development` / `test` 组;安装或使用本库不会引入
408
+ Minitest、Rake、RuboCop 及发布工具的依赖。
289
409
 
290
- | 运行时模块 | Gem | 用途 |
291
- | --- | --- | --- |
292
- | `forwardable` | `forwardable` | 会话配置委托 |
293
- | `io/console` | `io-console` | 终端模式和窗口大小 |
294
- | `io/wait` | `io-wait` | IO 可读等待 |
295
- | `shellwords` | `shellwords` | `stty` 参数拆分 |
296
- | `stringio` | `stringio` | Ruby `puts` 语义 |
297
- | `pty` | Ruby 自带扩展 | POSIX 伪终端,无独立 gem |
410
+ | 运行时模块 | Gem | 用途 |
411
+ |---------------|---------------|--------------------------|
412
+ | `forwardable` | `forwardable` | 会话配置委托 |
413
+ | `io/console` | `io-console` | 终端模式和窗口大小 |
414
+ | `io/wait` | `io-wait` | IO 可读等待 |
415
+ | `shellwords` | `shellwords` | `stty` 参数拆分 |
416
+ | `stringio` | `stringio` | Ruby `puts` 语义 |
417
+ | `pty` | Ruby 自带扩展 | POSIX 伪终端,无独立 gem |
298
418
 
299
- 仅发布 RubyGems 使用 `ruby script/release.rb --rubygems-only`,直接复用本机已有的 Gem 登录状态;添加 `--dry-run` 可先完成本地检查、测试、构建和安装验证。需要同时创建 GitHub Release 时使用 `ruby script/release.rb`,也可以在 GitHub Actions 手动运行 Release 工作流。版本准备、Actions 凭据和失败重试见 [发布说明](docs/RELEASING.md)。
419
+ 仅发布 RubyGems 使用 `ruby script/release.rb --rubygems-only`,直接复用本机已有的 Gem 登录状态;添加 `--dry-run`
420
+ 可先完成本地检查、测试、构建和安装验证。需要同时创建 GitHub Release 时使用 `ruby script/release.rb`,也可以在 GitHub Actions
421
+ 手动运行 Release 工作流。版本准备、Actions 凭据和失败重试见 [发布说明](docs/RELEASING.md)。
300
422
 
301
- SSH 示例用 `SSH_USER`、`SSH_HOST`、`SSH_KNOWN_HOSTS` 配置,密码隐藏输入或从 `EXPECT_PASSWORD` 读取;非本地主机要求受信任的 known_hosts 文件。`ssh_auto.rb` 顶部 `COMMANDS` 可直接修改,日志写入 `tmp/ssh-auto/`,权限 0600。
423
+ SSH 示例用 `SSH_USER`、`SSH_HOST`、`SSH_KNOWN_HOSTS` 配置,密码隐藏输入或从 `EXPECT_PASSWORD` 读取;非本地主机要求受信任的
424
+ known_hosts 文件。`ssh_auto.rb` 顶部 `COMMANDS` 可直接修改,日志写入 `tmp/ssh-auto/`,权限 0600。
302
425
 
303
- 多脚本验证入口为 `test/integration/ssh_scripts.rb`,人工/自动接管入口为 `examples/ssh_interact.rb`,详细配置及日志检查见 [SSH 测试说明](test/integration/README.md)。
426
+ 多脚本验证入口为 `test/integration/ssh_scripts.rb`,人工/自动接管入口为 `examples/ssh_interact.rb`
427
+ ,详细配置及日志检查见 [SSH 测试说明](test/integration/README.md)。
304
428
 
305
- 当前接口迁移表见 [接口说明](docs/COMPATIBILITY.md),本次与历史验证分列在 [验证记录](docs/VERIFICATION.md)。此次重构直接移除了旧入口,不提供兼容别名。
429
+ 当前接口迁移表见 [接口说明](docs/COMPATIBILITY.md),本次与历史验证分列在 [验证记录](docs/VERIFICATION.md)
430
+ 。此次重构直接移除了旧入口,不提供兼容别名。
306
431
  防火墙连接器已迁移到相邻的 `algosec` 项目;本库只保留 `Expect` 与 `expect-pty` 通用传输能力。
@@ -34,6 +34,32 @@ begin
34
34
  runner.measure("utf8/#{size}", bytes: session.buffer.bytesize, inputs: { prefix_bytes: prefix.bytesize },
35
35
  verify: verify) { matcher.__send__(:find_match) }
36
36
  end
37
+
38
+ # 同一次等待不断追加新字节,分别对照字面与正则;跨块命中仍按声明优先级选择。
39
+ (runner.smoke ? [4096] : [65_536, 1_048_576]).product(%i[literal regexp]).each do |size, kind|
40
+ chunks = runner.smoke ? 4 : 32
41
+ patterns = Array.new(32) { |index| "missing#{index}" }
42
+ patterns[-1] = "END"
43
+ patterns.map! { |value| Regexp.new(Regexp.escape(value)) } if kind == :regexp
44
+ verify = lambda do |result|
45
+ ExpectBenchmark.check(result && result[1].number == 32 && result[2] == [size + (chunks * 1024), 3, []])
46
+ end
47
+ runner.measure("stream/#{size}/#{kind}", bytes: size + (chunks * 1024) + 3,
48
+ inputs: { initial_bytes: size, chunks: chunks, patterns: 32, kind: kind },
49
+ iterations: 5, verify: verify) do
50
+ session.buffer = "x" * size
51
+ matcher = Expect::Matcher.new(Expect::PatternList.new([session], patterns), nil)
52
+ ExpectBenchmark.check(matcher.__send__(:find_match).nil?)
53
+ chunks.times do
54
+ writer.write("x" * 1024)
55
+ session.__send__(:read_available)
56
+ ExpectBenchmark.check(matcher.__send__(:find_match).nil?)
57
+ end
58
+ writer.write("END")
59
+ session.__send__(:read_available)
60
+ matcher.__send__(:find_match)
61
+ end
62
+ end
37
63
  ensure
38
64
  session.close
39
65
  reader.close