expect-pty 0.7.5 → 0.7.7
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 +12 -0
- data/docs/API.md +1 -0
- data/lib/expect/interaction.rb +22 -72
- data/lib/expect/literal_prefix.rb +63 -0
- data/lib/expect/logging.rb +6 -0
- data/lib/expect/matcher.rb +30 -16
- data/lib/expect/redactor.rb +6 -11
- data/lib/expect/relay.rb +20 -14
- data/lib/expect/session.rb +18 -22
- data/lib/expect/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45e6676261fec4b5bcd7db41298fd513a47da86308a29f53702311a6b5219f59
|
|
4
|
+
data.tar.gz: 19354431d871147ebd778424288ea2d3a2f788d92e8674f6c779b8c560b23f1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efdb068466acf441e74d839e9830d31f59ab18bf989726319dc6f97fb049d34a913ef53fdc399b6287bebc94d3993a5b97fb8fe41965c3c1a5e6ed3fb9aba924
|
|
7
|
+
data.tar.gz: c4c4d20ef93293c8958da3ffcf6f2ebc0245b37b07caea6fe5f2af3b9ef73ba29d365134f5c7be1a693a43a3c163724143a805ad4f19d27266df7ec7e9485717
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.7 - 2026-10-11
|
|
6
|
+
|
|
7
|
+
- 修复人工接管时终端模式交接中断、恢复异常覆盖业务主异常的问题;分别尝试恢复两侧输出、转义规则和终端模式。
|
|
8
|
+
- 修复 Matcher 与 Relay 在缓冲移交或归还期间被取消时丢失未消费输入的问题,仅在内部所有权交接期间延迟异步中断。
|
|
9
|
+
- 使用对象身份索引选择背压读取来源,减少多会话互为转发目标时的重复遍历,保持来源顺序和背压行为。
|
|
10
|
+
- SSH 示例保留 known_hosts 路径中的百分号,并在启动前拒绝 OpenSSH 会再次展开的 `${` 路径表达式。
|
|
11
|
+
|
|
12
|
+
## 0.7.6 - 2026-10-09
|
|
13
|
+
|
|
14
|
+
- 修复会话关闭时日志异常覆盖原始错误的问题,保留进程等待、线程中断或句柄关闭的失败原因,并继续尝试完成诊断与 transcript 收尾。
|
|
15
|
+
- 优化长秘密的流尾脱敏和长字面转义扫描,减少近似前缀输入下的耗时与临时对象,保持分片、重叠和部分秘密的过滤行为。
|
|
16
|
+
|
|
5
17
|
## 0.7.5 - 2026-10-08
|
|
6
18
|
|
|
7
19
|
- 修复 PTY、启动错误管道和工厂初始化的异步中断交接窗口;线程中断或终止后立即关闭已取得的句柄,保持用户初始化协议可中断。
|
data/docs/API.md
CHANGED
|
@@ -114,6 +114,7 @@ DEBUG 关闭期间仍推进分片脱敏,但这些字节不会在重新启用
|
|
|
114
114
|
|
|
115
115
|
`interact(input: $stdin, escape: nil, output: nil, timeout: nil, raw: true)` 临时建立双向转接,退出后恢复
|
|
116
116
|
outputs、转义和本地终端模式。
|
|
117
|
+
各项恢复都会尝试;恢复中的常规错误不会覆盖已有的业务异常,没有业务异常时仍报告恢复错误。
|
|
117
118
|
`output: nil` 使用默认输出,`escape: nil` 不注册退出序列;显式 false 不是缺省值,非法输出或转义会在转发前报错。
|
|
118
119
|
`raw: false` 将终端设置留给调用方。transcript、logger 及自定义 writer 同步执行,应及时返回。
|
|
119
120
|
|
data/lib/expect/interaction.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "relay_writer"
|
|
4
4
|
require_relative "relay"
|
|
5
|
+
require_relative "literal_prefix"
|
|
5
6
|
|
|
6
7
|
# 为会话补充人工接管和多路 IO 转接;核心会话定义位于 session.rb。
|
|
7
8
|
module Expect
|
|
@@ -63,20 +64,14 @@ module Expect
|
|
|
63
64
|
saved_self = outputs
|
|
64
65
|
saved_source = [source.outputs, source.sequences.dup]
|
|
65
66
|
terminal_state = prepare_interact_terminal(source) if raw
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if saved_self
|
|
75
|
-
self.outputs = saved_self
|
|
76
|
-
source.outputs, source.sequences = saved_source
|
|
77
|
-
end
|
|
78
|
-
ensure
|
|
79
|
-
restore_interact_terminal(terminal_state)
|
|
67
|
+
# 先持有恢复快照,再在同一清理作用域内切换终端;返回值交接中断不能留下 raw 模式。
|
|
68
|
+
Cleanup.always(-> { restore_interact(source, saved_self, saved_source, terminal_state) }) do
|
|
69
|
+
terminal_state&.first&.raw!
|
|
70
|
+
display = interact_display(source, output, terminal_state)
|
|
71
|
+
self.outputs = [display]
|
|
72
|
+
source.outputs = [self]
|
|
73
|
+
source.on_sequence(escape) unless escape.nil?
|
|
74
|
+
Relay.new([self, source], timeout).run
|
|
80
75
|
end
|
|
81
76
|
end
|
|
82
77
|
|
|
@@ -179,59 +174,12 @@ module Expect
|
|
|
179
174
|
sequences.each_key do |key|
|
|
180
175
|
next if key.is_a?(Regexp)
|
|
181
176
|
|
|
182
|
-
prefix_length =
|
|
177
|
+
prefix_length = LiteralPrefix.length(buffer, key, prefix_tables)
|
|
183
178
|
held = prefix_length if prefix_length > held
|
|
184
179
|
end
|
|
185
180
|
held
|
|
186
181
|
end
|
|
187
182
|
|
|
188
|
-
# KMP 在缓冲尾部寻找规则前缀,长规则只扫描相关字节一次,避免逐短前缀反复复制和比较。
|
|
189
|
-
# 短规则使用原生字符串操作,避免为小输入建立前缀表。
|
|
190
|
-
def self.literal_prefix_suffix(buffer, key, prefix_tables = {})
|
|
191
|
-
maximum = [key.bytesize - 1, buffer.bytesize].min
|
|
192
|
-
return 0 unless maximum.positive?
|
|
193
|
-
return short_literal_prefix_suffix(buffer, key, maximum) if maximum < 64
|
|
194
|
-
|
|
195
|
-
failure = key.frozen? ? (prefix_tables[key] ||= []) : []
|
|
196
|
-
literal_prefix_failure_table(key, failure, maximum)
|
|
197
|
-
matched = 0
|
|
198
|
-
index = buffer.bytesize - maximum
|
|
199
|
-
while index < buffer.bytesize
|
|
200
|
-
byte = buffer.getbyte(index)
|
|
201
|
-
matched = failure[matched - 1] while matched.positive? && byte != key.getbyte(matched)
|
|
202
|
-
matched += 1 if byte == key.getbyte(matched)
|
|
203
|
-
index += 1
|
|
204
|
-
end
|
|
205
|
-
matched
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def self.literal_prefix_failure_table(key, failure, length)
|
|
209
|
-
failure << 0 if failure.empty?
|
|
210
|
-
matched = failure.last
|
|
211
|
-
index = failure.length
|
|
212
|
-
while index < length
|
|
213
|
-
byte = key.getbyte(index)
|
|
214
|
-
if byte == key.getbyte(matched)
|
|
215
|
-
matched += 1
|
|
216
|
-
failure << matched
|
|
217
|
-
index += 1
|
|
218
|
-
elsif matched.positive?
|
|
219
|
-
matched = failure[matched - 1]
|
|
220
|
-
else
|
|
221
|
-
failure << 0
|
|
222
|
-
index += 1
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
failure
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def self.short_literal_prefix_suffix(buffer, key, maximum)
|
|
229
|
-
maximum.downto(1) do |length|
|
|
230
|
-
return length if buffer.end_with?(key.byteslice(0, length))
|
|
231
|
-
end
|
|
232
|
-
0
|
|
233
|
-
end
|
|
234
|
-
|
|
235
183
|
# 已转发历史有界保留,固定 UTF-8 正则不能从续字节开始匹配。
|
|
236
184
|
def self.trim_history(history, data, limit:, utf8:)
|
|
237
185
|
history << data
|
|
@@ -243,9 +191,7 @@ module Expect
|
|
|
243
191
|
history.slice!(0) while (byte = history.getbyte(0)) && (0x80..0xBF).cover?(byte)
|
|
244
192
|
end
|
|
245
193
|
|
|
246
|
-
private_class_method :expired?, :handle_escape, :scan_sequences, :hold_literal_prefix,
|
|
247
|
-
:literal_prefix_suffix, :literal_prefix_failure_table,
|
|
248
|
-
:short_literal_prefix_suffix, :trim_history
|
|
194
|
+
private_class_method :expired?, :handle_escape, :scan_sequences, :hold_literal_prefix, :trim_history
|
|
249
195
|
|
|
250
196
|
# 为当前数据块冻结目标选择并各建一个发送游标;此后修改 outputs 只影响后续数据。
|
|
251
197
|
# 调用方须先排空旧游标;显示转换也只做一次,短写重试时不能重复转换 CRLF。
|
|
@@ -316,16 +262,20 @@ module Expect
|
|
|
316
262
|
@interact_inputs[input] ||= Expect.open(input)
|
|
317
263
|
end
|
|
318
264
|
|
|
319
|
-
#
|
|
265
|
+
# 只取得快照;实际 raw! 由持有快照的 interact 在清理作用域内执行。
|
|
320
266
|
def prepare_interact_terminal(source)
|
|
321
267
|
return unless source.tty?
|
|
322
268
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
269
|
+
io = source.to_io
|
|
270
|
+
[io, io.console_mode]
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# 独立尝试每项恢复;常规恢复错误不能覆盖业务主异常,新的中断仍按 Cleanup 契约传播。
|
|
274
|
+
def restore_interact(source, saved_self, saved_source, terminal_state)
|
|
275
|
+
Cleanup.always(-> { restore_interact_terminal(terminal_state) }) do
|
|
276
|
+
Cleanup.always(-> { source.sequences = saved_source.last }) do
|
|
277
|
+
Cleanup.always(-> { source.outputs = saved_source.first }) { self.outputs = saved_self }
|
|
278
|
+
end
|
|
329
279
|
end
|
|
330
280
|
end
|
|
331
281
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expect
|
|
4
|
+
# 计算字节窗口尾部与规则真前缀重合的最长长度;转义暂存和流尾脱敏使用同一边界判断。
|
|
5
|
+
# 不持有输入或修改缓冲,调用方可为冻结规则提供前缀表缓存;短规则不建立表。
|
|
6
|
+
# @api private
|
|
7
|
+
module LiteralPrefix
|
|
8
|
+
def self.length(buffer, pattern, prefix_tables = nil)
|
|
9
|
+
maximum = [pattern.bytesize - 1, buffer.bytesize].min
|
|
10
|
+
return 0 unless maximum.positive?
|
|
11
|
+
|
|
12
|
+
# 尾字节不在候选前缀内时无需扫描;最长候选本身命中时也不必构造 KMP 表。
|
|
13
|
+
ending = pattern.rindex(buffer.byteslice(-1, 1), maximum - 1)
|
|
14
|
+
return 0 unless ending
|
|
15
|
+
|
|
16
|
+
maximum = ending + 1
|
|
17
|
+
return maximum if buffer.end_with?(pattern.byteslice(0, maximum))
|
|
18
|
+
return short_length(buffer, pattern, maximum - 1) if maximum < 64
|
|
19
|
+
|
|
20
|
+
failure = prefix_tables && pattern.frozen? ? (prefix_tables[pattern] ||= []) : []
|
|
21
|
+
extend_failure_table(pattern, failure, maximum)
|
|
22
|
+
matched = 0
|
|
23
|
+
index = buffer.bytesize - maximum
|
|
24
|
+
while index < buffer.bytesize
|
|
25
|
+
byte = buffer.getbyte(index)
|
|
26
|
+
matched = failure[matched - 1] while matched.positive? && byte != pattern.getbyte(matched)
|
|
27
|
+
matched += 1 if byte == pattern.getbyte(matched)
|
|
28
|
+
index += 1
|
|
29
|
+
end
|
|
30
|
+
matched
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.extend_failure_table(pattern, failure, length)
|
|
34
|
+
failure << 0 if failure.empty?
|
|
35
|
+
matched = failure.last
|
|
36
|
+
index = failure.length
|
|
37
|
+
while index < length
|
|
38
|
+
byte = pattern.getbyte(index)
|
|
39
|
+
if byte == pattern.getbyte(matched)
|
|
40
|
+
matched += 1
|
|
41
|
+
failure << matched
|
|
42
|
+
index += 1
|
|
43
|
+
elsif matched.positive?
|
|
44
|
+
matched = failure[matched - 1]
|
|
45
|
+
else
|
|
46
|
+
failure << 0
|
|
47
|
+
index += 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.short_length(buffer, pattern, maximum)
|
|
53
|
+
maximum.downto(1) do |length|
|
|
54
|
+
return length if buffer.end_with?(pattern.byteslice(0, length))
|
|
55
|
+
end
|
|
56
|
+
0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private_class_method :extend_failure_table, :short_length
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private_constant :LiteralPrefix
|
|
63
|
+
end
|
data/lib/expect/logging.rb
CHANGED
|
@@ -87,6 +87,12 @@ module Expect
|
|
|
87
87
|
|
|
88
88
|
private
|
|
89
89
|
|
|
90
|
+
# 关闭时两种记录都要尝试完成;后续 writer 的常规异常不能覆盖先发生的诊断异常。
|
|
91
|
+
# 不关闭借用目标,新的 Interrupt/SystemExit 仍由 Cleanup 原样传播。
|
|
92
|
+
def finish_logging
|
|
93
|
+
Cleanup.always(-> { self.transcript = nil }) { flush_diagnostics }
|
|
94
|
+
end
|
|
95
|
+
|
|
90
96
|
# 向目标写入并在支持时立即 flush,使日志和终端输出及时可见。
|
|
91
97
|
# 这里遵循同步写入协议,不受 Matcher 的 IO 等待期限中断;慢目标转接应使用 Relay。
|
|
92
98
|
# 短写只推进已确认的字节数;目标抛错时不猜测它是否已经产生副作用。
|
data/lib/expect/matcher.rb
CHANGED
|
@@ -25,17 +25,7 @@ module Expect
|
|
|
25
25
|
# 运行匹配状态机;内部 :retry 表示继续循环,最终返回一个 Result。
|
|
26
26
|
def run
|
|
27
27
|
@relay_buffers = {}.compare_by_identity
|
|
28
|
-
|
|
29
|
-
buffer = session.interaction_buffer
|
|
30
|
-
if buffer
|
|
31
|
-
# 转义回调中的显式匹配临时接管读取,先消费转接已经预读的尾部。
|
|
32
|
-
@relay_buffers[session] = buffer
|
|
33
|
-
session.interaction_buffer = nil
|
|
34
|
-
session.restore_relay_buffer(buffer)
|
|
35
|
-
buffer.clear
|
|
36
|
-
end
|
|
37
|
-
session.reset_result
|
|
38
|
-
end
|
|
28
|
+
prepare_sessions
|
|
39
29
|
loop do
|
|
40
30
|
# 先消费已缓冲的匹配,再处理 EOF,最后读取;避免进程退出时丢失最后一个匹配。
|
|
41
31
|
result = if !@expired_eof_continuation && !hard_expired? && (matched = find_match)
|
|
@@ -50,15 +40,39 @@ module Expect
|
|
|
50
40
|
return result unless result == :retry
|
|
51
41
|
end
|
|
52
42
|
ensure
|
|
53
|
-
|
|
54
|
-
@relay_buffers.each do |session, buffer|
|
|
55
|
-
buffer.replace(session.take_buffer)
|
|
56
|
-
session.interaction_buffer = buffer
|
|
57
|
-
end
|
|
43
|
+
restore_relay_buffers
|
|
58
44
|
end
|
|
59
45
|
|
|
60
46
|
private
|
|
61
47
|
|
|
48
|
+
# 转义回调中的匹配临时接管预读尾部;缓冲登记和移交是一个不可拆开的状态变更。
|
|
49
|
+
# 仅内部所有权操作延迟异步取消,扫描、IO、日志及用户回调仍可立即中断。
|
|
50
|
+
def prepare_sessions
|
|
51
|
+
@sessions.each do |session|
|
|
52
|
+
if (buffer = session.interaction_buffer)
|
|
53
|
+
Thread.handle_interrupt(Object => :never) do
|
|
54
|
+
@relay_buffers[session] = buffer
|
|
55
|
+
session.interaction_buffer = nil
|
|
56
|
+
session.restore_relay_buffer(buffer)
|
|
57
|
+
buffer.clear
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
session.reset_result
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 先归还所有借用来源再交付新的取消;返回尾部与恢复原缓冲对象不能被中断拆开。
|
|
65
|
+
def restore_relay_buffers
|
|
66
|
+
return if @relay_buffers.empty?
|
|
67
|
+
|
|
68
|
+
Thread.handle_interrupt(Object => :never) do
|
|
69
|
+
@relay_buffers.each do |session, buffer|
|
|
70
|
+
buffer.replace(session.take_buffer)
|
|
71
|
+
session.interaction_buffer = buffer
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
62
76
|
# 按声明组、会话、模式的顺序寻找首个匹配,不按文本中的出现位置重新排序。
|
|
63
77
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 声明优先级与单轮缓冲须在同一次扫描保持一致。
|
|
64
78
|
def find_match
|
data/lib/expect/redactor.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "literal_prefix"
|
|
4
|
+
|
|
3
5
|
module Expect
|
|
4
6
|
# 单个日志字节流的过滤器。保留最长秘密长度减一的尾部,跨 write/read 分片仍可识别。
|
|
5
7
|
# 掩码与原字节一起留存;重叠命中的区间取并集,已经输出的掩码不重复生成。
|
|
@@ -78,20 +80,13 @@ module Expect
|
|
|
78
80
|
|
|
79
81
|
# 流关闭时无法再等待后续字节,默认隐藏与秘密开头一致的未完成尾部。
|
|
80
82
|
def mark_partial_secrets
|
|
81
|
-
|
|
82
|
-
return unless tail
|
|
83
|
+
return if @pending.empty?
|
|
83
84
|
|
|
84
85
|
@patterns.each do |pattern|
|
|
85
|
-
length =
|
|
86
|
-
|
|
87
|
-
next unless length.positive? && (offset = pattern.rindex(tail, length - 1))
|
|
88
|
-
|
|
89
|
-
(offset + 1).downto(1) do |candidate|
|
|
90
|
-
next unless @pending.end_with?(pattern.byteslice(0, candidate))
|
|
86
|
+
length = LiteralPrefix.length(@pending, pattern)
|
|
87
|
+
next unless length.positive?
|
|
91
88
|
|
|
92
|
-
|
|
93
|
-
break
|
|
94
|
-
end
|
|
89
|
+
@hidden[-length, length] = "\1" * length
|
|
95
90
|
end
|
|
96
91
|
end
|
|
97
92
|
|
data/lib/expect/relay.rb
CHANGED
|
@@ -142,21 +142,27 @@ module Expect
|
|
|
142
142
|
|
|
143
143
|
@sessions.each { |session| session.relay_owner = @token }
|
|
144
144
|
end
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@
|
|
148
|
-
|
|
145
|
+
# take_buffer 已移出会话后,必须先登记到本轮账本才能交付异步取消;此处不执行 IO 或用户回调。
|
|
146
|
+
Thread.handle_interrupt(Object => :never) do
|
|
147
|
+
@sessions.each do |session|
|
|
148
|
+
@previous[session] = session.interaction_buffer
|
|
149
|
+
@buffers[session] = session.take_buffer
|
|
150
|
+
session.interaction_buffer = @buffers.fetch(session)
|
|
151
|
+
end
|
|
149
152
|
end
|
|
150
153
|
end
|
|
151
154
|
|
|
152
155
|
# 未处理输入还给会话,已排队字节仍归游标;构造中断也只恢复已转移的缓冲及自己的标记。
|
|
153
156
|
def restore_sources
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
# 归还多来源缓冲和释放 token 属于同一交接,不能只恢复前几个来源就响应取消。
|
|
158
|
+
Thread.handle_interrupt(Object => :never) do
|
|
159
|
+
@previous.each { |session, buffer| session.interaction_buffer = buffer }
|
|
160
|
+
@buffers.each { |session, buffer| session.restore_relay_buffer(buffer) }
|
|
161
|
+
ensure
|
|
162
|
+
OWNERSHIP_MUTEX.synchronize do
|
|
163
|
+
@sessions.each do |session|
|
|
164
|
+
session.relay_owner = nil if session.relay_owner.equal?(@token)
|
|
165
|
+
end
|
|
160
166
|
end
|
|
161
167
|
end
|
|
162
168
|
end
|
|
@@ -184,13 +190,13 @@ module Expect
|
|
|
184
190
|
# 背压源暂停吸收新输入,限制排队增长;作为写入目标的会话仍需读取以解除双向等待。
|
|
185
191
|
def read_sources(pending)
|
|
186
192
|
# 输出目标可能也在等待我们读取;这些来源即使有待发送数据也必须继续排空。
|
|
187
|
-
targets = pending.
|
|
188
|
-
output.target if output.target.is_a?(Session)
|
|
193
|
+
targets = pending.each_with_object({}.compare_by_identity) do |output, index|
|
|
194
|
+
index[output.target] = true if output.target.is_a?(Session)
|
|
189
195
|
end
|
|
190
196
|
sources = @active.reject do |session|
|
|
191
|
-
session.pending_output? && targets.
|
|
197
|
+
session.pending_output? && !targets.key?(session)
|
|
192
198
|
end
|
|
193
|
-
(sources + targets).uniq { |session| session.to_io.object_id }.reject(&:eof?)
|
|
199
|
+
(sources + targets.keys).uniq { |session| session.to_io.object_id }.reject(&:eof?)
|
|
194
200
|
end
|
|
195
201
|
|
|
196
202
|
# 总期限结束时将可交付尾部转为发送游标,至多尝试一轮交付;余量由源会话保存。
|
data/lib/expect/session.rb
CHANGED
|
@@ -503,30 +503,26 @@ module Expect
|
|
|
503
503
|
failure ||= error
|
|
504
504
|
nil
|
|
505
505
|
end
|
|
506
|
-
#
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
506
|
+
# 日志收尾是本作用域的清理:不能覆盖进程等待/中断或已记录的句柄错误。
|
|
507
|
+
Cleanup.always(-> { finish_logging }) do
|
|
508
|
+
# IO 关闭与进程退出独立记录:软关闭可能已经 closed?,但仍保留活跃 PID。
|
|
509
|
+
cleanup.call { @resources.close_handles }
|
|
510
|
+
@closed = true
|
|
511
|
+
@interact_inputs&.delete_if do |_io, input|
|
|
512
|
+
cleanup.call do
|
|
513
|
+
input.close(graceful: false)
|
|
514
|
+
true
|
|
515
|
+
end
|
|
513
516
|
end
|
|
517
|
+
@interact_output = nil
|
|
518
|
+
@pending_writes&.clear
|
|
519
|
+
@relay_history&.clear
|
|
520
|
+
@relay_callback = nil
|
|
521
|
+
status = close_child(timeout:, term_timeout:, force:)
|
|
522
|
+
raise failure if failure
|
|
523
|
+
|
|
524
|
+
status
|
|
514
525
|
end
|
|
515
|
-
@interact_output = nil
|
|
516
|
-
@pending_writes&.clear
|
|
517
|
-
@relay_history&.clear
|
|
518
|
-
@relay_callback = nil
|
|
519
|
-
status = close_child(timeout:, term_timeout:, force:)
|
|
520
|
-
completed = true
|
|
521
|
-
status
|
|
522
|
-
ensure
|
|
523
|
-
begin
|
|
524
|
-
cleanup.call { flush_diagnostics }
|
|
525
|
-
ensure
|
|
526
|
-
cleanup.call { self.transcript = nil }
|
|
527
|
-
end
|
|
528
|
-
# 用本次流程的完成状态判断异常传播,不能误把调用者 rescue 中的异常当成当前错误。
|
|
529
|
-
raise failure if failure && completed
|
|
530
526
|
end
|
|
531
527
|
|
|
532
528
|
# 句柄清理失败不改变进程策略;未回收 PID 保留给重复关闭或终结器继续处理。
|
data/lib/expect/version.rb
CHANGED
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.7.
|
|
4
|
+
version: 0.7.7
|
|
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-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: io-console
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- lib/expect.rb
|
|
68
68
|
- lib/expect/cleanup.rb
|
|
69
69
|
- lib/expect/interaction.rb
|
|
70
|
+
- lib/expect/literal_prefix.rb
|
|
70
71
|
- lib/expect/logging.rb
|
|
71
72
|
- lib/expect/matcher.rb
|
|
72
73
|
- lib/expect/pattern.rb
|