expect-pty 0.6.1 → 0.7.2

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: f26cf6f3329f5fce5c415a68148b495d144c3339b4c4e9bf48509785c70c5628
4
- data.tar.gz: f563051727dad3f619aac32a69535bb6a197b0313bfd5bbfd38bde4763625873
3
+ metadata.gz: 18507dadf6e9120856677f8958b1d01f8de6ead6702a0aabe3afdc21bd8600f2
4
+ data.tar.gz: 43be430fc418a34753c60eeb886984a30896d01bb714d40f9b3f9f12c671b126
5
5
  SHA512:
6
- metadata.gz: cc6300944319d2e472719cd21e8b921086fbc38e4514a9767407dd684c16f86a069e3776e3c19d19fe3dbd0fa614a4adcb25ca54ad4fa74c1e855a71c1bff464
7
- data.tar.gz: 66bd73096c3b70adf6034f975ce162f8f6abac511e65005500b2f2a086b256528e626f9523ff5111a8c2882459913da8f921c7eb2b7674288ea6d2aaeb614389
6
+ metadata.gz: bdd8a301e1473998ca4fef72d4801ea26df9285791459574e1016f279bad2648661d85680bda7a42486849d99432efa291da27a28fe3af17f8d0f6c36d4ab167
7
+ data.tar.gz: d05ac12ae3fb4cf94fb1c15f89d0df95904e278cc812bb8f9fdcc8160ab729eeb2a16c8abc3831262b7b3187f68b63385693076e206f47928b5cbb5bf273663e
data/CHANGELOG.md CHANGED
@@ -2,13 +2,42 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.2 - 2026-10-05
6
+
7
+ - **可信发布**:版本标签触发 GitHub Actions,通过 RubyGems OIDC 短期凭据发布,移除静态发布 Secret 依赖。
8
+ - **产物验证**:保留四环境 CI、原包发布与提交内容、GitHub/RubyGems SHA256 回读校验;同步配置和恢复说明。
9
+
10
+ ## 0.7.1 - 2026-10-01
11
+
12
+ - **会话接口(不兼容 0.6.x)**:本次发布包含 0.7.0 的显式 `Expect::Session`、操作关键字及标准 Logger 接口;
13
+ 删除全局 Configuration、委托门面和旧终端包装,升级前请阅读 `docs/MIGRATION.md`。
14
+ - **启动清理**:启动线程在 fork 返回后被中断时,先登记子进程归属,再沿用现有清理流程,避免遗留未回收进程。
15
+ - **回调控制**:超时与 EOF 回调只识别约定的继续控制符,自定义对象的相等运算不再触发重复回调或重置期限。
16
+ - **交互参数**:`interact` 仅把 nil 当作缺省输出或转义,显式传入 false 会报错,避免误用默认输出或静默忽略转义。
17
+ - **代码约定**:内部谓词统一返回布尔值,继续控制符比较不再分配临时数组,并同步 API 检查器命名与贡献文档中的验证入口。
18
+ - **模块注释**:补齐日志与转接宿主协议、资源账本、EOF 与写入游标的职责说明,明确借用对象及失败后的状态归属。
19
+
20
+ ## 0.7.0 - 2026-10-01
21
+
22
+ - **公开会话**:`Expect` 改为工厂模块,`Expect::Session` 直接承载会话、回调和结果来源,删除重复委托门面和全局 Configuration。
23
+ - **显式选项**:匹配使用 `consume:`、`reset_timeout_on_read:`,终端使用 `raw:`,关闭使用 `graceful:`;删除对应的常驻布尔开关及过渡接口。
24
+ - **标准日志**:诊断注入标准 Logger,统一级别与格式化协议;原始记录使用借用的 `transcript` writer,文件创建和关闭交给
25
+ `File.open`,流式秘密脱敏继续保留。
26
+ - **输出与终端**:转发目标统一为 `outputs`;移除外部 `stty` 包装,终端模式与窗口大小直接使用 `io-console`。删除无引用的
27
+ forwardable 运行依赖,shellwords 仅用于开发示例。
28
+ - **转接实现**:合并为唯一的排队发送路径,明确待写游标与回调状态;基准改为运行实际 Relay 循环。
29
+ - **清理异常**:已有主异常时,transcript writer 或 Logger formatter 的 StandardError
30
+ 不再覆盖它;没有主异常时仍传播清理错误,清理中断不被吞掉,所属子进程正常回收。
31
+ - **交付接口**:同步 RBS、API 文档、示例、安装验证和 0.6.x 迁移说明;API 检查使用方法的文档标注区分内部协议,避免手工维护方法白名单。
32
+
5
33
  ## 0.6.1 - 2026-09-30
6
34
 
7
35
  - **错误归属**:多来源等待在 EOF 后继续时,后续 IO 选择错误只更新仍活跃的来源,保留已结束来源的 EOF 与尾部结果。
8
36
  - **写入进度**:发送前对象转换或诊断中的嵌套写入超时,对当前命令报告零进度,并通过 `cause` 保留嵌套错误的真实进度。
9
37
  - **调度性能**:已处理 EOF 使用身份集合,批量转接就绪使用一次性 IO 身份索引,减少多来源场景的重复检索;保留事件、读取和回调顺序。
10
38
  - **脱敏性能**:流结束时先排除不可能匹配的秘密前缀长度,减少长尾未命中的扫描和分配;保留部分秘密、重叠及二进制语义。
11
- - **进程归属**:父进程创建 PTY、fork 后才启动命令时,按实际启动者登记子进程归属,保证等待与关闭能回收该子进程;继承已有 PID 的副本仍不接管它。
39
+ - **进程归属**:父进程创建 PTY、fork 后才启动命令时,按实际启动者登记子进程归属,保证等待与关闭能回收该子进程;继承已有 PID
40
+ 的副本仍不接管它。
12
41
  - **启动清理**:错误管道关闭失败仍尝试另一端,清理错误不再覆盖原始 fork 错误或 `SpawnError`。
13
42
  - **来源身份**:匹配分组、EOF 派发、就绪查询与转接按会话和 IO 对象身份处理;自定义值相等规则不再合并独立来源或遗漏可写目标。
14
43
  - **转义恢复**:超时交付的字面转义前缀也进入正则历史,混合规则跨次转接时不会漏掉完整退出序列。
@@ -20,7 +49,8 @@
20
49
  - 布尔配置仅保留 predicate 与 setter;模式声明在匹配前冻结,内部 Pattern 使用私有 Data。
21
50
  - 修正 Result 构造及 callable 日志协议的 RBS,API 门禁覆盖模块方法和 Data 构造接口。
22
51
  - 明确单字符串命令的 Ruby shell 语义,迁移示例和检查脚本,移除 Perl 差分工具与过时接口迁移表。
23
- - **Ruby 现代化(破坏性)**:最低 Ruby 3.4;CI 使用 Ruby 3.4/4.0。`Result` 改为不可变 Data,复制冻结文本和捕获值,移除字段写入和 `to_a`,使用 `to_h` 或模式解构。
52
+ - **Ruby 现代化(破坏性)**:最低 Ruby 3.4;CI 使用 Ruby 3.4/4.0。`Result` 改为不可变 Data,复制冻结文本和捕获值,移除字段写入和
53
+ `to_a`,使用 `to_h` 或模式解构。
24
54
  - **职责重组**:用户门面委托内部 Session;日志、终端和交互成为职责模块,Matcher/Relay 直接调用内核协议;回调与结果来源仍为用户会话。
25
55
  - **清理可靠性**:统一始终/失败清理作用域,日志交接和终端恢复的常规清理错误不再覆盖原始异常;保留所属资源、PID 与转接恢复语义。
26
56
  - **工程门禁**:增加公开 API 的 YARD/RBS 与覆盖检查、README 版本校验;精简 Gem 为运行源码、类型签名及使用文档,开发材料保留在仓库。
@@ -56,7 +86,8 @@
56
86
  ## 0.4.0 - 2026-09-27
57
87
 
58
88
  - **缓冲与生命周期**:新增只读 `buffer_discarded_bytes`,区分上限裁剪与正常消费;补齐外部回收进程、借用 IO、重复关闭和失败重试的契约回归。
59
- - **等待总期限**:类和实例的 `expect` / `expect_result` 支持绝对单调时钟 `deadline:`,接收重置与继续回调不能延长总预算,回调后到期也保留未消费文本和已知 EOF 顺序。
89
+ - **等待总期限**:类和实例的 `expect` / `expect_result` 支持绝对单调时钟 `deadline:`,接收重置与继续回调不能延长总预算,回调后到期也保留未消费文本和已知
90
+ EOF 顺序。
60
91
  - **诊断与脱敏**:会话级 `diagnostic_output` 支持 Logger、IO 和结构化回调;`redact` 对日志及收发诊断按字节流过滤,覆盖分片、重叠和流尾部,不改写匹配或协议转发。
61
92
  - **匹配性能**:字面模式复用同一缓冲代次中已排除的前缀,消费、替换和裁剪后失效;正则保留完整窗口及原有声明优先级。
62
93
  - **验证与维护**:增加持续输出、同时就绪来源和阻塞目标基准,记录 RSS 端点和描述符计数;补齐底层模块中文注释与内部契约。
data/README.md CHANGED
@@ -7,7 +7,8 @@ IO、同时监听多个会话和转接人工交互。交互能力参考 [Expect.
7
7
  的属性、关键字参数和代码块。
8
8
 
9
9
  要求 **Ruby 3.4+、POSIX 系统(Linux/macOS)**。运行时仅使用 Ruby 标准库,其中可独立安装的 gem 已在 gemspec 中声明,由
10
- RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提供独立的 `Expect` 类,不修改标准库的 `IO#expect`。
10
+ RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提供独立的 `Expect` 模块和 `Expect::Session` 会话,不修改标准库的
11
+ `IO#expect`。
11
12
 
12
13
  源码中的解释性注释主要使用中文;欢迎用中文或英文提交 issue 和 PR,参与方式见 [贡献指南](CONTRIBUTING.md)。
13
14
 
@@ -16,7 +17,7 @@ RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提
16
17
  项目和仓库名为 `expect-ruby`,Gem 名为 `expect-pty`。在应用的 Gemfile 中添加以下内容,然后运行 `bundle install`:
17
18
 
18
19
  ```ruby
19
- gem "expect-pty", "~> 0.6.1", require: "expect/pty"
20
+ gem "expect-pty", "~> 0.7.2", require: "expect/pty"
20
21
  ```
21
22
 
22
23
  也可直接执行 `gem install expect-pty`。需要跟随开发分支时,可从 GitHub 安装:
@@ -29,8 +30,8 @@ gem "expect-pty", git: "https://github.com/gatework/expect-ruby.git", branch: "m
29
30
 
30
31
  ```sh
31
32
  mkdir -p tmp
32
- gem build expect-pty.gemspec --output tmp/expect-pty-0.6.1.gem
33
- gem install ./tmp/expect-pty-0.6.1.gem
33
+ gem build expect-pty.gemspec --output tmp/expect-pty-0.7.2.gem
34
+ gem install ./tmp/expect-pty-0.7.2.gem
34
35
  ```
35
36
 
36
37
  ```ruby
@@ -48,54 +49,43 @@ Expect.spawn("/bin/sh", "-i") do |shell|
48
49
  end
49
50
  ```
50
51
 
51
- 块返回其执行结果,退出时关闭会话并回收子进程,异常和 `break` 也执行清理。构造或启动失败时同样释放已创建的资源;清理中的 IO
52
- 错误不会替换正在传播的原始异常。无块形式需用 `ensure` 显式调用 `close`。`Expect.new` 可以先创建 PTY、设置 `slave.echo` /
53
- `slave.winsize`,然后调用实例的 `spawn`。
52
+ 块返回其执行结果,退出时关闭会话并回收子进程,异常和 `break` 也执行清理。构造或启动失败时同样释放已创建的资源;清理中的
53
+ `StandardError` 不替换本次作用域已有的主异常,没有主异常时仍传播;清理中新发生的 `Interrupt` / `SystemExit` 不吞掉。
54
+ 无块形式需用 `ensure` 显式调用 `close`。`Expect::Session.new` 可以先创建 PTY,通过 `slave.echo=` /
55
+ `slave.winsize=` 设置终端,然后调用 `session.spawn`。工厂返回、回调参数和 `Result#session` 都是同一个真实 Session。
54
56
 
55
57
  多个命令参数原样传给 Ruby `exec`;单个命令字符串使用 Ruby 的 shell 语义。不可信参数应使用独立参数形式。支持
56
58
  `env: { "NAME" => "value" }` 和 `chdir: "/path"`。同一会话只能启动一次,启动失败抛出 `Expect::SpawnError`。
57
59
 
58
- ## 配置与属性
60
+ ## 会话设置与操作参数
59
61
 
60
62
  ```ruby
61
- Expect.configure do |config|
62
- config.timeout = 10
63
- config.buffer_limit = 65_536
64
- config.graceful_close = true
65
- end
66
-
67
- Expect.configure(debug_level: 0) # 也可用关键字修改默认值
68
- Expect.configuration.timeout # 默认值快照,只读
69
-
70
- Expect.spawn("/bin/sh", "-i", timeout: 3) do |session|
63
+ logger = Logger.new($stderr, level: Logger::INFO)
64
+ Expect.spawn("/bin/sh", "-i", timeout: 3, buffer_limit: 65_536,
65
+ logger: logger, outputs: [$stdout]) do |session|
71
66
  session.timeout = 5
72
- session.log_stdout = true
73
- session.raw_pty? # 布尔属性用问号方法查询
74
67
  session.puts("exit")
75
68
  end
76
69
  ```
77
70
 
78
- `configure` 校验后发布冻结的配置对象;块异常不会发布部分修改。并发调用按顺序完成读改写,不会互相覆盖不同属性。配置块在锁内执行,应保持简短,不要在块内再次调用
79
- `configure`(会抛出 `ThreadError`)或等待其他配置线程。每个会话独立持有配置,优先使用构造参数;修改默认值不会改变已有会话,修改一个会话也不会影响其他会话。子类继承父类默认配置,可独立覆盖。
71
+ 所有设置显式传给会话,不提供全局默认配置或配置基类。需要应用默认值时,由调用方保存 Hash,再用关键字展开传入。
72
+ 各会话独立保存以下属性,修改不会影响其他会话;借用的 logger、transcript 和输出对象可以由调用方共享。
80
73
 
81
- 全局默认配置通常在应用启动时设置;每次会话的动态差异使用构造参数或会话属性,避免在高频路径反复获取共享配置锁。
74
+ | 会话属性 | 默认值 | 行为 |
75
+ |-----------------|--------|-------------------------------------------------------|
76
+ | `timeout` | `nil` | `session.expect` 的默认相对期限;`nil` 无限、`0` 轮询 |
77
+ | `write_timeout` | `nil` | 写入遇到背压或 EINTR 时的等待期限 |
78
+ | `buffer_limit` | `nil` | 匹配缓冲保留的尾部字节数;正整数或 `nil` |
79
+ | `logger` | `nil` | 借用支持 `add` / `debug?` 的诊断 logger |
80
+ | `transcript` | `nil` | 借用支持 `write` 的接收字节记录目标 |
81
+ | `outputs` | `[]` | 原始接收字节的转发目标数组,可包含 `$stdout` |
82
82
 
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` 先尝试软关闭,再完成强制清理 |
83
+ 这些属性都有同名 reader/writer;非法赋值不改变原设置。超时必须有限且非负。输出数组在设置时复制,读取时也返回副本。
96
84
 
97
- 布尔属性只提供 `name?` 和 `name=`,按 Ruby 真值规则转换:仅 `nil` / `false` 为假,`0` 为真。超时必须有限且非负,`nil`
98
- 表示无限;无效赋值不改变原值。`debug_level` 仅接受整数 `0..3`。
85
+ 只属于一次操作的策略放在该操作的关键字中:`spawn(raw: false)` 控制子终端,`expect(consume: true,
86
+ reset_timeout_on_read: false)` 控制匹配消费和输入重置,`interact(raw: true)` 控制本地终端模式,
87
+ `close(graceful: false)` 控制清理顺序。工厂的 `graceful:` 决定块退出时的关闭策略。`Session.new` 只接收会话属性,
88
+ 命令、`env:`、`chdir:` 和 `raw:` 由之后的 `session.spawn` 接收;均无旧名称别名。
99
89
 
100
90
  ## 等待和匹配
101
91
 
@@ -109,7 +99,8 @@ session.expect(timeout: 1) # 仅收集输出,直到超时或 EOF
109
99
  ```
110
100
 
111
101
  字符串始终按字面匹配,包括 `"-i"`、`"-re"`、`"timeout"` 和 `"eof"`;正则直接使用 Ruby `Regexp`
112
- 。按声明顺序选择第一个能匹配的模式,不按它们在文本中的位置排序。返回不可变 `Expect::Result`,`number` 为模式的 **1 起始序号**;超时、EOF 或 IO 错误时 `number` 为 `nil`。
102
+ 。按声明顺序选择第一个能匹配的模式,不按它们在文本中的位置排序。返回不可变 `Expect::Result`,`number` 为模式的 **1
103
+ 起始序号**;超时、EOF 或 IO 错误时 `number` 为 `nil`。
113
104
  判断成功使用 `matched?`,不能直接判断 Result 对象的真值。超时使用单调时钟。
114
105
 
115
106
  ```ruby
@@ -150,16 +141,15 @@ IO 等待的 `timeout` 不会中断单次正则计算。处理用户提供的正
150
141
 
151
142
  ## 回调、事件与多会话
152
143
 
153
-
154
144
  ```ruby
155
145
  session.expect(timeout: 10) do
156
146
  on(/username:\s*/i) do |connection|
157
147
  connection.puts("demo")
158
- connection.continue
148
+ Expect.continue
159
149
  end
160
150
  on(/password:\s*/i) do |connection|
161
151
  connection.puts(password)
162
- connection.continue(reset_timeout: false)
152
+ Expect.continue(reset_timeout: false)
163
153
  end
164
154
  on("ready>")
165
155
  eof { |connection| warn "EOF: #{connection.before}" }
@@ -170,18 +160,17 @@ end
170
160
  回调通过闭包访问局部变量。无参数声明块在模式构建器中执行;希望保留调用方 `self` 时使用 `do |patterns|`,调用
171
161
  `patterns.on(...)`。所有模式注册完成后才读取 IO;注册异常或 `break` 不消费输入。块和位置模式不能混用。注册完成后规则冻结,回调不能再向本次等待追加模式。
172
162
 
173
- `continue` 继续等待并重新计时;`continue(reset_timeout: false)`
174
- 保留原期限,类和实例均可调用。回调返回后若保留的期限已过,不再扫描新的文本匹配,未消费的输入留给下一次等待。无回调或返回其他值时结束本次匹配。超时回调只有返回重置计时的
175
- `continue` 才再次等待。EOF 回调继续时移除该源并等待其余会话;已知的 EOF 仍依次派发,全部 EOF 时直接返回,期限已过时仅对剩余活跃源触发超时。
163
+ `Expect.continue` 继续等待并重新计时;`Expect.continue(reset_timeout: false)`
164
+ 保留原期限。回调返回后若保留的期限已过,不再扫描新的文本匹配,未消费的输入留给下一次等待。无回调或返回其他值时结束本次匹配。超时回调只有返回重置计时的
165
+ `Expect.continue` 才再次等待。EOF 回调继续时移除该源并等待其余会话;已知的 EOF 仍依次派发,全部 EOF 时直接返回,期限已过时仅对剩余活跃源触发超时。
176
166
 
177
167
  `expect` 另接受 `deadline:`,值为 `Expect.monotonic` 时钟上的绝对秒数,`nil` 表示不设总期限。总期限与普通
178
168
  `timeout` 取较早者,接收重置、文本/EOF 继续及超时回调均不能延长它。同一个 deadline 可用于连续多次等待:
179
169
 
180
170
  ```ruby
181
171
  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)
172
+ session.expect("ready", timeout: 5, deadline: deadline, reset_timeout_on_read: true)
173
+ session.expect("done", timeout: 5, deadline: deadline, reset_timeout_on_read: true)
185
174
  ```
186
175
 
187
176
  上例每次等待允许最多 5 秒无新输入,两次等待共用 60 秒总预算。到达总期限后返回超时,不消费已缓冲的文本或读取新数据;已经确认的
@@ -200,10 +189,9 @@ end
200
189
  Expect.expect("ready", from: [first, second], timeout: 5)
201
190
  ```
202
191
 
203
- `from:` 指定一个或多个会话;实例块默认当前会话,类方法需提供来源。相邻且来源列表相同的模式组成一组,按组、会话、模式顺序匹配。类方法省略超时使用
204
- `Expect.configuration.timeout`。
192
+ `from:` 指定一个或多个会话;实例块默认当前会话,模块方法需提供来源。相邻且来源列表按对象身份及顺序相同的模式组成一组,按组、会话、模式顺序匹配。模块方法省略超时为无限等待。
205
193
 
206
- `preserve_buffer = true` 时,继续回调通常应自行消费匹配,例如 `connection.buffer = connection.after`
194
+ `expect(consume: false)` 时,继续回调通常应自行消费匹配,例如 `connection.buffer = connection.after`
207
195
  。如果回调没有改变缓冲,当前模式会等待缓冲变化后才重新匹配,避免反复处理同一内容。被信号中断的匹配 select/read 会自动重试,保留原期限。
208
196
 
209
197
  ## 已有 IO、写入和终端
@@ -218,7 +206,8 @@ ready = Expect.readable_sessions(first, second, timeout: 5)
218
206
  ```
219
207
 
220
208
  `Expect.open` 支持可 `select` 的 File、管道、Socket 和 PTY,`writer:` 可指定独立写端。默认借用 IO,关闭会话不关闭原始 IO;
221
- `own: true` 转移关闭责任,初始化失败也会释放接管的 IO。`StringIO` 可以用作日志和监听器,不能用作读取会话。
209
+ `own: true` 转移关闭责任,初始化中的属性校验失败也会释放接管的 IO。未知关键字在 Ruby 调用入口拒绝,此时不接管 IO。`StringIO`
210
+ 可以用作 transcript 和 outputs,不能用作读取会话。
222
211
 
223
212
  用于写入或转接的真实 IO 应在首次写入前设置 `io.sync = true`,并由调用方保证没有未刷新的 Ruby 写缓冲;`write_nonblock`
224
213
  可能先阻塞刷新已有缓冲,这一步不受本库的 IO 等待期限控制。已有缓冲应在交付给本库前由调用方排空,库不会绕过缓冲或改变字节顺序。
@@ -226,17 +215,17 @@ ready = Expect.readable_sessions(first, second, timeout: 5)
226
215
  `readable_sessions` 返回可读的会话对象数组,不消费数据、不包含已关闭会话、同一会话只返回一次。默认 `timeout: 0`;`nil`
227
216
  无限等待。同一会话应由一个读取者驱动,多会话共同监听使用 `Expect.expect`。
228
217
 
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 和终端信息 |
218
+ | API | 行为 |
219
+ |---------------------------------------------------------------|---------------------------------------------|
220
+ | `write(*objects)` | 通过 `to_s` 转换并写入所有字节,返回字节数 |
221
+ | `puts(*objects)` | 原生 IO 风格的换行、数组递归和 `nil` 返回值 |
222
+ | `session << object` | 写入并返回会话,可链式追加 |
223
+ | `send_slow(*objects, delay:)` | 每个字符之前等待指定秒数,同时收集返回数据 |
224
+ | `buffer` / `buffer=` | 获取副本 / 复制字节并应用上限 |
225
+ | `clear_buffer` | 清空缓冲并返回旧内容 |
226
+ | `to_io.console_mode` / `to_io.console_mode=` | 原生终端模式快照与恢复 |
227
+ | `to_io.winsize` / `to_io.winsize=` | 原生 `[rows, cols]` 窗口尺寸接口 |
228
+ | `slave` / `tty_name` / `to_io` / `writer` / `fileno` / `tty?` | 底层 IO 和终端信息 |
240
229
 
241
230
  `send_slow` 在每次写入后只检查已经可读的回复,不附加固定等待;返回时不保证收齐最后一个字符引发的回复,完整对话请继续使用
242
231
  `expect`。
@@ -250,49 +239,45 @@ ready = Expect.readable_sessions(first, second, timeout: 5)
250
239
  它与匹配的 `timeout/deadline`、整次 `interconnect` 的总 `timeout` 分别计算。非空写入要求底层返回实际接受的正整数字节数,
251
240
  且不能超过本次片段长度;非法计数立即抛出 `IOError`,空写入仍返回 0。
252
241
 
253
- `stty` 需要系统命令位于 `PATH`;缺失时抛出带安装提示的 `IOError`,原始 `Errno::ENOENT` 保留在 `cause`。窗口尺寸和人工接管的终端恢复使用
254
- Ruby `io/console`。
242
+ 终端操作直接使用 Ruby `io/console`:例如 `session.to_io.echo = false`、`session.to_io.winsize = [24, 80]`。
243
+ 需要作用域恢复时使用原生 `raw` / `cooked` 块;本库不启动 `stty` 子进程。
255
244
 
256
- ## 日志与人工交互
245
+ ## 诊断、接收记录与人工交互
257
246
 
258
247
  ```ruby
259
- session.log_to("session.log") # 文件追加
260
- session.log_to("session.log", mode: "w") # 文件覆盖
261
- session.log_to { |bytes| custom_logger.call(bytes) }
262
- session.log_output = output_io # 借用 IO 或 callable
263
- session.write_log("annotation\n")
264
- session.log_output = nil # 关闭本库打开的文件,借用的 IO 保留
265
- session.listeners = [output_io, another_session]
266
- session.log_listeners = false
248
+ require "logger"
249
+
250
+ logger = Logger.new($stderr, level: Logger::DEBUG)
251
+ File.open("session.log", File::WRONLY | File::CREAT | File::APPEND, 0o600) do |transcript|
252
+ Expect.spawn("/bin/sh", "-i", logger: logger, transcript: transcript) do |session|
253
+ session.redact(password, token) # 在首次通信前注册需要保护的原始字节
254
+ session.outputs = [$stdout, another_session]
255
+ session.write_transcript("annotation\n")
256
+ session.puts("exit")
257
+ end
258
+ end
267
259
  ```
268
260
 
269
- 日志读取用 `log_output`,设置用 `log_output=`,打开路径或注册日志块用 `log_to`。新建日志权限为 `0600`(仍受 umask
270
- 限制),已有文件保留原权限。不能同时提供日志目标与块。`listeners` 返回列表副本,`listeners = []` 清空;替换无效目标不会丢失原目标。
271
-
272
- 所有会话默认不输出到 stdout。日志仅记录实际读取的接收字节;写入不重复记录,终端回显可能作为接收内容返回。密码交互应关闭日志、调试,并确保被控程序不回显密码。
273
-
274
- 普通 `expect` 按顺序同步写入日志、stdout 和 `listeners`,这些目标须及时消费数据;匹配的 `timeout`
275
- 不会中断阻塞中的输出。需要在慢目标背压时继续处理其他输入,应使用下面的 `interconnect` 非阻塞转接接口并设置期限。
261
+ 三类目标职责独立:`logger` 接收结构化诊断;`transcript` 仅记录实际读取的接收字节;`outputs` 原样转发协议字节。
262
+ 所有目标均由调用方创建和关闭。`transcript = nil` 停止记录,替换前先交付旧流过滤尾部;`write_transcript` 补写记录,返回
263
+ `nil`,
264
+ 不发送到子进程。writer 必须返回实际接受的字节数,支持短写;路径与 callable 不会自动包装成 writer。
276
265
 
277
- 诊断与接收字节日志分别设置。`diagnostic_output:` 可作为构造参数,也可通过同名属性修改;接受标准 Logger、可写目标或回调,`nil`
278
- 使用 stderr。目标均为借用资源,关闭会话不会关闭它们。`debug_level` 仍控制内容:1 为生命周期和匹配,2 增加收发字节,3
279
- 增加缓冲。Logger 的级别分别使用 `info` 和 `debug`;回调收到冻结的 Hash,包含 `event`、`level`、`pid`、`fd`、`message`,不包含会话对象。
266
+ `logger` 采用 Ruby Logger 的 `add` / `debug?` 协议,默认 `nil` 禁用。Logger 自己决定级别、格式及输出位置;可直接注入兼容该协议的
267
+ ActiveSupport logger,本库不依赖 ActiveSupport。生命周期和匹配使用 INFO,收发字节使用 DEBUG,不生成缓冲快照诊断。
268
+ `add` 的 message 是冻结的 Hash,包含 `event`、`pid`、`fd`、`message`,其中 message 字符串也被冻结;progname 为 `"Expect"`。
269
+ 自定义格式使用 Logger 的 formatter;事件不含会话对象。logger 的返回值不控制匹配。
280
270
 
281
- ```ruby
282
- require "logger" # 使用 Logger 的应用需在自己的 Gemfile 声明 logger
283
- session.diagnostic_output = Logger.new($stderr)
284
- session.debug_level = 2
285
- session.redact(password, token) # 在首次通信前注册需要保护的原始字节
286
- session.log_to("session.log")
287
- ```
271
+ 所有会话默认没有 stdout 输出。需要显示时显式把 `$stdout` 放进 `outputs`。普通 `expect` 按读取顺序同步执行诊断、
272
+ transcript 和 outputs,目标须及时消费;匹配期限不会中断这些代码。慢目标需要独立调度时使用 `interconnect`。
273
+ 记录不重复包含发送字节,但终端回显可能作为接收内容返回;敏感会话须控制回显、显示和目标文件权限。
288
274
 
289
- `redact` 复制并追加本会话的非空字符串秘密,以 `[FILTERED]` 遮盖 `log_to` / `write_log`
290
- 及诊断中的对应字节,支持跨读取、跨写入和重叠秘密;匹配缓冲、结果、stdout 显示和 listeners 的转发仍是原始字节。应继续关闭敏感会话的
291
- `log_stdout`,并自行保护交互显示及协议转发目标。它不推断编码、终端转义、哈希或其他变换后的秘密,也不能删除已输出的日志。
275
+ `redact` 复制并追加非空字符串秘密,以 `[FILTERED]` 遮盖 transcript、`write_transcript` 及收发诊断,支持跨分片和重叠秘密。
276
+ 匹配缓冲、Result 和 outputs 始终保留原始字节。它不推断编码、终端转义、哈希或其他变换后的秘密,也不能删除已交付的记录。
292
277
 
293
- 过滤器最多延迟最长秘密长度减一的尾部字节,EOF、目标替换和显式关闭时交付剩余内容;流边界处疑似秘密前缀也会被遮盖。发送与接收诊断各自保留过滤状态;启用脱敏时,level
294
- 3 的缓冲快照只显示 `[FILTERED]`,避免部分消费或裁剪后剩下的秘密片段绕过过滤。日志回调的分块边界因此可能变化。GC
295
- 兜底不会调用用户日志回调,需显式关闭会话以交付过滤器尾部。
278
+ 过滤器最多延迟最长秘密长度减一的尾部字节,EOF、目标替换和显式关闭时交付剩余内容;流边界的疑似秘密前缀也会遮盖。
279
+ 发送与接收诊断独立保留过滤状态,因此实际写入分块可能变化。GC 兜底不调用用户代码;需显式关闭会话以交付过滤尾部,
280
+ 然后由调用方关闭 transcript 和 logger。
296
281
 
297
282
  应用需要过滤自己的日志或错误文本时,可以直接使用独立的字节过滤器,无需打开 PTY 或创建会话:
298
283
 
@@ -315,8 +300,8 @@ output.write(filter.finish)
315
300
  session.interact(input: $stdin, escape: "\x1d", output: $stdout) # Ctrl-]
316
301
 
317
302
  Expect.open($stdin) do |input|
318
- input.listeners = [session]
319
- session.listeners = [$stdout]
303
+ input.outputs = [session]
304
+ session.outputs = [$stdout]
320
305
  input.on_sequence("\x1d") { false }
321
306
  Expect.interconnect(input, session, timeout: 60)
322
307
  end
@@ -327,16 +312,16 @@ end
327
312
 
328
313
  `interconnect` 统一调度真实 IO 的非阻塞读写;慢目标不会阻止其他源前进,等待同时受总 `timeout` 和目标会话的 `write_timeout`
329
314
  约束。总期限到达返回 `nil`,目标写期限先到则抛出 `WriteTimeout`。超时后的字面转义前缀只尝试非阻塞发送,不再等待下游。作为写入目标但未显式列出的
330
- Expect 会话,背压期间读取的回复保留在其匹配缓冲;需要同时转发这些回复时,把它也传给 `interconnect`。
315
+ Session,背压期间读取的回复保留在其匹配缓冲;需要同时转发这些回复时,把它也传给 `interconnect`。
331
316
 
332
317
  每个源独立保存待发送数据以及各目标的发送位置,`source.pending_output?` 表示仍有未交付内容。超时或异常后再次对同一源调用
333
318
  `interconnect`,会接着发送未完成的后缀,已完成的目标不会重复接收;转义回调在前缀交付后执行。待发送数据与 `buffer`
334
- 中尚未处理的输入分开保存,修改 `listeners` 仅影响后续数据,旧数据仍发往原目标。恢复时不要把原始数据再次赋给 `buffer`
319
+ 中尚未处理的输入分开保存,修改 `outputs` 仅影响后续数据,旧数据仍发往原目标。恢复时不要把原始数据再次赋给 `buffer`
335
320
  ,也不要在排空旧输出前插入新的直接写入;关闭源会话会放弃其待发送数据。转接保留的输入暂不按 `buffer_limit` 裁剪,下次匹配时重新应用该上限。
336
321
 
337
322
  自定义写入对象必须及时返回实际接受的字节数,短写入会继续发送后缀,零、负数或非法返回值抛出 `IOError`
338
323
  。对象若先写入再抛错而不报告进度,库无法推断其副作用。日志、用户回调及自定义 `write` / `flush` 同步运行,应由调用方保证它们不会无限阻塞;上述
339
- IO 期限不会强行中断这些代码。普通 `expect` 的同步日志和监听器输出也不受匹配等待期限限制。
324
+ IO 期限不会强行中断这些代码。普通 `expect` 的同步 transcript 和 outputs 也不受匹配等待期限限制。
340
325
 
341
326
  字面转义可以跨读取完整过滤,尾部留给下次调用。正则转义使用历史记录,默认最多保留最近 65,536 字节;设置 `buffer_limit`
342
327
  后改用该值。正则及其锚点作用于当前历史窗口,超过窗口的跨读取正则无法匹配,已实时转发的前缀也无法撤回;零长度正则匹配抛出
@@ -347,8 +332,8 @@ IO 期限不会强行中断这些代码。普通 `expect` 的同步日志和监
347
332
  自定义 `write` 若已产生副作用却抛错、未返回计数,库无法推断已接受的字节数,此时不能保证恢复交付恰好一次。
348
333
  这一保护不代表所有会话 API 都可以跨线程并发调用。
349
334
 
350
- `interact` 会自动设置并恢复本地输入终端模式,同时保留输出换行处理;输入会话的 `raw_terminal = false` 将设置交给调用方。通用的
351
- `interconnect` 只负责字节转发,由调用方管理终端模式。`interact` 还会恢复临时监听组、日志开关和转义设置,包括超时和异常路径。
335
+ `interact` 默认 `raw: true`,自动设置并恢复本地输入终端模式,同时保留输出换行处理;`raw: false` 将设置交给调用方。
336
+ 通用的 `interconnect` 只负责字节转发,由调用方管理终端模式。`interact` 会恢复临时 outputs 和转义设置,包括超时和异常路径。
352
337
 
353
338
  对同一连接重复传入同一个原始输入 IO 时,`interact` 会复用输入包装器,接续上次预读的尾部。包装器由该连接持有,关闭连接时释放,但不关闭借用的原始
354
339
  IO;已关闭的输入或包装器不再复用。需要跨连接共享或自行管理输入生命周期时,显式传入 `Expect.open(input)` 创建的会话。
@@ -368,7 +353,7 @@ session.close(graceful: true) # 先软关闭,必要时继续硬关闭
368
353
  - `hard_close`:立即关闭所属 IO,不收集尾部输出;等待 `timeout:`,必要时发送 TERM 再等待同样时长,仍未退出则 KILL 并最多等待
369
354
  1 秒。默认 `timeout: 0.2`,必须有限。
370
355
  - 两者返回已回收的 `Process::Status`,没有子进程或尚未回收时返回 `nil`;重复调用保留已获得的状态。借用 IO 不关闭。
371
- - `close(graceful: graceful_close?)`:可选先软关闭,`ensure` 中硬关闭,返回 `nil`。块生命周期使用它完成清理;软关闭发生日志异常时也会回收子进程。
356
+ - `close(graceful: false)`:可选先软关闭,`ensure` 中硬关闭,返回 `nil`。块生命周期使用它完成清理;软关闭发生日志异常时也会回收子进程。
372
357
  - `wait(timeout: nil)`:等待并回收,返回 `Process::Status`;超时返回 `nil`。`process_status` 非阻塞查询,`exit_code`
373
358
  读取普通退出码,信号退出看 `process_status.termsig`。
374
359
  - `closed?` 表示会话 IO 已关闭;`alive?` / `pid` 表示子进程状态。软关闭后可能同时 `closed? == true`、`alive? == true`。成功回收后
@@ -378,10 +363,8 @@ session.close(graceful: true) # 先软关闭,必要时继续硬关闭
378
363
 
379
364
  ## 安全注意事项
380
365
 
381
- - 将不可信命令和参数分别传给 `spawn`,例如 `Expect.spawn("ssh", host)`;单个命令字符串会使用 Ruby 的 shell 语义。`stty`
382
- 参数经拆分后作为独立参数传给进程,不拼接 shell 命令。
383
- - 会话日志可能记录密码回显、令牌和其他敏感字节。新日志文件以 `0600` 创建,已有文件保留原权限;请按需关闭 `log_to`、
384
- `log_stdout` 和调试输出,并管理日志留存。
366
+ - 将不可信命令和参数分别传给 `spawn`,例如 `Expect.spawn("ssh", host)`;单个命令字符串会使用 Ruby 的 shell 语义。
367
+ - 接收记录和诊断可能包含密码回显、令牌和其他敏感字节。调用方负责文件权限与留存,按需关闭 transcript、outputs 和 logger。
385
368
  - `spawn` 在子进程中使用 `fork` 后的 Ruby 操作与 `exec`。高度多线程的宿主进程,尤其使用第三方 C 扩展时,可能受到 fork
386
369
  时其他线程持锁的影响;尽量在启动其他线程前创建会话,并在自己的运行环境中验证。
387
370
  - 不可信正则可能耗费较长时间;使用带 `timeout:` 的 `Regexp` 实例,并为匹配缓冲设置合适的 `buffer_limit`。普通 `expect` 的
@@ -416,18 +399,17 @@ Artifacts 下载。
416
399
  运行时依赖只在 `expect-pty.gemspec` 声明,开发依赖放在 Gemfile 的 `development` / `test` 组;安装或使用本库不会引入
417
400
  Minitest、Rake、RuboCop 及发布工具的依赖。
418
401
 
419
- | 运行时模块 | Gem | 用途 |
420
- |---------------|---------------|--------------------------|
421
- | `forwardable` | `forwardable` | 会话配置委托 |
422
- | `io/console` | `io-console` | 终端模式和窗口大小 |
423
- | `IO#wait_readable` | Ruby 3.2 内置 | IO 可读等待,无独立 gem |
424
- | `shellwords` | `shellwords` | `stty` 参数拆分 |
425
- | `stringio` | `stringio` | Ruby `puts` 语义 |
426
- | `pty` | Ruby 自带扩展 | POSIX 伪终端,无独立 gem |
402
+ | 运行时模块 | Gem | 用途 |
403
+ |--------------------|---------------|--------------------------|
404
+ | `io/console` | `io-console` | 终端模式和窗口大小 |
405
+ | `IO#wait_readable` | Ruby 3.2 内置 | IO 可读等待,无独立 gem |
406
+ | `logger` | `logger` | 标准诊断协议与级别 |
407
+ | `stringio` | `stringio` | Ruby `puts` 语义 |
408
+ | `pty` | Ruby 自带扩展 | POSIX 伪终端,无独立 gem |
427
409
 
428
410
  仅发布 RubyGems 使用 `ruby script/release.rb --rubygems-only`,直接复用本机已有的 Gem 登录状态;添加 `--dry-run`
429
411
  可先完成本地检查、测试、构建和安装验证。需要同时创建 GitHub Release 时使用 `ruby script/release.rb`,也可以在 GitHub Actions
430
- 手动运行 Release 工作流。版本准备、Actions 凭据和失败重试见 [发布说明](docs/RELEASING.md)。
412
+ 推送版本标签触发 OIDC 可信发布,或手动恢复同一标签。版本准备、可信发布配置和失败重试见 [发布说明](docs/RELEASING.md)。
431
413
 
432
414
  SSH 示例用 `SSH_USER`、`SSH_HOST`、`SSH_KNOWN_HOSTS` 配置,密码隐藏输入或从 `EXPECT_PASSWORD` 读取;非本地主机要求受信任的
433
415
  known_hosts 文件。`ssh_auto.rb` 顶部 `COMMANDS` 可直接修改,日志写入 `tmp/ssh-auto/`,权限 0600。
@@ -439,9 +421,10 @@ known_hosts 文件。`ssh_auto.rb` 顶部 `COMMANDS` 可直接修改,日志写
439
421
  。此次重构直接移除了旧入口,不提供兼容别名。
440
422
  防火墙连接器已迁移到相邻的 `algosec` 项目;本库只保留 `Expect` 与 `expect-pty` 通用传输能力。
441
423
 
442
- ## 类型、文档与 0.6.0 接口变更
424
+ ## 类型、文档与 0.7.0 接口变更
443
425
 
444
- 0.6.0 最低支持 Ruby 3.4,包含不可变 `Result` 和内部 Session 重组。完整接口见 [API 文档](docs/API.md),从 0.5.x 升级前请阅读
426
+ 0.7.0 将 `Expect` 改为模块,公开真实 `Session`,移除全局配置、门面、终端命令包装和旧日志接口。
427
+ 最低仍为 Ruby 3.4,保留不可变 `Result`。完整接口见 [API 文档](docs/API.md),从 0.6.x 或更早版本升级前请阅读
445
428
  [迁移说明](docs/MIGRATION.md)。类型签名随 Gem 发布在 `sig/expect.rbs`。
446
429
 
447
430
  发布包仅包含运行源码、类型签名和使用文档。测试、基准、示例及维护脚本请从仓库取得;