expect-pty 0.5.3 → 0.7.1
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 +55 -1
- data/README.md +122 -130
- data/docs/API.md +113 -0
- data/docs/MIGRATION.md +68 -0
- data/lib/expect/cleanup.rb +34 -0
- data/lib/expect/interaction.rb +228 -210
- data/lib/expect/logging.rb +117 -162
- data/lib/expect/matcher.rb +42 -38
- data/lib/expect/pattern.rb +10 -3
- data/lib/expect/pattern_list.rb +39 -9
- data/lib/expect/redactor.rb +12 -5
- data/lib/expect/relay.rb +106 -81
- data/lib/expect/relay_writer.rb +7 -5
- data/lib/expect/result.rb +33 -9
- data/lib/expect/session.rb +554 -0
- data/lib/expect/session_resources.rb +19 -18
- data/lib/expect/version.rb +2 -2
- data/lib/expect.rb +68 -578
- data/sig/expect.rbs +279 -0
- metadata +14 -123
- data/.rubocop.yml +0 -66
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -38
- data/benchmark/matching.rb +0 -98
- data/benchmark/redactor.rb +0 -64
- data/benchmark/relay.rb +0 -48
- data/benchmark/scaling.rb +0 -70
- data/benchmark/send_slow.rb +0 -41
- data/benchmark/support.rb +0 -114
- data/docs/COMPATIBILITY.md +0 -98
- data/docs/INTERNAL_CONTRACTS.md +0 -154
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -456
- data/examples/dialogue.rb +0 -30
- data/examples/kibitz/README.md +0 -81
- data/examples/kibitz/kibitz.rb +0 -142
- data/examples/kibitz/test_kibitz.rb +0 -37
- data/examples/ssh_auto.rb +0 -94
- data/examples/ssh_interact.rb +0 -159
- data/examples/ssh_login.rb +0 -64
- data/expect-pty.gemspec +0 -33
- data/lib/expect/configuration.rb +0 -122
- data/lib/expect/terminal.rb +0 -102
- data/script/ci +0 -121
- data/script/release.rb +0 -319
- data/test/buffer_accounting_test.rb +0 -83
- data/test/cleanup_test.rb +0 -251
- data/test/compare_upstream.rb +0 -157
- data/test/configuration_test.rb +0 -133
- data/test/deadline_test.rb +0 -237
- data/test/diagnostics_test.rb +0 -353
- data/test/edge_case_test.rb +0 -262
- data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
- data/test/fixtures/ssh_scripts/02_output.sh +0 -5
- data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
- data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
- data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
- data/test/initialization_failure_test.rb +0 -92
- data/test/integration/README.md +0 -109
- data/test/integration/ssh_scripts.rb +0 -94
- data/test/interact_test.rb +0 -253
- data/test/interconnect_test.rb +0 -425
- data/test/io_test.rb +0 -321
- data/test/kibitz_test.rb +0 -45
- data/test/lifecycle_contract_test.rb +0 -71
- data/test/literal_scan_test.rb +0 -97
- data/test/matching_test.rb +0 -211
- data/test/multi_session_test.rb +0 -66
- data/test/ownership_sequence_test.rb +0 -208
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_interruption_test.rb +0 -296
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/relay_reentrancy_test.rb +0 -159
- data/test/release_test.rb +0 -375
- data/test/ruby_api_test.rb +0 -515
- data/test/scan_reuse_test.rb +0 -109
- data/test/script_logging_test.rb +0 -124
- data/test/support/interact_probe.rb +0 -125
- data/test/support/kibitz_probe.rb +0 -177
- data/test/support/script_probe.rb +0 -158
- data/test/terminal_cleanup_test.rb +0 -349
- data/test/test_helper.rb +0 -58
- data/test/timeout_test.rb +0 -289
- data/test/write_contract_test.rb +0 -105
data/lib/expect/logging.rb
CHANGED
|
@@ -1,200 +1,155 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "logger"
|
|
3
4
|
require_relative "redactor"
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
# 借用标准 Logger、接收记录 writer 和协议输出 writer,不接管它们的生命周期。
|
|
7
|
+
# 脱敏只作用于诊断与接收记录,协议输出和匹配缓冲始终保留原始字节。
|
|
8
|
+
module Expect
|
|
9
|
+
# 会话诊断、接收记录和协议转发的独立输出接口。
|
|
10
|
+
# 宿主 Session 初始化目标与过滤器状态,并提供 pid、fileno 作为诊断元数据。
|
|
11
|
+
# 三种输出各有边界:Logger 接收事件,transcript 接收脱敏字节,outputs 接收原协议字节。
|
|
12
|
+
module Logging
|
|
13
|
+
# 调用方提供的诊断 Logger 与原始接收记录 writer,均由调用方管理生命周期。
|
|
14
|
+
attr_reader :logger, :transcript
|
|
15
|
+
|
|
16
|
+
# Logger 自己决定级别、格式与失败策略;nil 禁用诊断,不隐式写入 stderr。
|
|
17
|
+
# 替换前先完成旧流脱敏,失败时仍保留旧 logger。
|
|
18
|
+
def logger=(target)
|
|
19
|
+
unless target.nil? || (target.respond_to?(:add) && target.respond_to?(:debug?))
|
|
20
|
+
raise ArgumentError, "logger must support add and debug?, or be nil"
|
|
21
|
+
end
|
|
22
|
+
return if @logger.equal?(target)
|
|
23
|
+
|
|
24
|
+
flush_diagnostics
|
|
25
|
+
@logger = target
|
|
26
|
+
@diagnostic_redactors = {}
|
|
17
27
|
end
|
|
18
|
-
return if @diagnostic_output.equal?(target)
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
# 秘密仅作用于本会话的接收记录和诊断,不改写匹配或 outputs 的协议字节。
|
|
30
|
+
# 先校验全部值再发布;注册是追加操作,应在首次通信前完成,不能追溯已交付的日志。
|
|
31
|
+
def redact(*secrets)
|
|
32
|
+
unless secrets.any? && secrets.all? { |secret| secret.is_a?(String) && !secret.empty? }
|
|
33
|
+
raise ArgumentError, "secrets must be nonempty Strings"
|
|
34
|
+
end
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
raise ArgumentError, "secrets must be nonempty Strings"
|
|
36
|
+
@secrets = ((@secrets || []) + secrets.map { |secret| secret.b.freeze }).uniq.freeze
|
|
37
|
+
@transcript_redactor.patterns = @secrets if @transcript_redactor
|
|
38
|
+
@diagnostic_redactors.each_value { |redactor| redactor.patterns = @secrets }
|
|
39
|
+
self
|
|
29
40
|
end
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
42
|
+
# 接收记录只采用 write 协议;文件打开、权限和关闭由调用方管理。
|
|
43
|
+
def transcript=(target)
|
|
44
|
+
raise ArgumentError, "transcript must support write, or be nil" unless target.nil? || target.respond_to?(:write)
|
|
45
|
+
return if @transcript.equal?(target)
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
raise ArgumentError, "log output must support write or call, or be nil"
|
|
47
|
+
flush_transcript
|
|
48
|
+
@transcript = target
|
|
49
|
+
@transcript_redactor = nil
|
|
41
50
|
end
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
# 向接收记录补写内容,不发送给子进程或 outputs;脱敏尾部可能延迟交付,返回 nil。
|
|
53
|
+
def write_transcript(*objects)
|
|
54
|
+
target = transcript
|
|
55
|
+
return unless target
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
data = objects.map { |object| object.to_s.b }.join
|
|
58
|
+
if @secrets
|
|
59
|
+
@transcript_redactor ||= Redactor.new(@secrets)
|
|
60
|
+
data = @transcript_redactor.append(data)
|
|
61
|
+
return if data.empty?
|
|
62
|
+
end
|
|
63
|
+
emit(target, data)
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
raise ArgumentError, "log mode must be a or w" unless %w[a w].include?(mode)
|
|
67
|
+
# 返回副本,避免外部原地修改转发关系;stdout 与其他 writer 使用相同协议。
|
|
68
|
+
def outputs = @outputs.dup
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
raise ArgumentError, "provide a log target or a block" unless target
|
|
70
|
+
# 先校验整个数组再替换,失败时保留原输出图。
|
|
71
|
+
def outputs=(targets)
|
|
72
|
+
unless targets.is_a?(Array) && targets.all? { |target| target.respond_to?(:write) }
|
|
73
|
+
raise ArgumentError, "outputs must be an Array of writers"
|
|
74
|
+
end
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
@outputs = targets.dup
|
|
62
77
|
end
|
|
63
|
-
end
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return unless target
|
|
70
|
-
|
|
71
|
-
data = objects.map { |object| object.to_s.b }.join
|
|
72
|
-
if @secrets
|
|
73
|
-
@log_redactor ||= Redactor.new(@secrets)
|
|
74
|
-
data = @log_redactor.append(data)
|
|
75
|
-
return if data.empty?
|
|
79
|
+
# 原样转发协议字节,接收记录在读取时单独写入。
|
|
80
|
+
# @api private
|
|
81
|
+
def propagate(data)
|
|
82
|
+
@outputs.each { |output| emit(output, data) }
|
|
76
83
|
end
|
|
77
|
-
target.respond_to?(:call) ? target.call(data) : emit(target, data)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# 返回监听器列表副本,避免外部原地修改转发关系。
|
|
81
|
-
def listeners = @listeners.dup
|
|
82
|
-
|
|
83
|
-
# 校验所有监听器均可写后一次性替换列表,外部数组后续修改不会影响会话。
|
|
84
|
-
def listeners=(outputs)
|
|
85
|
-
outputs = Array(outputs)
|
|
86
|
-
raise ArgumentError, "listeners must support write" unless outputs.all? { |output| output.respond_to?(:write) }
|
|
87
|
-
|
|
88
|
-
@listeners = outputs.dup
|
|
89
|
-
end
|
|
90
84
|
|
|
91
|
-
|
|
85
|
+
private
|
|
92
86
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
# 向目标写入并在支持时立即 flush,使日志和终端输出及时可见。
|
|
88
|
+
# 这里遵循同步写入协议,不受 Matcher 的 IO 等待期限中断;慢目标转接应使用 Relay。
|
|
89
|
+
# 短写只推进已确认的字节数;目标抛错时不猜测它是否已经产生副作用。
|
|
90
|
+
def emit(target, data)
|
|
91
|
+
offset = 0
|
|
92
|
+
while offset < data.bytesize
|
|
93
|
+
count = target.write(data.byteslice(offset..))
|
|
94
|
+
unless count.is_a?(Integer) && count.positive? && count <= data.bytesize - offset
|
|
95
|
+
raise IOError, "write must return the number of accepted bytes"
|
|
96
|
+
end
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
# 这里遵循同步写入协议,不受 Matcher 的 IO 等待期限中断;慢目标转接应使用 Relay。
|
|
101
|
-
# 短写只推进已确认的字节数;目标抛错时不猜测它是否已经产生副作用。
|
|
102
|
-
def emit(target, data)
|
|
103
|
-
offset = 0
|
|
104
|
-
while offset < data.bytesize
|
|
105
|
-
count = target.write(data.byteslice(offset..))
|
|
106
|
-
unless count.is_a?(Integer) && count.positive? && count <= data.bytesize - offset
|
|
107
|
-
raise IOError, "write must return the number of accepted bytes"
|
|
98
|
+
offset += count
|
|
108
99
|
end
|
|
109
|
-
|
|
110
|
-
offset += count
|
|
100
|
+
target.flush if target.respond_to?(:flush)
|
|
111
101
|
end
|
|
112
|
-
target.flush if target.respond_to?(:flush)
|
|
113
|
-
end
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return trace("buffer [FILTERED]", level: level, event: event) if event == :buffer
|
|
103
|
+
# 字节诊断在 inspect 转义之前过滤,发送与接收各自保留分片状态。
|
|
104
|
+
def trace_data(event, data)
|
|
105
|
+
return unless logger&.debug?
|
|
119
106
|
|
|
120
|
-
@
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
107
|
+
if @secrets
|
|
108
|
+
redactor = (@diagnostic_redactors[event] ||= Redactor.new(@secrets))
|
|
109
|
+
data = redactor.append(data)
|
|
110
|
+
return if data.empty?
|
|
111
|
+
end
|
|
112
|
+
trace("#{event} #{data.inspect}", severity: Logger::DEBUG, event:)
|
|
124
113
|
end
|
|
125
|
-
trace("#{event} #{data.inspect}", level: level, event: event)
|
|
126
|
-
end
|
|
127
114
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
severity = level == 1 ? :info : :debug
|
|
133
|
-
target = diagnostic_output
|
|
134
|
-
if target.respond_to?(:info) && target.respond_to?(:debug)
|
|
135
|
-
target.public_send(severity, "#{inspect}: #{message}")
|
|
136
|
-
elsif target.respond_to?(:call)
|
|
137
|
-
target.call({ event: event, level: severity, pid: pid, fd: fileno, message: message.freeze }.freeze)
|
|
138
|
-
elsif target
|
|
139
|
-
emit(target, "#{inspect}: #{message}\n")
|
|
140
|
-
else
|
|
141
|
-
warn("#{inspect}: #{message}")
|
|
115
|
+
# 向标准 Logger 提交不可变事件,不包含会话对象;格式化由 logger 完成。
|
|
116
|
+
def trace(message, severity: Logger::INFO, event: :matched)
|
|
117
|
+
logger&.add(severity, { event:, pid:, fd: fileno, message: message.freeze }.freeze, "Expect")
|
|
142
118
|
end
|
|
143
|
-
end
|
|
144
119
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
120
|
+
# 结束当前日志片段并交付过滤器尾部;重复调用不会重放已释放字节,写入异常原样传播。
|
|
121
|
+
def flush_transcript
|
|
122
|
+
loop do
|
|
123
|
+
return unless @transcript_redactor && transcript
|
|
149
124
|
|
|
150
|
-
|
|
151
|
-
|
|
125
|
+
data = @transcript_redactor.finish
|
|
126
|
+
return if data.empty?
|
|
152
127
|
|
|
153
|
-
|
|
154
|
-
|
|
128
|
+
emit(transcript, data)
|
|
129
|
+
# writer 可能重入并追加尾部;交接前仍归当前记录流。
|
|
130
|
+
end
|
|
155
131
|
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# EOF 只结束接收诊断;目标替换或显式关闭结束全部方向,发送与接收不能拼成一个秘密。
|
|
159
|
-
# GC 终结器不进入本方法,避免在回收阶段调用用户回调或持有会话对象。
|
|
160
|
-
def flush_diagnostics(event = nil)
|
|
161
|
-
return unless @diagnostic_redactors
|
|
162
|
-
|
|
163
|
-
loop do
|
|
164
|
-
delivered = false
|
|
165
|
-
# 回调可能首次创建另一个方向,也可能写入本轮已经处理的方向。
|
|
166
|
-
@diagnostic_redactors.to_a.each do |name, redactor|
|
|
167
|
-
next if event && name != event
|
|
168
132
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
133
|
+
# EOF 只结束接收诊断;目标替换或显式关闭结束全部方向,发送与接收不能拼成一个秘密。
|
|
134
|
+
# GC 终结器不进入本方法,避免在回收阶段调用用户回调或持有会话对象。
|
|
135
|
+
def flush_diagnostics(event = nil)
|
|
136
|
+
return unless @diagnostic_redactors
|
|
137
|
+
|
|
138
|
+
loop do
|
|
139
|
+
delivered = false
|
|
140
|
+
# 回调可能首次创建另一个方向,也可能写入本轮已经处理的方向。
|
|
141
|
+
@diagnostic_redactors.to_a.each do |name, redactor|
|
|
142
|
+
next if event && name != event
|
|
143
|
+
|
|
144
|
+
data = redactor.finish
|
|
145
|
+
next if data.empty?
|
|
146
|
+
|
|
147
|
+
delivered = true
|
|
148
|
+
trace("#{name} #{data.inspect}", severity: Logger::DEBUG, event: name)
|
|
149
|
+
end
|
|
150
|
+
# 目标尚未交接,新产生的尾部仍归旧诊断流;排空后才结束这次冲刷。
|
|
151
|
+
break unless delivered
|
|
174
152
|
end
|
|
175
|
-
# 目标尚未交接,新产生的尾部仍归旧诊断流;排空后才结束这次冲刷。
|
|
176
|
-
break unless delivered
|
|
177
153
|
end
|
|
178
154
|
end
|
|
179
|
-
|
|
180
|
-
# 交接日志目标和所有权,只关闭库拥有的旧文件;失败时释放新打开的文件。
|
|
181
|
-
def replace_log(target, owned: false)
|
|
182
|
-
# 重复赋值同一目标时保留原所有权,防止将库打开的文件误变成借用资源。
|
|
183
|
-
return target if log_output.equal?(target)
|
|
184
|
-
|
|
185
|
-
flush_log
|
|
186
|
-
# 尾部回调可以重入并替换目标;交接时重新读取目标和所有权,不能关闭旧的回调对象。
|
|
187
|
-
return target if log_output.equal?(target)
|
|
188
|
-
|
|
189
|
-
previous = @resources.owned_log
|
|
190
|
-
previous.close if previous && !previous.closed?
|
|
191
|
-
# 终结器只持有所属文件;借用回调可能捕获会话,不能让它经资源对象成为 GC 根。
|
|
192
|
-
@resources.owned_log = owned ? target : nil
|
|
193
|
-
@log_output = target
|
|
194
|
-
@log_redactor = nil
|
|
195
|
-
target
|
|
196
|
-
rescue Exception # rubocop:disable Lint/RescueException -- 替换失败时仍释放刚打开的文件。
|
|
197
|
-
target.close if owned && target && !target.closed?
|
|
198
|
-
raise
|
|
199
|
-
end
|
|
200
155
|
end
|
data/lib/expect/matcher.rb
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module Expect
|
|
4
4
|
# 驱动一次单会话或多会话匹配,管理模式优先级、EOF 和共享期限,不接管 IO 所有权。
|
|
5
5
|
# 调度只在扫描、回调和 IO 操作之间检查期限,不强行中断用户代码或单次正则计算。
|
|
6
|
+
# @api private
|
|
6
7
|
class Matcher
|
|
7
8
|
# 固定本次参与的会话及初始期限;已处理 EOF 的会话仅从本次等待中移除。
|
|
8
|
-
def initialize(patterns, timeout, deadline: nil)
|
|
9
|
-
@patterns = patterns
|
|
9
|
+
def initialize(patterns, timeout, deadline: nil, consume: true, reset_timeout_on_read: false)
|
|
10
|
+
@patterns = patterns.finalize!
|
|
10
11
|
@sessions = patterns.sessions
|
|
12
|
+
@groups = patterns.groups
|
|
13
|
+
@consume = consume
|
|
14
|
+
@reset_timeout_on_read = reset_timeout_on_read
|
|
11
15
|
@timeout = Expect.duration(timeout)
|
|
12
16
|
# 相对期限可因接收或 continue 重算,总期限始终固定;两者共用单调时钟。
|
|
13
17
|
@hard_deadline = deadline
|
|
14
18
|
@deadline = next_deadline
|
|
15
|
-
@handled_eof =
|
|
16
|
-
@stalled_matches = {}
|
|
19
|
+
@handled_eof = {}.compare_by_identity
|
|
20
|
+
@stalled_matches = {}.compare_by_identity
|
|
17
21
|
@polled = false
|
|
18
22
|
@expired_eof_continuation = false
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
# 运行匹配状态机;内部 :retry 表示继续循环,最终返回一个 Result。
|
|
22
26
|
def run
|
|
23
|
-
@relay_buffers = {}
|
|
27
|
+
@relay_buffers = {}.compare_by_identity
|
|
24
28
|
@sessions.each do |session|
|
|
25
|
-
buffer = session.
|
|
29
|
+
buffer = session.interaction_buffer
|
|
26
30
|
if buffer
|
|
27
31
|
# 转义回调中的显式匹配临时接管读取,先消费转接已经预读的尾部。
|
|
28
32
|
@relay_buffers[session] = buffer
|
|
29
|
-
session.
|
|
30
|
-
session.
|
|
33
|
+
session.interaction_buffer = nil
|
|
34
|
+
session.restore_relay_buffer(buffer)
|
|
31
35
|
buffer.clear
|
|
32
36
|
end
|
|
33
|
-
session.
|
|
37
|
+
session.reset_result
|
|
34
38
|
end
|
|
35
39
|
loop do
|
|
36
40
|
# 先消费已缓冲的匹配,再处理 EOF,最后读取;避免进程退出时丢失最后一个匹配。
|
|
@@ -49,7 +53,7 @@ class Expect
|
|
|
49
53
|
# 嵌套 expect 即使异常退出,也要把未消费尾部还给原 Relay 的同一个缓冲对象。
|
|
50
54
|
@relay_buffers.each do |session, buffer|
|
|
51
55
|
buffer.replace(session.clear_buffer)
|
|
52
|
-
session.
|
|
56
|
+
session.interaction_buffer = buffer
|
|
53
57
|
end
|
|
54
58
|
end
|
|
55
59
|
|
|
@@ -59,13 +63,13 @@ class Expect
|
|
|
59
63
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 声明优先级与单轮快照须在同一次扫描保持一致。
|
|
60
64
|
def find_match
|
|
61
65
|
# 单组且来源不重复时无需缓存;重复来源才为本轮扫描建立快照表。
|
|
62
|
-
groups = @
|
|
66
|
+
groups = @groups
|
|
63
67
|
if groups.size > 1 || (groups.first && groups.first.first.size > @sessions.size)
|
|
64
68
|
snapshots = {}.compare_by_identity
|
|
65
69
|
end
|
|
66
70
|
groups.each do |sessions, patterns|
|
|
67
71
|
sessions.each do |session|
|
|
68
|
-
next if @handled_eof.
|
|
72
|
+
next if @handled_eof.key?(session)
|
|
69
73
|
|
|
70
74
|
buffer = snapshots ? (snapshots[session] ||= session.buffer) : session.buffer
|
|
71
75
|
stalled = @stalled_matches[session]
|
|
@@ -99,23 +103,22 @@ class Expect
|
|
|
99
103
|
@literal_misses ||= {}.compare_by_identity
|
|
100
104
|
misses = (@literal_misses[session] ||= {}.compare_by_identity)
|
|
101
105
|
previous = misses[pattern]
|
|
102
|
-
generation = session.
|
|
103
|
-
#
|
|
104
|
-
offset = if previous && previous[0] == generation
|
|
106
|
+
generation = session.buffer_generation
|
|
107
|
+
# 模式已冻结;缓冲代次未变意味着只有尾部追加。
|
|
108
|
+
offset = if previous && previous[0] == generation
|
|
105
109
|
return nil if previous[1] == buffer.bytesize
|
|
106
110
|
|
|
107
111
|
[previous[1] - pattern.value.bytesize + 1, 0].max
|
|
108
112
|
else
|
|
109
113
|
0
|
|
110
114
|
end
|
|
111
|
-
position = pattern.locate(buffer, offset:
|
|
115
|
+
position = pattern.locate(buffer, offset:)
|
|
112
116
|
if position
|
|
113
117
|
misses.delete(pattern)
|
|
114
118
|
else
|
|
115
119
|
entry = (misses[pattern] ||= [])
|
|
116
120
|
entry[0] = generation
|
|
117
121
|
entry[1] = buffer.bytesize
|
|
118
|
-
entry[2] = pattern.value
|
|
119
122
|
end
|
|
120
123
|
position
|
|
121
124
|
end
|
|
@@ -123,7 +126,7 @@ class Expect
|
|
|
123
126
|
# 先记录并消费匹配,再执行回调;回调可选择结束、重置期限或保留期限继续。
|
|
124
127
|
def handle_match(session, pattern, position)
|
|
125
128
|
previous_buffer = session.buffer
|
|
126
|
-
result = session.
|
|
129
|
+
result = session.record_match(pattern, position, consume: @consume)
|
|
127
130
|
action = pattern.call(session)
|
|
128
131
|
return result unless continuing?(action)
|
|
129
132
|
|
|
@@ -132,30 +135,31 @@ class Expect
|
|
|
132
135
|
stalled = (@stalled_matches[session] ||= { buffer: previous_buffer, patterns: [] })
|
|
133
136
|
stalled[:patterns] << pattern
|
|
134
137
|
end
|
|
135
|
-
@deadline = next_deadline if action
|
|
138
|
+
@deadline = next_deadline if CONTINUE.equal?(action)
|
|
136
139
|
# 总期限到达时回主循环先派发已知 EOF;仅相对期限延续原有立即超时语义。
|
|
137
|
-
return handle_timeout if action
|
|
140
|
+
return handle_timeout if CONTINUE_WITHOUT_RESET.equal?(action) && expired? && !hard_expired?
|
|
138
141
|
|
|
139
142
|
:retry
|
|
140
143
|
end
|
|
141
144
|
|
|
142
145
|
# 找出尚未派发 EOF 事件的会话,保证每个源只处理一次结束事件。
|
|
143
146
|
def unhandled_eof
|
|
144
|
-
@sessions.find { |session| session.eof? && !@handled_eof.
|
|
147
|
+
@sessions.find { |session| session.eof? && !@handled_eof.key?(session) }
|
|
145
148
|
end
|
|
146
149
|
|
|
147
150
|
# 将剩余字节交给 EOF 回调;需要继续时等待其他源,全部结束则立即返回。
|
|
148
151
|
def handle_eof(session)
|
|
149
|
-
result = session.
|
|
150
|
-
@handled_eof
|
|
152
|
+
result = session.record_eof
|
|
153
|
+
@handled_eof[session] = true
|
|
151
154
|
actions = @patterns.eof_patterns_for(session).map { |pattern| pattern.call(session) }
|
|
152
155
|
return result unless actions.any? { |action| continuing?(action) }
|
|
153
156
|
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
reset_timeout = actions.any? { |action| CONTINUE.equal?(action) }
|
|
158
|
+
@deadline = next_deadline if reset_timeout
|
|
159
|
+
return result if @handled_eof.size == @sessions.size
|
|
156
160
|
|
|
157
161
|
# 期限已过时不再扫描文本,但先派发已知 EOF;最后一个源结束不能被误报为超时。
|
|
158
|
-
@expired_eof_continuation = !
|
|
162
|
+
@expired_eof_continuation = !reset_timeout && expired?
|
|
159
163
|
|
|
160
164
|
:retry
|
|
161
165
|
end
|
|
@@ -192,23 +196,23 @@ class Expect
|
|
|
192
196
|
session = by_io ? by_io.fetch(io) : sessions.find { |candidate| candidate.to_io.equal?(io) }
|
|
193
197
|
begin
|
|
194
198
|
# 转接回调消费匹配内容,余下字节交回 Relay,不能在这里提前转发两次。
|
|
195
|
-
data = session.
|
|
199
|
+
data = session.read_available(propagate: !@relay_buffers.key?(session))
|
|
196
200
|
rescue Errno::EINTR
|
|
197
201
|
next
|
|
198
202
|
rescue IOError, SystemCallError => error
|
|
199
|
-
return session.
|
|
203
|
+
return session.record_error(error)
|
|
200
204
|
end
|
|
201
205
|
@stalled_matches.delete(session) if data
|
|
202
|
-
@deadline = next_deadline if data &&
|
|
206
|
+
@deadline = next_deadline if data && @reset_timeout_on_read
|
|
203
207
|
end
|
|
204
208
|
:retry
|
|
205
209
|
end
|
|
206
210
|
|
|
207
211
|
# 返回本次仍需监听的会话,供读取选择和超时回调使用。
|
|
208
|
-
def active_sessions = @sessions.reject { |session| @handled_eof.
|
|
212
|
+
def active_sessions = @sessions.reject { |session| @handled_eof.key?(session) }
|
|
209
213
|
|
|
210
214
|
# 只有约定的继续符号会驱动下一轮,普通回调返回值不会改变等待流程。
|
|
211
|
-
def continuing?(action) =
|
|
215
|
+
def continuing?(action) = CONTINUE.equal?(action) || CONTINUE_WITHOUT_RESET.equal?(action)
|
|
212
216
|
|
|
213
217
|
# 使用单调时钟计算期限;nil 一直表示无限等待,不受系统时间调整影响。
|
|
214
218
|
# 每次重置都重新与总期限取较早者,避免连续输入或继续回调无限推迟结束。
|
|
@@ -224,22 +228,22 @@ class Expect
|
|
|
224
228
|
def remaining = @deadline && [@deadline - Expect.monotonic, 0].max
|
|
225
229
|
|
|
226
230
|
# 判断有限期限是否已到达;无限等待不会触发超时。
|
|
227
|
-
def expired? =
|
|
231
|
+
def expired? = !@deadline.nil? && Expect.monotonic >= @deadline
|
|
228
232
|
|
|
229
233
|
# 绝对总期限不受接收数据和 continue 重置;已知 EOF 仍按原顺序派发。
|
|
230
|
-
def hard_expired? =
|
|
234
|
+
def hard_expired? = !@hard_deadline.nil? && Expect.monotonic >= @hard_deadline
|
|
231
235
|
|
|
232
|
-
# select
|
|
236
|
+
# select 失败时无法归属单个源,只更新仍监听的会话;已派发 EOF 的结果保持不变。
|
|
233
237
|
def record_error(error)
|
|
234
|
-
|
|
238
|
+
active_sessions.map { |session| session.record_error(error) }.first
|
|
235
239
|
end
|
|
236
240
|
|
|
237
241
|
# 为活跃会话记录超时,回调接收全部活跃源;只有重置计时的继续符号能重新等待。
|
|
238
242
|
# 已到总期限仍通知超时回调,但不接受继续请求,且不消费尚未匹配的字节。
|
|
239
243
|
def handle_timeout
|
|
240
|
-
results = active_sessions.map { |session| session.
|
|
244
|
+
results = active_sessions.map { |session| session.record_error(:timeout) }
|
|
241
245
|
action = @patterns.timeout_pattern&.call(active_sessions)
|
|
242
|
-
return results.first unless action
|
|
246
|
+
return results.first unless CONTINUE.equal?(action) && !hard_expired?
|
|
243
247
|
|
|
244
248
|
@deadline = next_deadline
|
|
245
249
|
@polled = false
|
data/lib/expect/pattern.rb
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module Expect
|
|
4
4
|
# 一个已编号的文本模式或事件及其回调;匹配结果统一使用字节偏移,便于精确消费 IO 缓冲。
|
|
5
|
-
|
|
5
|
+
# @api private
|
|
6
|
+
Pattern = Data.define(:number, :value, :callback) do
|
|
7
|
+
# 复制字符串模式;Regexp 和 callable 只引用,不取得外部对象的生命周期。
|
|
8
|
+
def initialize(value:, number: nil, callback: nil)
|
|
9
|
+
super(value: value.is_a?(String) ? value.b.freeze : value, number:, callback:)
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
# 将匹配会话或超时会话数组交给回调;额外上下文由调用方闭包保存。
|
|
7
13
|
def call(subject)
|
|
8
14
|
callback&.call(subject)
|
|
@@ -28,7 +34,7 @@ class Expect
|
|
|
28
34
|
end
|
|
29
35
|
unless text.valid_encoding?
|
|
30
36
|
# 不完整的尾字符可能改变锚点或前瞻结果,必须等字符收齐后再匹配。
|
|
31
|
-
validate_incomplete_suffix!(text, final:
|
|
37
|
+
validate_incomplete_suffix!(text, final:)
|
|
32
38
|
return nil
|
|
33
39
|
end
|
|
34
40
|
found = value.match(text)
|
|
@@ -58,4 +64,5 @@ class Expect
|
|
|
58
64
|
raise EncodingError, "received invalid #{text.encoding} data; use a binary regexp (/.../n) for binary streams"
|
|
59
65
|
end
|
|
60
66
|
end
|
|
67
|
+
private_constant :Pattern
|
|
61
68
|
end
|