expect-pty 0.9.6 → 0.10.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +1 -1
- data/README.zh-CN.md +1 -1
- data/docs/API.md +13 -9
- data/docs/COMPATIBILITY.md +2 -1
- data/docs/MIGRATION.md +16 -0
- data/lib/expect/interaction.rb +96 -16
- data/lib/expect/logging.rb +13 -19
- data/lib/expect/matcher.rb +1 -1
- data/lib/expect/pattern.rb +3 -3
- data/lib/expect/pattern_list.rb +3 -3
- data/lib/expect/pty.rb +4 -0
- data/lib/expect/relay/writer.rb +1 -1
- data/lib/expect/responder.rb +3 -3
- data/lib/expect/result.rb +39 -17
- data/lib/expect/session.rb +18 -21
- data/lib/expect/version.rb +1 -1
- data/sig/expect.rbs +18 -10
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eabbf4b5d408191298426e8bc078219eacd371b58fd0cb06bdfda6ca26d43f22
|
|
4
|
+
data.tar.gz: 6735e5a170c9b82f8043909001004bcb705f6be5971bfdd0c96db495f2cef29c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16e185ae81ce6355aa215c2e91c42ad625c6082317f19f45f51e9f300bfe937318f75f3de81183cd405f9fafaf4ecd99709a54502efd089d10d7d24011c194eb
|
|
7
|
+
data.tar.gz: ab419a9535eed4647d6b4c095caf6a0af84cd5522e5213fe2b186c4dd5ca67428300b40025843d56705a7a9e32b5c3d2a1f8ec29a803c1c3d794405850c3f178
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.10.0 - 2026-10-30
|
|
6
|
+
|
|
7
|
+
- 最低 Ruby 升级至 4.0.6;Result 明确区分 matched、timeout、eof、error,编号统一为 match_number,error 只保存原始异常。构造和 with 拒绝矛盾状态,旧 number 字段移除。
|
|
8
|
+
- 保留既有模块结构,将日志与转接状态初始化归回所属模块,协议输出归 Interaction 管理;等待与转接发送诊断使用显式内部协作入口。
|
|
9
|
+
- 转义规则变更后首次使用时统一编译,读取期间复用正则包装与冻结文本规则快照;保持优先级、UTF-8、零宽校验、跨次 interact 历史和短写恢复契约。
|
|
10
|
+
- API 检查同时拒绝已移除接口的残留 RBS 声明;同步迁移示例、签名和安装验证。
|
|
11
|
+
|
|
5
12
|
## 0.9.6 - 2026-10-23
|
|
6
13
|
|
|
7
14
|
- 移除 io-console、json、logger 和 stringio 的额外版本下限,沿用 Ruby 3.4+ 的运行时要求。
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
A Ruby library for automating interactive programs through POSIX pseudo-terminals. Start a child process, wait for text or regular expressions, send input, and manage timeouts, callbacks, and cleanup with Ruby objects and blocks.
|
|
8
8
|
|
|
9
|
-
RubyGems package: **expect-pty**. Requires Ruby
|
|
9
|
+
RubyGems package: **expect-pty**. Requires Ruby 4.0.6+ on Linux or macOS.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
data/README.zh-CN.md
CHANGED
data/docs/API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ruby API
|
|
2
2
|
|
|
3
|
-
支持 Ruby
|
|
3
|
+
支持 Ruby 4.0.6+ 和 Linux/macOS。加载入口为 `require "expect/pty"`,独立流过滤器可加载 `expect/redactor`。
|
|
4
4
|
`Expect` 是模块;`Expect::Session` 是公开的真实会话。工厂、回调、链式方法与 `Result#session` 使用同一个 Session。
|
|
5
5
|
|
|
6
6
|
## 创建与关闭
|
|
@@ -37,7 +37,7 @@ nil。
|
|
|
37
37
|
`Expect::Result`。
|
|
38
38
|
`Expect.expect` 使用相同操作关键字,另加 `from:` 指定默认来源;其 timeout 默认 nil。模式为 String、Regexp、`:eof` 或
|
|
39
39
|
`:timeout`,
|
|
40
|
-
位置模式与声明块不能混用。`
|
|
40
|
+
位置模式与声明块不能混用。`match_number` 为从 1 开始的文本模式序号或 nil,事件声明也占序号;使用 `matched?` 判断成功。
|
|
41
41
|
|
|
42
42
|
```ruby
|
|
43
43
|
result = session.expect(timeout: 3) do |patterns|
|
|
@@ -47,7 +47,7 @@ result = session.expect(timeout: 3) do |patterns|
|
|
|
47
47
|
end
|
|
48
48
|
patterns.on(/hello (\w+)/)
|
|
49
49
|
end
|
|
50
|
-
result => {
|
|
50
|
+
result => { status: :matched, match_number:, captures: }
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
无参数声明块在 PatternList 上运行,直接调用 `on`、`eof`、`timeout`;有参数块保留调用者 self。
|
|
@@ -69,11 +69,15 @@ nil 保留全缓冲扫描;指定时,字符串和正则只搜索最新的尾
|
|
|
69
69
|
|
|
70
70
|
Result 的 `inspect` 与 `to_s` 仅显示匹配序号和错误状态,不打印原始文本、异常消息或来源对象。
|
|
71
71
|
需要原始结果时显式读取字段;`redact` 不改变这些字段。
|
|
72
|
-
Result
|
|
73
|
-
nil
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
Result 字段按顺序为 `status`、`match_number`、`error`、`match`、`before`、`after`、`session`、`captures`。
|
|
73
|
+
状态为 `:matched`、`:timeout`、`:eof` 或 `:error`;`error` 只保存原始异常或 nil,等待超时和 EOF 均不是异常。
|
|
74
|
+
使用 `matched?`、`timeout?`、`eof?`、`error?` 查询。仅匹配状态具有正整数编号和字符串 match(包括零宽匹配的空串);
|
|
75
|
+
其余状态没有编号、match、after 或捕获,只有 `:error` 状态携带异常。参数、编码与回调异常仍按原规则直接抛出。
|
|
76
|
+
|
|
77
|
+
对象、文本及捕获数组均不可变,未参与的捕获为 nil;来源会话与异常不复制、不冻结。
|
|
78
|
+
使用 `to_h`、位置/键模式解构或 `with` 构造新结果;构造必须指定 status,new、[]、with 都拒绝矛盾状态与非字符串文本。
|
|
79
|
+
没有字段 writer、旧 number 或 to_a。会话的 `last_result`、`before`、`after`、`match`、`match_number`、`captures`、`error`
|
|
80
|
+
读取最近结果;最近等待状态使用 `last_result.status`,与输入 EOF、IO 关闭、进程状态分别查询。
|
|
77
81
|
|
|
78
82
|
匹配、捕获按原始字节保存;文本显示时先 `dup` 再 `force_encoding`。固定 UTF-8 正则等待尾部字符收齐,非法编码抛出
|
|
79
83
|
EncodingError。
|
|
@@ -95,7 +99,7 @@ end.freeze
|
|
|
95
99
|
commands.each do |command|
|
|
96
100
|
session.puts(command)
|
|
97
101
|
result = rules.expect(session, "device>", timeout: 30, deadline: Expect.monotonic + 60)
|
|
98
|
-
raise "command incomplete: #{result.
|
|
102
|
+
raise "command incomplete: #{result.status}" unless result.matched?
|
|
99
103
|
end
|
|
100
104
|
```
|
|
101
105
|
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -36,7 +36,8 @@ Austin Schutz、Roland Giersig、Dave Jacoby,采用与 Perl 相同的许可。
|
|
|
36
36
|
UTF-8、EOF/超时回调、绝对期限与接收重置、多会话、缓冲上限、慢速写入和背压、借用 writer 的接收记录、标准 Logger 诊断、outputs
|
|
37
37
|
转发图、人工接管、跨读取转义及终端恢复。
|
|
38
38
|
|
|
39
|
-
`expect` / `last_result`
|
|
39
|
+
`expect` / `last_result` 返回不可变八字段 `Data`,匹配序号通过 `match_number` 读取,等待结果通过 `status` 查询。
|
|
40
|
+
`error` 只保存原始异常或 nil;EOF 与超时各有独立状态。使用 `to_h` 或模式解构;
|
|
40
41
|
字段及文本快照不可修改,不提供 `to_a`。日志按实际读取时记录,转接与匹配切换不重复写日志;显式启用 `redact` 时过滤已注册秘密。
|
|
41
42
|
|
|
42
43
|
## 软关闭与硬关闭
|
data/docs/MIGRATION.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# 迁移说明
|
|
2
2
|
|
|
3
|
+
## 0.9.x → 0.10.0:Ruby 4 与明确的等待结果
|
|
4
|
+
|
|
5
|
+
- 最低运行环境为 Ruby 4.0.6,保留 Linux/macOS 支持;本地开发使用 RVM。
|
|
6
|
+
- `result.number` 改为 `result.match_number`,与 Session 的快照访问器一致,不提供旧字段别名。
|
|
7
|
+
- `result.error == :timeout` / `:eof` 改用 `result.timeout?` / `result.eof?`,或判断 `result.status`。
|
|
8
|
+
`error` 现在只保存原始异常或 nil;`session.error` 同样遵守此约定,最近等待状态为 `session.last_result.status`。
|
|
9
|
+
- Result 的位置参数与解构在开头加入 status,第二项为 match_number;优先使用键模式。
|
|
10
|
+
手工构造匹配结果用 `Result.new(status: :matched, match_number: 1, match: "ready")`,
|
|
11
|
+
EOF 用 `Result.new(status: :eof, before: "tail")`,IO 错误用 `Result.new(status: :error, error: exception)`。
|
|
12
|
+
- `new`、`[]`、`with` 校验状态一致性:修改匹配结果为超时还须清除编号、match、after、captures。
|
|
13
|
+
输入 EOF、IO 关闭、子进程退出继续独立表达,关闭和命令发送策略不变。
|
|
14
|
+
- net-connector 使用配套 0.22.x;它在 PTY 适配层转换结果,上层设备事件和零基索引协议保持不变。
|
|
15
|
+
|
|
16
|
+
内部保留既有类与混入模块。转义注册表改为冻结快照;内部恢复须使用 sequences=,避免绕过缓存更新。
|
|
17
|
+
历史按实际扫描规则失效,临时 interact 注册与恢复仍支持同规则跨次接管。
|
|
18
|
+
|
|
3
19
|
## 0.8.x → 0.9.0:启动与时间参数
|
|
4
20
|
|
|
5
21
|
- 始终使用 `require "expect/pty"`,Gemfile 指定 `require: "expect/pty"`。本库删除 `lib/expect.rb` 后,
|
data/lib/expect/interaction.rb
CHANGED
|
@@ -8,7 +8,7 @@ require_relative "literal_scan"
|
|
|
8
8
|
# 为会话补充人工接管和多路 IO 转接;核心会话定义位于 session.rb。
|
|
9
9
|
module Expect
|
|
10
10
|
# 为公开 Session 混入转义注册与人工接管接口;协作方法各自标记为内部协议。
|
|
11
|
-
# 宿主提供真实读写 IO
|
|
11
|
+
# 宿主提供真实读写 IO 和缓冲;本模块保存输出目标、转义、显示历史和待写游标。
|
|
12
12
|
# Relay 暂借输入缓冲并调度游标,Matcher 可在转义回调中临时接管,退出时归还未消费尾部。
|
|
13
13
|
module Interaction
|
|
14
14
|
# 正则没有“潜在部分匹配”接口,只保留有限历史;已转发的历史字节不能撤回。
|
|
@@ -54,7 +54,7 @@ module Expect
|
|
|
54
54
|
end
|
|
55
55
|
raise ArgumentError, "escape sequence must not be empty" if key == ""
|
|
56
56
|
|
|
57
|
-
@sequences
|
|
57
|
+
self.sequences = @sequences.merge(key => block || STOP).freeze
|
|
58
58
|
self
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -79,15 +79,34 @@ module Expect
|
|
|
79
79
|
# 超时或异常后仍有未交付的数据;重新 interconnect 同一源会话可继续发送。
|
|
80
80
|
def pending_output? = @pending_writes.any? { |output| !output.done? }
|
|
81
81
|
|
|
82
|
+
# 返回协议转发目标的副本,外部修改不能原地改变转发图。
|
|
83
|
+
def outputs = @outputs.dup
|
|
84
|
+
|
|
85
|
+
# 先校验整个数组再替换,失败时保留原输出图;目标始终由调用方拥有。
|
|
86
|
+
def outputs=(targets)
|
|
87
|
+
unless targets.is_a?(Array) && targets.all? { |target| target.respond_to?(:write) }
|
|
88
|
+
raise ArgumentError, "outputs must be an Array of writers"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@outputs = targets.dup
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 普通接收同步交付原始字节;日志脱敏不影响协议输出。
|
|
95
|
+
# @api private
|
|
96
|
+
def propagate(data)
|
|
97
|
+
@outputs.each { |output| emit(output, data) }
|
|
98
|
+
end
|
|
99
|
+
|
|
82
100
|
# 将待处理输入排入唯一的转接发送队列;返回 :ready、:queued、:stopped 或 :timeout。
|
|
83
101
|
# 字面序列暂存潜在前缀,正则序列结合历史匹配;final 为真时不再等待后续字节。
|
|
84
102
|
# @api private
|
|
85
103
|
def self.queue_input(session, buffer, final: false, deadline: nil, literal_scan: nil)
|
|
86
104
|
loop do
|
|
87
|
-
sequences = session.
|
|
105
|
+
sequences = session.text_sequences
|
|
88
106
|
history = session.relay_history
|
|
89
107
|
found, regexp_scanned, utf8_regexp = scan_sequences(
|
|
90
|
-
buffer, sequences, history, final:, literal_scan:, generation: session.buffer_generation
|
|
108
|
+
buffer, sequences, history, final:, literal_scan:, generation: session.buffer_generation,
|
|
109
|
+
compiled: session.sequence_patterns
|
|
91
110
|
)
|
|
92
111
|
return :timeout if expired?(deadline)
|
|
93
112
|
|
|
@@ -141,17 +160,19 @@ module Expect
|
|
|
141
160
|
end
|
|
142
161
|
|
|
143
162
|
# 单轮正则共用一个组合文本;回调返回后下一轮重新取得规则和历史。
|
|
144
|
-
def self.scan_sequences(buffer, sequences, history, final:, literal_scan: nil, generation: nil)
|
|
163
|
+
def self.scan_sequences(buffer, sequences, history, final:, literal_scan: nil, generation: nil, compiled: {})
|
|
145
164
|
text = nil
|
|
146
165
|
utf8_regexp = false
|
|
147
166
|
earliest = nil
|
|
167
|
+
earliest_offset = Float::INFINITY
|
|
148
168
|
# 只保存最早候选,同位置不替换先注册者;仍扫描全部正则以校验零宽规则并收集编码状态。
|
|
149
169
|
sequences.each do |key, handler|
|
|
150
170
|
if key.is_a?(Regexp)
|
|
151
171
|
# 仅在本轮扫描复用组合文本;回调可能更换规则或消费缓冲,下一轮必须重新构造。
|
|
152
172
|
text ||= history + buffer
|
|
153
173
|
utf8_regexp ||= key.fixed_encoding? && key.encoding == Encoding::UTF_8
|
|
154
|
-
|
|
174
|
+
pattern = compiled.fetch(key) { Pattern.new(value: key) }
|
|
175
|
+
position = pattern.locate(text, final:, captures: false)
|
|
155
176
|
next unless position
|
|
156
177
|
|
|
157
178
|
offset, length, = position
|
|
@@ -165,7 +186,10 @@ module Expect
|
|
|
165
186
|
|
|
166
187
|
length = key.bytesize
|
|
167
188
|
end
|
|
168
|
-
|
|
189
|
+
if offset < earliest_offset
|
|
190
|
+
earliest = [offset, length, handler]
|
|
191
|
+
earliest_offset = offset
|
|
192
|
+
end
|
|
169
193
|
end
|
|
170
194
|
[earliest, !text.nil?, utf8_regexp]
|
|
171
195
|
end
|
|
@@ -173,11 +197,10 @@ module Expect
|
|
|
173
197
|
# 普通转发和超时排出的字面前缀都属于正则历史,跨次转接时必须接续同一字节流。
|
|
174
198
|
# @api private
|
|
175
199
|
def self.remember_output(session, data)
|
|
176
|
-
|
|
177
|
-
return if regexps.empty?
|
|
200
|
+
return if session.sequence_patterns.empty?
|
|
178
201
|
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
trim_history(session.relay_history, data, limit: session.buffer_limit || REGEXP_ESCAPE_HISTORY_LIMIT,
|
|
203
|
+
utf8: session.utf8_sequences?)
|
|
181
204
|
end
|
|
182
205
|
|
|
183
206
|
# 只暂存可能拼成完整字面转义的最长后缀。
|
|
@@ -221,14 +244,54 @@ module Expect
|
|
|
221
244
|
end
|
|
222
245
|
end
|
|
223
246
|
|
|
224
|
-
#
|
|
247
|
+
# 冻结的注册快照只能通过 on_sequence 或内部恢复入口替换,不能绕过编译及历史失效。
|
|
225
248
|
# @api private
|
|
226
249
|
attr_reader :sequences
|
|
227
250
|
|
|
228
251
|
# @api private
|
|
229
252
|
def sequences=(sequences)
|
|
230
|
-
|
|
231
|
-
@
|
|
253
|
+
# Hash 的值相等忽略插入顺序;转义优先级必须同时比较声明顺序。
|
|
254
|
+
return if @sequences == sequences && @sequences.keys == sequences.keys
|
|
255
|
+
|
|
256
|
+
@sequences = sequences.frozen? ? sequences : sequences.dup.freeze
|
|
257
|
+
@literal_prefix_tables.delete_if { |sequence, _table| !sequences.key?(sequence) }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# 连续声明只发布注册快照;首次扫描统一编译,后续读取不重复创建集合及正则包装。
|
|
261
|
+
# @api private
|
|
262
|
+
def text_sequences
|
|
263
|
+
return @text_sequences if @compiled_sequences.equal?(@sequences)
|
|
264
|
+
|
|
265
|
+
registered = @sequences
|
|
266
|
+
text = registered.except(:eof).freeze
|
|
267
|
+
patterns = text.each_key.with_object({}) do |key, patterns|
|
|
268
|
+
patterns[key] = @sequence_patterns[key] || Pattern.new(value: key) if key.is_a?(Regexp)
|
|
269
|
+
end.freeze
|
|
270
|
+
utf8 = patterns.each_key.any? do |regexp|
|
|
271
|
+
regexp.fixed_encoding? && regexp.encoding == Encoding::UTF_8
|
|
272
|
+
end
|
|
273
|
+
# 编译结果一起发布;失败时旧缓存仍完整,下次扫描可以重新编译,不读取陈旧规则。
|
|
274
|
+
Thread.handle_interrupt(Object => :never) do
|
|
275
|
+
@text_sequences = text
|
|
276
|
+
@sequence_patterns = patterns
|
|
277
|
+
@utf8_sequences = utf8
|
|
278
|
+
@compiled_sequences = registered
|
|
279
|
+
end
|
|
280
|
+
text
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# 冻结的正则包装快照;已存在的正则跨规则变更复用。
|
|
284
|
+
# @api private
|
|
285
|
+
def sequence_patterns
|
|
286
|
+
text_sequences
|
|
287
|
+
@sequence_patterns
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# 正则历史的编码边界由当前已注册规则决定。
|
|
291
|
+
# @api private
|
|
292
|
+
def utf8_sequences?
|
|
293
|
+
text_sequences
|
|
294
|
+
@utf8_sequences
|
|
232
295
|
end
|
|
233
296
|
# 冻结字面规则对应的 KMP 前缀表,跨转接复用,避免每个读块重新编译长规则。
|
|
234
297
|
# @api private
|
|
@@ -252,8 +315,9 @@ module Expect
|
|
|
252
315
|
# 同规则保留跨次转接的连续历史;换规则或消费、改写输入后,不能拼接旧的前缀。
|
|
253
316
|
# @api private
|
|
254
317
|
def relay_history
|
|
255
|
-
|
|
256
|
-
|
|
318
|
+
# interact 的临时注册和恢复不消费输入;只比较实际扫描时的规则,保留同规则跨次接管的历史。
|
|
319
|
+
sequences = text_sequences
|
|
320
|
+
@relay_history.clear unless @relay_history_sequences == sequences
|
|
257
321
|
@relay_history_sequences = sequences
|
|
258
322
|
@relay_history
|
|
259
323
|
end
|
|
@@ -267,6 +331,22 @@ module Expect
|
|
|
267
331
|
|
|
268
332
|
private
|
|
269
333
|
|
|
334
|
+
# 转接状态集中在所属模块初始化;缓冲和进程状态仍由 Session 管理。
|
|
335
|
+
def initialize_interaction
|
|
336
|
+
@outputs = []
|
|
337
|
+
@sequences = {}.freeze
|
|
338
|
+
@text_sequences = @sequence_patterns = @sequences
|
|
339
|
+
@compiled_sequences = @sequences
|
|
340
|
+
@utf8_sequences = false
|
|
341
|
+
@literal_prefix_tables = {}
|
|
342
|
+
@pending_writes = []
|
|
343
|
+
@interact_inputs = {}.compare_by_identity
|
|
344
|
+
@interact_output = nil
|
|
345
|
+
@interaction_buffer = @relay_owner = @relay_callback = nil
|
|
346
|
+
@relay_history = "".b
|
|
347
|
+
@relay_history_sequences = @text_sequences
|
|
348
|
+
end
|
|
349
|
+
|
|
270
350
|
# 同一输入 IO 重用一个借用会话,以保留上次接管预读的尾部并避免不断积累包装器。
|
|
271
351
|
def interact_source(input)
|
|
272
352
|
return input if input.is_a?(Session)
|
data/lib/expect/logging.rb
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
require "logger"
|
|
4
4
|
require_relative "redactor"
|
|
5
5
|
|
|
6
|
-
# 借用标准 Logger
|
|
6
|
+
# 借用标准 Logger 和接收记录 writer,不接管它们的生命周期。
|
|
7
7
|
# 脱敏只作用于诊断与接收记录,协议输出和匹配缓冲始终保留原始字节。
|
|
8
8
|
module Expect
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# 三种输出各有边界:Logger 接收事件,transcript 接收脱敏字节,outputs 接收原协议字节。
|
|
9
|
+
# 会话诊断与接收记录接口;Session 构造时调用本模块初始化过滤器,并提供 pid、fileno 元数据。
|
|
10
|
+
# Logger 接收事件,transcript 接收脱敏字节;Interaction 的协议输出只复用同步 writer 交付。
|
|
12
11
|
module Logging
|
|
13
12
|
# 调用方提供的诊断 Logger 与接收记录 writer,均由调用方管理生命周期。
|
|
14
13
|
attr_reader :logger, :transcript
|
|
@@ -68,26 +67,21 @@ module Expect
|
|
|
68
67
|
nil
|
|
69
68
|
end
|
|
70
69
|
|
|
71
|
-
#
|
|
72
|
-
def outputs = @outputs.dup
|
|
73
|
-
|
|
74
|
-
# 先校验整个数组再替换,失败时保留原输出图。
|
|
75
|
-
def outputs=(targets)
|
|
76
|
-
unless targets.is_a?(Array) && targets.all? { |target| target.respond_to?(:write) }
|
|
77
|
-
raise ArgumentError, "outputs must be an Array of writers"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
@outputs = targets.dup
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# 原样转发协议字节,接收记录在读取时单独写入。
|
|
70
|
+
# 转接确认交付后记录发送字节;调用者已登记游标,诊断失败不能回退发送进度。
|
|
84
71
|
# @api private
|
|
85
|
-
def
|
|
86
|
-
|
|
72
|
+
def record_sent(data)
|
|
73
|
+
trace_data(:sending, data)
|
|
87
74
|
end
|
|
88
75
|
|
|
89
76
|
private
|
|
90
77
|
|
|
78
|
+
# 每个日志方向独立保留脱敏状态;协议缓冲及转发图属于 Session 与 Interaction。
|
|
79
|
+
def initialize_logging
|
|
80
|
+
@secrets = @transcript_redactor = nil
|
|
81
|
+
@diagnostic_redactors = {}
|
|
82
|
+
@logger = @transcript = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
91
85
|
# 关闭时两种记录都要尝试完成;后续 writer 的常规异常不能覆盖先发生的诊断异常。
|
|
92
86
|
# 不关闭借用目标,新的 Interrupt/SystemExit 仍由 Cleanup 原样传播。
|
|
93
87
|
def finish_logging
|
data/lib/expect/matcher.rb
CHANGED
|
@@ -255,7 +255,7 @@ module Expect
|
|
|
255
255
|
# 为活跃会话记录超时,回调接收全部活跃源;只有重置计时的继续符号能重新等待。
|
|
256
256
|
# 已到总期限仍通知超时回调,但不接受继续请求,且不消费尚未匹配的字节。
|
|
257
257
|
def handle_timeout
|
|
258
|
-
results = active_sessions.map
|
|
258
|
+
results = active_sessions.map(&:record_timeout)
|
|
259
259
|
action = @patterns.timeout_pattern&.call(active_sessions)
|
|
260
260
|
return results.first unless CONTINUE.equal?(action) && !hard_expired?
|
|
261
261
|
|
data/lib/expect/pattern.rb
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module Expect
|
|
4
4
|
# 一个已编号的文本模式或事件及其回调;匹配结果统一使用字节偏移,便于精确消费 IO 缓冲。
|
|
5
5
|
# @api private
|
|
6
|
-
Pattern = Data.define(:
|
|
6
|
+
Pattern = Data.define(:match_number, :value, :callback) do
|
|
7
7
|
# 复制字符串模式;Regexp 和 callable 只引用,不取得外部对象的生命周期。
|
|
8
|
-
def initialize(value:,
|
|
9
|
-
super(value: value.is_a?(String) ? value.b.freeze : value,
|
|
8
|
+
def initialize(value:, match_number: nil, callback: nil)
|
|
9
|
+
super(value: value.is_a?(String) ? value.b.freeze : value, match_number:, callback:)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# 将匹配会话或超时会话数组交给回调;额外上下文由调用方闭包保存。
|
data/lib/expect/pattern_list.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Expect
|
|
|
14
14
|
validate_sessions!(@default_sessions) unless @default_sessions.empty?
|
|
15
15
|
# 保留空模式的默认会话组,使 expect(timeout:) 也能只收集输出而不匹配文本。
|
|
16
16
|
@groups = @default_sessions.empty? ? [] : [[@default_sessions, []]]
|
|
17
|
-
@
|
|
17
|
+
@match_number = 0
|
|
18
18
|
patterns.each do |value|
|
|
19
19
|
case value
|
|
20
20
|
when :eof then eof
|
|
@@ -140,8 +140,8 @@ module Expect
|
|
|
140
140
|
|
|
141
141
|
# 按注册顺序分配从 1 开始的序号,文本模式与事件共用编号。
|
|
142
142
|
def build(value, callback)
|
|
143
|
-
@
|
|
144
|
-
Pattern.new(
|
|
143
|
+
@match_number += 1
|
|
144
|
+
Pattern.new(match_number: @match_number, value:, callback:)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# 拒绝空来源和非 Session 对象,在任何 IO 读取之前暴露调用错误。
|
data/lib/expect/pty.rb
CHANGED
|
@@ -188,6 +188,8 @@ module Expect
|
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
# 声明完成后固定模式;有参数块保留调用方 self,无参数块使用模式 DSL。
|
|
191
|
+
# Session 与 Responder 共用的内部入口,不通过反射绕过方法边界。
|
|
192
|
+
# @api private
|
|
191
193
|
def run_expect(sessions, patterns, timeout, deadline:, consume:, reset_timeout_on_read:,
|
|
192
194
|
match_by_position: false, search_window: nil, &block)
|
|
193
195
|
unless search_window.nil? || (search_window.is_a?(Integer) && search_window.positive?)
|
|
@@ -205,5 +207,7 @@ module Expect
|
|
|
205
207
|
Matcher.new(pattern_list, timeout, deadline:, consume:, reset_timeout_on_read:, match_by_position:,
|
|
206
208
|
search_window:).run
|
|
207
209
|
end
|
|
210
|
+
|
|
211
|
+
public :run_expect
|
|
208
212
|
end
|
|
209
213
|
end
|
data/lib/expect/relay/writer.rb
CHANGED
|
@@ -89,7 +89,7 @@ module Expect
|
|
|
89
89
|
|
|
90
90
|
# 诊断可嵌套写入;外层只报告本游标的进度,原超时及其内层进度保留在 cause。
|
|
91
91
|
def trace_delivery(data)
|
|
92
|
-
target.
|
|
92
|
+
target.record_sent(data)
|
|
93
93
|
rescue WriteTimeout
|
|
94
94
|
raise WriteTimeout.new("relay interrupted by a diagnostic timeout", bytes_written: @offset)
|
|
95
95
|
end
|
data/lib/expect/responder.rb
CHANGED
|
@@ -38,9 +38,9 @@ module Expect
|
|
|
38
38
|
raise ArgumentError, "provide patterns or a pattern block, not both" if block && !patterns.empty?
|
|
39
39
|
|
|
40
40
|
rules = @rules.dup.freeze
|
|
41
|
-
Expect.
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
Expect.run_expect(
|
|
42
|
+
[session], [], timeout, deadline:, consume: true,
|
|
43
|
+
reset_timeout_on_read: false, match_by_position: true, search_window:
|
|
44
44
|
) do |list|
|
|
45
45
|
patterns.each do |pattern|
|
|
46
46
|
case pattern
|
data/lib/expect/result.rb
CHANGED
|
@@ -21,38 +21,60 @@ module Expect
|
|
|
21
21
|
# 按成员声明顺序返回字段数组。
|
|
22
22
|
# @!method deconstruct_keys(keys)
|
|
23
23
|
# 按指定键解构结果,nil 表示全部字段。
|
|
24
|
-
Result = Data.define(:
|
|
24
|
+
Result = Data.define(:status, :match_number, :error, :match, :before, :after, :session, :captures) do
|
|
25
25
|
# 构造独立不可变快照,复制字符串和捕获数组。
|
|
26
26
|
# @param captures [Array<String, nil>] 按声明顺序保存捕获组,未参与的组保留 nil。
|
|
27
|
-
def initialize(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
def initialize(status:, match_number: nil, error: nil, match: nil, before: nil, after: nil,
|
|
28
|
+
session: nil, captures: [])
|
|
29
|
+
raise ArgumentError, "captures must be an Array" unless captures.is_a?(Array)
|
|
30
|
+
|
|
31
|
+
validate_outcome!(status, match_number, error, match, after, captures)
|
|
32
|
+
super(status:, match_number:, error:, session:, match: copy_text(match), before: copy_text(before),
|
|
33
|
+
after: copy_text(after), captures: captures.map { |value| copy_text(value) }.freeze)
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
# 诊断只显示匹配序号和错误状态,不检查原始文本、异常或来源对象。
|
|
34
37
|
# @return [String] 不包含协议内容的结果摘要。
|
|
35
|
-
def inspect
|
|
36
|
-
state = if error.nil? then "none"
|
|
37
|
-
elsif error.equal?(:eof) then "eof"
|
|
38
|
-
elsif error.equal?(:timeout) then "timeout"
|
|
39
|
-
else "exception"
|
|
40
|
-
end
|
|
41
|
-
index = number.is_a?(Integer) ? number.to_s : "nil"
|
|
42
|
-
"#<#{self.class} number=#{index} error=#{state}>"
|
|
43
|
-
end
|
|
38
|
+
def inspect = "#<#{self.class} status=#{status} match_number=#{match_number.inspect}>"
|
|
44
39
|
|
|
45
40
|
# Data 的默认 to_s 独立于 inspect;字符串插值同样只使用安全摘要。
|
|
46
41
|
# @return [String] 与 inspect 相同的结果摘要。
|
|
47
42
|
def to_s = inspect
|
|
48
43
|
|
|
49
44
|
# 是否命中文本;EOF 和超时没有模式序号。
|
|
50
|
-
def matched? =
|
|
45
|
+
def matched? = status == :matched
|
|
51
46
|
|
|
52
47
|
# 是否因等待期限到达而返回。
|
|
53
|
-
def timeout? =
|
|
48
|
+
def timeout? = status == :timeout
|
|
54
49
|
|
|
55
50
|
# 是否读取源已经结束,子进程状态由会话单独查询。
|
|
56
|
-
def eof? =
|
|
51
|
+
def eof? = status == :eof
|
|
52
|
+
|
|
53
|
+
# 是否因原始 IO 异常结束;等待事件不属于异常。
|
|
54
|
+
def error? = status == :error
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# 正文和捕获使用同一快照边界;不转换任意对象,也不冻结调用方的字符串。
|
|
59
|
+
def copy_text(value)
|
|
60
|
+
return nil if value.nil?
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "result text must be a String or nil" unless value.is_a?(String)
|
|
63
|
+
|
|
64
|
+
value.dup.freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 状态是唯一判据;new、[] 和 with 都经过同一边界,不能产生相互矛盾的结果。
|
|
68
|
+
def validate_outcome!(status, match_number, error, match, after, captures)
|
|
69
|
+
valid = case status
|
|
70
|
+
when :matched
|
|
71
|
+
match_number.is_a?(Integer) && match_number.positive? && match.is_a?(String) && error.nil?
|
|
72
|
+
when :eof, :timeout, :error
|
|
73
|
+
match_number.nil? && match.nil? && after.nil? && captures.empty? &&
|
|
74
|
+
(status == :error ? error.is_a?(Exception) : error.nil?)
|
|
75
|
+
else false
|
|
76
|
+
end
|
|
77
|
+
raise ArgumentError, "inconsistent result status and fields" unless valid
|
|
78
|
+
end
|
|
57
79
|
end
|
|
58
80
|
end
|
data/lib/expect/session.rb
CHANGED
|
@@ -50,8 +50,8 @@ module Expect
|
|
|
50
50
|
# 等待自身输入,返回 Result;匹配消费和接收重置策略只对本轮有效。
|
|
51
51
|
def expect(*patterns, timeout: self.timeout, deadline: nil, consume: true, reset_timeout_on_read: false,
|
|
52
52
|
search_window: nil, &)
|
|
53
|
-
Expect.
|
|
54
|
-
|
|
53
|
+
Expect.run_expect([self], patterns, timeout,
|
|
54
|
+
deadline:, consume:, reset_timeout_on_read:, search_window:, &)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# 在新控制终端中执行命令并同步确认 exec 结果;同一会话只能启动一次。
|
|
@@ -119,12 +119,12 @@ module Expect
|
|
|
119
119
|
def match = @last_result&.match
|
|
120
120
|
|
|
121
121
|
# 最近命中的模式序号;EOF、超时或尚无结果时为 nil。
|
|
122
|
-
def match_number = @last_result&.
|
|
122
|
+
def match_number = @last_result&.match_number
|
|
123
123
|
|
|
124
124
|
# 最近捕获值的不可变数组;未参与的捕获为 nil。
|
|
125
125
|
def captures = @last_result&.captures || []
|
|
126
126
|
|
|
127
|
-
#
|
|
127
|
+
# 最近结果的原始 IO 异常;等待状态通过 last_result.status 查询。
|
|
128
128
|
def error = @last_result&.error
|
|
129
129
|
|
|
130
130
|
# 返回缓冲副本,防止调用方原地修改绕过裁剪规则。
|
|
@@ -309,7 +309,8 @@ module Expect
|
|
|
309
309
|
# @api private
|
|
310
310
|
def record_match(pattern, position, consume: true)
|
|
311
311
|
offset, length, captures = position
|
|
312
|
-
@last_result = Result.new(
|
|
312
|
+
@last_result = Result.new(status: :matched, match_number: pattern.match_number,
|
|
313
|
+
before: @buffer.byteslice(0, offset),
|
|
313
314
|
match: @buffer.byteslice(offset, length), after: @buffer.byteslice((offset + length)..),
|
|
314
315
|
session: self, captures:)
|
|
315
316
|
if consume
|
|
@@ -320,22 +321,28 @@ module Expect
|
|
|
320
321
|
end
|
|
321
322
|
# 诊断回调可能嵌套等待;恢复本次结果后再交给正式模式回调,不能返回内层等待的结果。
|
|
322
323
|
result = @last_result
|
|
323
|
-
trace { "matched pattern #{pattern.
|
|
324
|
+
trace { "matched pattern #{pattern.match_number}" }
|
|
324
325
|
@last_result = result
|
|
325
326
|
end
|
|
326
327
|
|
|
327
|
-
#
|
|
328
|
+
# 记录原始 IO 异常,保留当前缓冲快照并清除旧匹配及捕获组。
|
|
328
329
|
# @api private
|
|
329
330
|
def record_error(error)
|
|
330
331
|
# Result 自行复制冻结文本,无须先通过公开 buffer 再复制一遍。
|
|
331
|
-
@last_result = Result.new(error:, before: @buffer, session: self
|
|
332
|
+
@last_result = Result.new(status: :error, error:, before: @buffer, session: self)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# 超时是本次等待的正常终止事件,未消费输入继续留在缓冲中。
|
|
336
|
+
# @api private
|
|
337
|
+
def record_timeout
|
|
338
|
+
@last_result = Result.new(status: :timeout, before: @buffer, session: self)
|
|
332
339
|
end
|
|
333
340
|
|
|
334
341
|
# 输入结束时将剩余缓冲放入 before 并清空,尝试回收但不终止仍活跃的子进程。
|
|
335
342
|
# @api private
|
|
336
343
|
def record_eof
|
|
337
344
|
process_status
|
|
338
|
-
|
|
345
|
+
@last_result = Result.new(status: :eof, before: @buffer, session: self)
|
|
339
346
|
clear_buffer
|
|
340
347
|
@last_result
|
|
341
348
|
end
|
|
@@ -403,18 +410,8 @@ module Expect
|
|
|
403
410
|
@buffer_generation = 0
|
|
404
411
|
@received_bytes = 0
|
|
405
412
|
@buffer_discarded_bytes = 0
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
@literal_prefix_tables = {}
|
|
409
|
-
@pending_writes = []
|
|
410
|
-
@interact_inputs = {}.compare_by_identity
|
|
411
|
-
@interact_output = nil
|
|
412
|
-
@interaction_buffer = @relay_owner = @relay_callback = nil
|
|
413
|
-
@relay_history = "".b
|
|
414
|
-
@relay_history_sequences = {}
|
|
415
|
-
@secrets = @transcript_redactor = nil
|
|
416
|
-
@diagnostic_redactors = {}
|
|
417
|
-
@logger = @transcript = nil
|
|
413
|
+
initialize_interaction
|
|
414
|
+
initialize_logging
|
|
418
415
|
@last_result = @command = nil
|
|
419
416
|
@closed = @eof = @receiving = false
|
|
420
417
|
self.timeout = timeout
|
data/lib/expect/version.rb
CHANGED
data/sig/expect.rbs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
module Expect
|
|
3
3
|
type duration = Numeric
|
|
4
4
|
type pattern = String | Regexp | :eof | :timeout
|
|
5
|
-
type
|
|
6
|
-
type
|
|
5
|
+
type result_status = :matched | :timeout | :eof | :error
|
|
6
|
+
type result_error = Exception | nil
|
|
7
|
+
type result_fields = { status: result_status, match_number: Integer?, error: result_error, match: String?, before: String?, after: String?, session: Session?, captures: Array[String?] }
|
|
7
8
|
type diagnostic_event = { event: Symbol, pid: Integer?, fd: Integer?, message: String }
|
|
8
9
|
|
|
9
10
|
interface _Writer
|
|
@@ -189,7 +190,8 @@ module Expect
|
|
|
189
190
|
|
|
190
191
|
class Result < Data
|
|
191
192
|
def self.new: (
|
|
192
|
-
|
|
193
|
+
status: result_status,
|
|
194
|
+
?match_number: Integer?,
|
|
193
195
|
?error: result_error,
|
|
194
196
|
?match: String?,
|
|
195
197
|
?before: String?,
|
|
@@ -197,10 +199,11 @@ module Expect
|
|
|
197
199
|
?session: Session?,
|
|
198
200
|
?captures: Array[String?]
|
|
199
201
|
) -> Result
|
|
200
|
-
| (?Integer?, ?result_error, ?String?, ?String?, ?String?, ?Session?, ?Array[String?]) -> Result
|
|
202
|
+
| (result_status, ?Integer?, ?result_error, ?String?, ?String?, ?String?, ?Session?, ?Array[String?]) -> Result
|
|
201
203
|
|
|
202
204
|
def self.[]: (
|
|
203
|
-
|
|
205
|
+
status: result_status,
|
|
206
|
+
?match_number: Integer?,
|
|
204
207
|
?error: result_error,
|
|
205
208
|
?match: String?,
|
|
206
209
|
?before: String?,
|
|
@@ -208,7 +211,7 @@ module Expect
|
|
|
208
211
|
?session: Session?,
|
|
209
212
|
?captures: Array[String?]
|
|
210
213
|
) -> Result
|
|
211
|
-
| (?Integer?, ?result_error, ?String?, ?String?, ?String?, ?Session?, ?Array[String?]) -> Result
|
|
214
|
+
| (result_status, ?Integer?, ?result_error, ?String?, ?String?, ?String?, ?Session?, ?Array[String?]) -> Result
|
|
212
215
|
|
|
213
216
|
def self.members: () -> Array[Symbol]
|
|
214
217
|
|
|
@@ -216,7 +219,8 @@ module Expect
|
|
|
216
219
|
|
|
217
220
|
def members: () -> Array[Symbol]
|
|
218
221
|
|
|
219
|
-
attr_reader
|
|
222
|
+
attr_reader status: result_status
|
|
223
|
+
attr_reader match_number: Integer?
|
|
220
224
|
attr_reader error: result_error
|
|
221
225
|
attr_reader match: String?
|
|
222
226
|
attr_reader before: String?
|
|
@@ -225,7 +229,8 @@ module Expect
|
|
|
225
229
|
attr_reader captures: Array[String?]
|
|
226
230
|
|
|
227
231
|
def initialize: (
|
|
228
|
-
|
|
232
|
+
status: result_status,
|
|
233
|
+
?match_number: Integer?,
|
|
229
234
|
?error: result_error,
|
|
230
235
|
?match: String?,
|
|
231
236
|
?before: String?,
|
|
@@ -236,6 +241,8 @@ module Expect
|
|
|
236
241
|
|
|
237
242
|
def matched?: () -> bool
|
|
238
243
|
|
|
244
|
+
def error?: () -> bool
|
|
245
|
+
|
|
239
246
|
def timeout?: () -> bool
|
|
240
247
|
|
|
241
248
|
def eof?: () -> bool
|
|
@@ -244,10 +251,11 @@ module Expect
|
|
|
244
251
|
|
|
245
252
|
def to_s: () -> String
|
|
246
253
|
|
|
247
|
-
def deconstruct: () -> [Integer?, result_error, String?, String?, String?, Session?, Array[String?]]
|
|
254
|
+
def deconstruct: () -> [result_status, Integer?, result_error, String?, String?, String?, Session?, Array[String?]]
|
|
248
255
|
|
|
249
256
|
def with: (
|
|
250
|
-
?
|
|
257
|
+
?status: result_status,
|
|
258
|
+
?match_number: Integer?,
|
|
251
259
|
?error: result_error,
|
|
252
260
|
?match: String?,
|
|
253
261
|
?before: String?,
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: expect-pty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- expect-pty contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: io-console
|
|
@@ -114,7 +114,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
114
114
|
requirements:
|
|
115
115
|
- - ">="
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version:
|
|
117
|
+
version: 4.0.6
|
|
118
118
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
119
|
requirements:
|
|
120
120
|
- - ">="
|