expect-pty 0.6.1 → 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 +29 -3
- data/README.md +102 -119
- data/docs/API.md +76 -51
- data/docs/MIGRATION.md +68 -15
- data/lib/expect/cleanup.rb +4 -3
- data/lib/expect/interaction.rb +57 -56
- data/lib/expect/logging.rb +60 -106
- data/lib/expect/matcher.rb +22 -19
- data/lib/expect/pattern.rb +2 -1
- data/lib/expect/pattern_list.rb +5 -5
- data/lib/expect/redactor.rb +1 -1
- data/lib/expect/relay.rb +20 -19
- data/lib/expect/relay_writer.rb +5 -4
- data/lib/expect/result.rb +1 -1
- data/lib/expect/session.rb +114 -68
- data/lib/expect/session_resources.rb +10 -17
- data/lib/expect/version.rb +2 -2
- data/lib/expect.rb +47 -370
- data/sig/expect.rbs +203 -279
- metadata +4 -20
- data/lib/expect/configuration.rb +0 -143
- data/lib/expect/terminal.rb +0 -102
data/lib/expect/logging.rb
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "logger"
|
|
3
4
|
require_relative "redactor"
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
#
|
|
6
|
+
# 借用标准 Logger、接收记录 writer 和协议输出 writer,不接管它们的生命周期。
|
|
7
|
+
# 脱敏只作用于诊断与接收记录,协议输出和匹配缓冲始终保留原始字节。
|
|
8
|
+
module Expect
|
|
9
|
+
# 会话诊断、接收记录和协议转发的独立输出接口。
|
|
10
|
+
# 宿主 Session 初始化目标与过滤器状态,并提供 pid、fileno 作为诊断元数据。
|
|
11
|
+
# 三种输出各有边界:Logger 接收事件,transcript 接收脱敏字节,outputs 接收原协议字节。
|
|
9
12
|
module Logging
|
|
10
|
-
#
|
|
11
|
-
attr_reader :
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
def
|
|
16
|
-
unless target.nil? || (target.respond_to?(:
|
|
17
|
-
|
|
18
|
-
raise ArgumentError, "diagnostic output must support info/debug, write or call, or be nil"
|
|
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"
|
|
19
21
|
end
|
|
20
|
-
return if @
|
|
22
|
+
return if @logger.equal?(target)
|
|
21
23
|
|
|
22
24
|
flush_diagnostics
|
|
23
|
-
@
|
|
25
|
+
@logger = target
|
|
26
|
+
@diagnostic_redactors = {}
|
|
24
27
|
end
|
|
25
28
|
|
|
26
|
-
#
|
|
29
|
+
# 秘密仅作用于本会话的接收记录和诊断,不改写匹配或 outputs 的协议字节。
|
|
27
30
|
# 先校验全部值再发布;注册是追加操作,应在首次通信前完成,不能追溯已交付的日志。
|
|
28
31
|
def redact(*secrets)
|
|
29
32
|
unless secrets.any? && secrets.all? { |secret| secret.is_a?(String) && !secret.empty? }
|
|
@@ -31,69 +34,52 @@ class Expect
|
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
@secrets = ((@secrets || []) + secrets.map { |secret| secret.b.freeze }).uniq.freeze
|
|
34
|
-
@
|
|
37
|
+
@transcript_redactor.patterns = @secrets if @transcript_redactor
|
|
35
38
|
@diagnostic_redactors.each_value { |redactor| redactor.patterns = @secrets }
|
|
36
|
-
|
|
39
|
+
self
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
#
|
|
40
|
-
def
|
|
41
|
-
unless target.nil? || target.respond_to?(:write)
|
|
42
|
-
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
replace_log(target)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# 打开追加/覆盖日志文件,或注册接收字节的日志块;同一次只能指定一种目标。
|
|
49
|
-
def log_to(target = nil, mode: "a", &block)
|
|
50
|
-
raise ArgumentError, "provide a log target or a block, not both" if block && target
|
|
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)
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# 先交付旧过滤尾部,再允许新路径截断;同一文件不能在截断后被旧句柄写回。
|
|
57
|
-
flush_log
|
|
58
|
-
# 库打开的文件由 SessionResources 持有,替换日志或关闭会话时释放;外部 IO 只借用。
|
|
59
|
-
replace_log(File.open(target, "#{mode}b", 0o600), owned: true)
|
|
60
|
-
else
|
|
61
|
-
raise ArgumentError, "provide a log target or a block" unless target
|
|
62
|
-
|
|
63
|
-
self.log_output = target
|
|
64
|
-
end
|
|
47
|
+
flush_transcript
|
|
48
|
+
@transcript = target
|
|
49
|
+
@transcript_redactor = nil
|
|
65
50
|
end
|
|
66
51
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
target = log_output
|
|
52
|
+
# 向接收记录补写内容,不发送给子进程或 outputs;脱敏尾部可能延迟交付,返回 nil。
|
|
53
|
+
def write_transcript(*objects)
|
|
54
|
+
target = transcript
|
|
71
55
|
return unless target
|
|
72
56
|
|
|
73
57
|
data = objects.map { |object| object.to_s.b }.join
|
|
74
58
|
if @secrets
|
|
75
|
-
@
|
|
76
|
-
data = @
|
|
59
|
+
@transcript_redactor ||= Redactor.new(@secrets)
|
|
60
|
+
data = @transcript_redactor.append(data)
|
|
77
61
|
return if data.empty?
|
|
78
62
|
end
|
|
79
|
-
|
|
63
|
+
emit(target, data)
|
|
64
|
+
nil
|
|
80
65
|
end
|
|
81
66
|
|
|
82
|
-
#
|
|
83
|
-
def
|
|
67
|
+
# 返回副本,避免外部原地修改转发关系;stdout 与其他 writer 使用相同协议。
|
|
68
|
+
def outputs = @outputs.dup
|
|
84
69
|
|
|
85
|
-
#
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
89
75
|
|
|
90
|
-
@
|
|
76
|
+
@outputs = targets.dup
|
|
91
77
|
end
|
|
92
78
|
|
|
93
|
-
#
|
|
79
|
+
# 原样转发协议字节,接收记录在读取时单独写入。
|
|
80
|
+
# @api private
|
|
94
81
|
def propagate(data)
|
|
95
|
-
emit(
|
|
96
|
-
@listeners.each { |listener| emit(listener, data) } if log_listeners?
|
|
82
|
+
@outputs.each { |output| emit(output, data) }
|
|
97
83
|
end
|
|
98
84
|
|
|
99
85
|
private
|
|
@@ -114,45 +100,33 @@ class Expect
|
|
|
114
100
|
target.flush if target.respond_to?(:flush)
|
|
115
101
|
end
|
|
116
102
|
|
|
117
|
-
# 字节诊断在 inspect
|
|
118
|
-
def trace_data(event, data
|
|
119
|
-
|
|
120
|
-
return trace("buffer [FILTERED]", level:, event:) if event == :buffer
|
|
103
|
+
# 字节诊断在 inspect 转义之前过滤,发送与接收各自保留分片状态。
|
|
104
|
+
def trace_data(event, data)
|
|
105
|
+
return unless logger&.debug?
|
|
121
106
|
|
|
107
|
+
if @secrets
|
|
122
108
|
redactor = (@diagnostic_redactors[event] ||= Redactor.new(@secrets))
|
|
123
109
|
data = redactor.append(data)
|
|
124
110
|
return if data.empty?
|
|
125
111
|
end
|
|
126
|
-
trace("#{event} #{data.inspect}",
|
|
112
|
+
trace("#{event} #{data.inspect}", severity: Logger::DEBUG, event:)
|
|
127
113
|
end
|
|
128
114
|
|
|
129
|
-
#
|
|
130
|
-
def trace(message,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
severity = level == 1 ? :info : :debug
|
|
134
|
-
target = diagnostic_output
|
|
135
|
-
if target.respond_to?(:info) && target.respond_to?(:debug)
|
|
136
|
-
target.public_send(severity, "#{inspect}: #{message}")
|
|
137
|
-
elsif target.respond_to?(:call)
|
|
138
|
-
target.call({ event:, level: severity, pid:, fd: fileno, message: message.freeze }.freeze)
|
|
139
|
-
elsif target
|
|
140
|
-
emit(target, "#{inspect}: #{message}\n")
|
|
141
|
-
else
|
|
142
|
-
warn("#{inspect}: #{message}")
|
|
143
|
-
end
|
|
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")
|
|
144
118
|
end
|
|
145
119
|
|
|
146
120
|
# 结束当前日志片段并交付过滤器尾部;重复调用不会重放已释放字节,写入异常原样传播。
|
|
147
|
-
def
|
|
121
|
+
def flush_transcript
|
|
148
122
|
loop do
|
|
149
|
-
return unless @
|
|
123
|
+
return unless @transcript_redactor && transcript
|
|
150
124
|
|
|
151
|
-
data = @
|
|
125
|
+
data = @transcript_redactor.finish
|
|
152
126
|
return if data.empty?
|
|
153
127
|
|
|
154
|
-
|
|
155
|
-
#
|
|
128
|
+
emit(transcript, data)
|
|
129
|
+
# writer 可能重入并追加尾部;交接前仍归当前记录流。
|
|
156
130
|
end
|
|
157
131
|
end
|
|
158
132
|
|
|
@@ -171,31 +145,11 @@ class Expect
|
|
|
171
145
|
next if data.empty?
|
|
172
146
|
|
|
173
147
|
delivered = true
|
|
174
|
-
trace("#{name} #{data.inspect}",
|
|
148
|
+
trace("#{name} #{data.inspect}", severity: Logger::DEBUG, event: name)
|
|
175
149
|
end
|
|
176
150
|
# 目标尚未交接,新产生的尾部仍归旧诊断流;排空后才结束这次冲刷。
|
|
177
151
|
break unless delivered
|
|
178
152
|
end
|
|
179
153
|
end
|
|
180
|
-
|
|
181
|
-
# 交接日志目标和所有权,只关闭库拥有的旧文件;失败时释放新打开的文件。
|
|
182
|
-
def replace_log(target, owned: false)
|
|
183
|
-
# 重复赋值同一目标时保留原所有权,防止将库打开的文件误变成借用资源。
|
|
184
|
-
return target if log_output.equal?(target)
|
|
185
|
-
|
|
186
|
-
Cleanup.on_failure(-> { target.close if owned && target && !target.closed? }) do
|
|
187
|
-
flush_log
|
|
188
|
-
# 尾部回调可以重入并替换目标;交接时重新读取目标和所有权,不能关闭旧的回调对象。
|
|
189
|
-
next target if log_output.equal?(target)
|
|
190
|
-
|
|
191
|
-
previous = @resources.owned_log
|
|
192
|
-
previous.close if previous && !previous.closed?
|
|
193
|
-
# 终结器只持有所属文件;借用回调可能捕获会话,不能让它经资源对象成为 GC 根。
|
|
194
|
-
@resources.owned_log = owned ? target : nil
|
|
195
|
-
@log_output = target
|
|
196
|
-
@log_redactor = nil
|
|
197
|
-
target
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
154
|
end
|
|
201
155
|
end
|
data/lib/expect/matcher.rb
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module Expect
|
|
4
4
|
# 驱动一次单会话或多会话匹配,管理模式优先级、EOF 和共享期限,不接管 IO 所有权。
|
|
5
5
|
# 调度只在扫描、回调和 IO 操作之间检查期限,不强行中断用户代码或单次正则计算。
|
|
6
6
|
# @api private
|
|
7
7
|
class Matcher
|
|
8
8
|
# 固定本次参与的会话及初始期限;已处理 EOF 的会话仅从本次等待中移除。
|
|
9
|
-
def initialize(patterns, timeout, deadline: nil)
|
|
9
|
+
def initialize(patterns, timeout, deadline: nil, consume: true, reset_timeout_on_read: false)
|
|
10
10
|
@patterns = patterns.finalize!
|
|
11
|
-
@sessions = patterns.sessions
|
|
12
|
-
@groups = patterns.groups
|
|
11
|
+
@sessions = patterns.sessions
|
|
12
|
+
@groups = patterns.groups
|
|
13
|
+
@consume = consume
|
|
14
|
+
@reset_timeout_on_read = reset_timeout_on_read
|
|
13
15
|
@timeout = Expect.duration(timeout)
|
|
14
16
|
# 相对期限可因接收或 continue 重算,总期限始终固定;两者共用单调时钟。
|
|
15
17
|
@hard_deadline = deadline
|
|
16
18
|
@deadline = next_deadline
|
|
17
19
|
@handled_eof = {}.compare_by_identity
|
|
18
|
-
@stalled_matches = {}
|
|
20
|
+
@stalled_matches = {}.compare_by_identity
|
|
19
21
|
@polled = false
|
|
20
22
|
@expired_eof_continuation = false
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
# 运行匹配状态机;内部 :retry 表示继续循环,最终返回一个 Result。
|
|
24
26
|
def run
|
|
25
|
-
@relay_buffers = {}
|
|
27
|
+
@relay_buffers = {}.compare_by_identity
|
|
26
28
|
@sessions.each do |session|
|
|
27
29
|
buffer = session.interaction_buffer
|
|
28
30
|
if buffer
|
|
@@ -124,8 +126,8 @@ class Expect
|
|
|
124
126
|
# 先记录并消费匹配,再执行回调;回调可选择结束、重置期限或保留期限继续。
|
|
125
127
|
def handle_match(session, pattern, position)
|
|
126
128
|
previous_buffer = session.buffer
|
|
127
|
-
result = session.record_match(pattern, position)
|
|
128
|
-
action = pattern.call(session
|
|
129
|
+
result = session.record_match(pattern, position, consume: @consume)
|
|
130
|
+
action = pattern.call(session)
|
|
129
131
|
return result unless continuing?(action)
|
|
130
132
|
|
|
131
133
|
# 回调未改变缓冲时暂停当前模式,等待缓冲变化后再匹配,避免原地空转。
|
|
@@ -133,9 +135,9 @@ class Expect
|
|
|
133
135
|
stalled = (@stalled_matches[session] ||= { buffer: previous_buffer, patterns: [] })
|
|
134
136
|
stalled[:patterns] << pattern
|
|
135
137
|
end
|
|
136
|
-
@deadline = next_deadline if action
|
|
138
|
+
@deadline = next_deadline if CONTINUE.equal?(action)
|
|
137
139
|
# 总期限到达时回主循环先派发已知 EOF;仅相对期限延续原有立即超时语义。
|
|
138
|
-
return handle_timeout if action
|
|
140
|
+
return handle_timeout if CONTINUE_WITHOUT_RESET.equal?(action) && expired? && !hard_expired?
|
|
139
141
|
|
|
140
142
|
:retry
|
|
141
143
|
end
|
|
@@ -149,14 +151,15 @@ class Expect
|
|
|
149
151
|
def handle_eof(session)
|
|
150
152
|
result = session.record_eof
|
|
151
153
|
@handled_eof[session] = true
|
|
152
|
-
actions = @patterns.eof_patterns_for(session
|
|
154
|
+
actions = @patterns.eof_patterns_for(session).map { |pattern| pattern.call(session) }
|
|
153
155
|
return result unless actions.any? { |action| continuing?(action) }
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
reset_timeout = actions.any? { |action| CONTINUE.equal?(action) }
|
|
158
|
+
@deadline = next_deadline if reset_timeout
|
|
156
159
|
return result if @handled_eof.size == @sessions.size
|
|
157
160
|
|
|
158
161
|
# 期限已过时不再扫描文本,但先派发已知 EOF;最后一个源结束不能被误报为超时。
|
|
159
|
-
@expired_eof_continuation = !
|
|
162
|
+
@expired_eof_continuation = !reset_timeout && expired?
|
|
160
163
|
|
|
161
164
|
:retry
|
|
162
165
|
end
|
|
@@ -200,7 +203,7 @@ class Expect
|
|
|
200
203
|
return session.record_error(error)
|
|
201
204
|
end
|
|
202
205
|
@stalled_matches.delete(session) if data
|
|
203
|
-
@deadline = next_deadline if data &&
|
|
206
|
+
@deadline = next_deadline if data && @reset_timeout_on_read
|
|
204
207
|
end
|
|
205
208
|
:retry
|
|
206
209
|
end
|
|
@@ -209,7 +212,7 @@ class Expect
|
|
|
209
212
|
def active_sessions = @sessions.reject { |session| @handled_eof.key?(session) }
|
|
210
213
|
|
|
211
214
|
# 只有约定的继续符号会驱动下一轮,普通回调返回值不会改变等待流程。
|
|
212
|
-
def continuing?(action) =
|
|
215
|
+
def continuing?(action) = CONTINUE.equal?(action) || CONTINUE_WITHOUT_RESET.equal?(action)
|
|
213
216
|
|
|
214
217
|
# 使用单调时钟计算期限;nil 一直表示无限等待,不受系统时间调整影响。
|
|
215
218
|
# 每次重置都重新与总期限取较早者,避免连续输入或继续回调无限推迟结束。
|
|
@@ -225,10 +228,10 @@ class Expect
|
|
|
225
228
|
def remaining = @deadline && [@deadline - Expect.monotonic, 0].max
|
|
226
229
|
|
|
227
230
|
# 判断有限期限是否已到达;无限等待不会触发超时。
|
|
228
|
-
def expired? =
|
|
231
|
+
def expired? = !@deadline.nil? && Expect.monotonic >= @deadline
|
|
229
232
|
|
|
230
233
|
# 绝对总期限不受接收数据和 continue 重置;已知 EOF 仍按原顺序派发。
|
|
231
|
-
def hard_expired? =
|
|
234
|
+
def hard_expired? = !@hard_deadline.nil? && Expect.monotonic >= @hard_deadline
|
|
232
235
|
|
|
233
236
|
# select 失败时无法归属单个源,只更新仍监听的会话;已派发 EOF 的结果保持不变。
|
|
234
237
|
def record_error(error)
|
|
@@ -239,8 +242,8 @@ class Expect
|
|
|
239
242
|
# 已到总期限仍通知超时回调,但不接受继续请求,且不消费尚未匹配的字节。
|
|
240
243
|
def handle_timeout
|
|
241
244
|
results = active_sessions.map { |session| session.record_error(:timeout) }
|
|
242
|
-
action = @patterns.timeout_pattern&.call(active_sessions
|
|
243
|
-
return results.first unless action
|
|
245
|
+
action = @patterns.timeout_pattern&.call(active_sessions)
|
|
246
|
+
return results.first unless CONTINUE.equal?(action) && !hard_expired?
|
|
244
247
|
|
|
245
248
|
@deadline = next_deadline
|
|
246
249
|
@polled = false
|
data/lib/expect/pattern.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module Expect
|
|
4
4
|
# 一个已编号的文本模式或事件及其回调;匹配结果统一使用字节偏移,便于精确消费 IO 缓冲。
|
|
5
5
|
# @api private
|
|
6
6
|
Pattern = Data.define(:number, :value, :callback) do
|
|
7
|
+
# 复制字符串模式;Regexp 和 callable 只引用,不取得外部对象的生命周期。
|
|
7
8
|
def initialize(value:, number: nil, callback: nil)
|
|
8
9
|
super(value: value.is_a?(String) ? value.b.freeze : value, number:, callback:)
|
|
9
10
|
end
|
data/lib/expect/pattern_list.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module Expect
|
|
4
4
|
# 将 Ruby 模式和块回调整理为有序会话组;注册阶段不读取 IO、不执行匹配回调。
|
|
5
5
|
# 分组结构直接决定 Matcher 的“声明组 → 会话 → 模式”优先级,不能按匹配位置重排。
|
|
6
6
|
class PatternList
|
|
@@ -103,7 +103,7 @@ class Expect
|
|
|
103
103
|
|
|
104
104
|
# 会话的业务相等性不能合并不同读取源;来源对象及排列都必须相同。
|
|
105
105
|
def same_sessions?(previous, sessions)
|
|
106
|
-
previous && previous.size == sessions.size &&
|
|
106
|
+
!previous.nil? && previous.size == sessions.size &&
|
|
107
107
|
previous.each_with_index.all? { |session, index| session.equal?(sessions[index]) }
|
|
108
108
|
end
|
|
109
109
|
|
|
@@ -113,11 +113,11 @@ class Expect
|
|
|
113
113
|
Pattern.new(number: @number, value:, callback:)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
# 拒绝空来源和非
|
|
116
|
+
# 拒绝空来源和非 Session 对象,在任何 IO 读取之前暴露调用错误。
|
|
117
117
|
def validate_sessions!(sessions)
|
|
118
|
-
return if !sessions.empty? && sessions.all?(
|
|
118
|
+
return if !sessions.empty? && sessions.all?(Session)
|
|
119
119
|
|
|
120
|
-
raise ArgumentError, "patterns require
|
|
120
|
+
raise ArgumentError, "patterns require Session objects; specify from: for class-level waits"
|
|
121
121
|
end
|
|
122
122
|
end
|
|
123
123
|
end
|
data/lib/expect/redactor.rb
CHANGED
data/lib/expect/relay.rb
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module Expect
|
|
4
4
|
# 一次转接的共同读写循环;发送游标留在源会话中,调用结束后仍可继续。
|
|
5
5
|
# 本对象只暂借未处理输入;读缓冲、发送进度与转义回调分开保存,避免重入时重复交付。
|
|
6
6
|
# @api private
|
|
7
7
|
class Relay
|
|
8
|
+
# 只串行化来源归属交接;Session 的其他操作仍须由调用方避免并发修改。
|
|
8
9
|
OWNERSHIP_MUTEX = Mutex.new
|
|
9
10
|
private_constant :OWNERSHIP_MUTEX
|
|
10
11
|
|
|
11
12
|
# 这里只校验参数;run 取得所有来源后才转移缓冲,不关闭或接管任何外部 IO。
|
|
12
13
|
def initialize(sessions, timeout)
|
|
13
|
-
@sessions = sessions.uniq
|
|
14
|
+
@sessions = sessions.uniq(&:object_id)
|
|
14
15
|
@active = @sessions.dup
|
|
15
16
|
period = Expect.duration(timeout)
|
|
16
17
|
@deadline = period && (Expect.monotonic + period)
|
|
17
18
|
@token = Object.new
|
|
18
|
-
@buffers = {}
|
|
19
|
-
@previous = {}
|
|
19
|
+
@buffers = {}.compare_by_identity
|
|
20
|
+
@previous = {}.compare_by_identity
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# 每轮先处理转义和已排队输出,再共同选择读写;停止返回来源会话,总期限到达返回 nil。
|
|
23
24
|
# 真实 IO 的写入由非阻塞游标推进,用户日志和回调仍同步执行,须由调用方保证及时返回。
|
|
24
25
|
def run
|
|
25
26
|
prepare_sources
|
|
26
|
-
|
|
27
|
+
pending_writes.each(&:restart_timeout)
|
|
27
28
|
polled = false
|
|
28
29
|
loop do
|
|
29
30
|
stopped, idle, queued = dispatch_sources
|
|
@@ -53,15 +54,13 @@ class Expect
|
|
|
53
54
|
|
|
54
55
|
if (callback = session.relay_callback)
|
|
55
56
|
session.relay_callback = nil
|
|
56
|
-
return [session, idle, queued] unless callback.
|
|
57
|
+
return [session, idle, queued] unless callback.call
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
result = Interaction.
|
|
60
|
-
|
|
61
|
-
end
|
|
62
|
-
return [session, idle, queued] unless result
|
|
60
|
+
result = Interaction.queue_input(session, @buffers.fetch(session), final: session.eof?)
|
|
61
|
+
return [session, idle, queued] if result == :stopped
|
|
63
62
|
|
|
64
|
-
if result == :
|
|
63
|
+
if result == :queued
|
|
65
64
|
queued = true
|
|
66
65
|
next
|
|
67
66
|
end
|
|
@@ -72,14 +71,14 @@ class Expect
|
|
|
72
71
|
callback = session.sequences[:eof]
|
|
73
72
|
return [session, idle, queued] unless callback&.call
|
|
74
73
|
|
|
75
|
-
@active.
|
|
74
|
+
@active.delete_if { |active| active.equal?(session) }
|
|
76
75
|
end
|
|
77
76
|
[nil, idle, queued]
|
|
78
77
|
end
|
|
79
78
|
|
|
80
79
|
# 共同等待来源与目标,每轮读每个就绪来源一次;EINTR 返回原期限循环。
|
|
81
80
|
def select_and_read(progress)
|
|
82
|
-
pending =
|
|
81
|
+
pending = pending_writes
|
|
83
82
|
readers = read_sources(pending)
|
|
84
83
|
deadlines = [@deadline, *pending.map(&:deadline)].compact
|
|
85
84
|
remaining = deadlines.empty? ? nil : [deadlines.min - Expect.monotonic, 0].max
|
|
@@ -150,13 +149,13 @@ class Expect
|
|
|
150
149
|
end
|
|
151
150
|
|
|
152
151
|
# 只收集尚未完成的目标,供共同 select 以及最早写入期限计算使用。
|
|
153
|
-
def
|
|
152
|
+
def pending_writes = @sessions.flat_map(&:pending_writes).reject(&:done?)
|
|
154
153
|
|
|
155
154
|
# 每个目标每轮最多推进一个片段;移除已完成游标,并告知主循环是否值得立即继续轮询。
|
|
156
155
|
def advance_outputs(check_timeout: true)
|
|
157
156
|
progress = false
|
|
158
157
|
@sessions.each do |session|
|
|
159
|
-
pending = session.
|
|
158
|
+
pending = session.pending_writes
|
|
160
159
|
pending.each { |output| progress = output.advance(check_timeout:) || progress }
|
|
161
160
|
pending.reject!(&:done?)
|
|
162
161
|
end
|
|
@@ -164,15 +163,17 @@ class Expect
|
|
|
164
163
|
end
|
|
165
164
|
|
|
166
165
|
# 转接总期限不因持续输入或某个目标的写入进展而重置。
|
|
167
|
-
def expired? =
|
|
166
|
+
def expired? = !@deadline.nil? && Expect.monotonic >= @deadline
|
|
168
167
|
|
|
169
168
|
# 背压源暂停吸收新输入,限制排队增长;作为写入目标的会话仍需读取以解除双向等待。
|
|
170
169
|
def read_sources(pending)
|
|
171
170
|
# 输出目标可能也在等待我们读取;这些来源即使有待发送数据也必须继续排空。
|
|
172
171
|
targets = pending.filter_map do |output|
|
|
173
|
-
|
|
172
|
+
output.target if output.target.is_a?(Session)
|
|
173
|
+
end
|
|
174
|
+
sources = @active.reject do |session|
|
|
175
|
+
session.pending_output? && targets.none? { |target| target.equal?(session) }
|
|
174
176
|
end
|
|
175
|
-
sources = @active.reject { |session| session.pending_output? && !targets.include?(session) }
|
|
176
177
|
(sources + targets).uniq { |session| session.to_io.object_id }.reject(&:eof?)
|
|
177
178
|
end
|
|
178
179
|
|
|
@@ -180,7 +181,7 @@ class Expect
|
|
|
180
181
|
# 目标的写期限更早到达时保留 WriteTimeout 语义,不能被转接的普通超时掩盖。
|
|
181
182
|
def finish_timeout(idle)
|
|
182
183
|
# 调度延迟可能让两种期限均已到达,仍按先到的期限决定结果。
|
|
183
|
-
|
|
184
|
+
pending_writes.each { |output| output.check_timeout! if output.deadline && output.deadline < @deadline }
|
|
184
185
|
idle.each do |session|
|
|
185
186
|
next if session.pending_output?
|
|
186
187
|
|
data/lib/expect/relay_writer.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module Expect
|
|
4
4
|
# 向单个转接目标写入数据,维护发送进度与期限;恢复时不重放已成功写出的前缀。
|
|
5
5
|
# 只借用目标,不 dup 描述符,也不负责读取;所有就绪等待统一交给 Relay 调度。
|
|
6
6
|
# @api private
|
|
7
7
|
class RelayWriter
|
|
8
|
+
# target 始终借用;deadline 是本轮背压预算,不表示整块数据已交付。
|
|
8
9
|
attr_reader :target, :deadline
|
|
9
10
|
|
|
10
11
|
# data 在本游标存活期间由上层保持不变,offset 始终以实际交付的字节数计量。
|
|
@@ -19,13 +20,13 @@ class Expect
|
|
|
19
20
|
# 初次排队或重新进入 interconnect 时开始新的写入预算;普通短写不会刷新期限。
|
|
20
21
|
# 只有 Expect 目标提供 write_timeout,原生 IO 和自定义目标只受转接总期限约束。
|
|
21
22
|
def restart_timeout
|
|
22
|
-
period = target.write_timeout if target.is_a?(
|
|
23
|
+
period = target.write_timeout if target.is_a?(Session)
|
|
23
24
|
@deadline = period && (Expect.monotonic + period)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# 真实 IO 可参与共同 select;自定义可写对象返回 nil,沿用其同步 write/flush 协议。
|
|
27
28
|
def io
|
|
28
|
-
return target.writer if target.is_a?(
|
|
29
|
+
return target.writer if target.is_a?(Session)
|
|
29
30
|
|
|
30
31
|
target if target.is_a?(IO)
|
|
31
32
|
end
|
|
@@ -37,7 +38,7 @@ class Expect
|
|
|
37
38
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 游标、短写、flush 和期限按单个目标推进。
|
|
38
39
|
def advance(check_timeout: true)
|
|
39
40
|
return false if done?
|
|
40
|
-
raise IOError, "closed Expect session" if target.is_a?(
|
|
41
|
+
raise IOError, "closed Expect session" if target.is_a?(Session) && target.closed?
|
|
41
42
|
|
|
42
43
|
if @offset < @data.bytesize
|
|
43
44
|
bytes = @data.byteslice(@offset, READ_SIZE)
|