expect-pty 0.6.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "logger"
3
4
  require_relative "redactor"
4
5
 
5
- # 管理日志目标、监听器及同步输出;资源所有权仍由会话的 SessionResources 统一保存。
6
- # 接收日志、诊断和协议转发是三条独立通道;脱敏只改变前两条,不改变匹配或转发字节。
7
- class Expect
8
- # @api private
6
+ # 借用标准 Logger、接收记录 writer 和协议输出 writer,不接管它们的生命周期。
7
+ # 脱敏只作用于诊断与接收记录,协议输出和匹配缓冲始终保留原始字节。
8
+ module Expect
9
+ # 会话诊断、接收记录和协议转发的独立输出接口。
10
+ # 宿主 Session 初始化目标与过滤器状态,并提供 pid、fileno 作为诊断元数据。
11
+ # 三种输出各有边界:Logger 接收事件,transcript 接收脱敏字节,outputs 接收原协议字节。
9
12
  module Logging
10
- # 读取当前日志目标,可能为库打开的文件、借用的 IO、回调或 nil。
11
- attr_reader :log_output, :diagnostic_output
12
-
13
- # 诊断目标仅借用;nil 沿用 stderr,Logger 使用 info/debug,回调接收冻结的事件 Hash。
14
- # 先校验再冲刷旧流;校验或冲刷失败时保留旧目标,避免把暂存尾部交给错误的接收方。
15
- def diagnostic_output=(target)
16
- unless target.nil? || (target.respond_to?(:info) && target.respond_to?(:debug)) ||
17
- target.respond_to?(:write) || target.respond_to?(:call)
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 @diagnostic_output.equal?(target)
22
+ return if @logger.equal?(target)
21
23
 
22
24
  flush_diagnostics
23
- @diagnostic_output = target
25
+ @logger = target
26
+ @diagnostic_redactors = {}
24
27
  end
25
28
 
26
- # 秘密仅作用于本会话的日志和诊断,不改写匹配、stdout 显示或 listeners 的协议字节。
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
- @log_redactor.patterns = @secrets if @log_redactor
37
+ @transcript_redactor.patterns = @secrets if @transcript_redactor
35
38
  @diagnostic_redactors.each_value { |redactor| redactor.patterns = @secrets }
36
- connection
39
+ self
37
40
  end
38
41
 
39
- # 替换借用的日志目标或停止日志;先校验新目标,失败时保留旧目标。
40
- def log_output=(target)
41
- unless target.nil? || target.respond_to?(:write) || target.respond_to?(:call)
42
- raise ArgumentError, "log output must support write or call, or be nil"
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
- target = block if block
53
- if target.respond_to?(:to_path) || target.is_a?(String)
54
- raise ArgumentError, "log mode must be a or w" unless %w[a w].include?(mode)
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
- # 向当前日志目标补写内容,支持 IO 和回调,不发送给子进程或监听器。
68
- # 启用脱敏后可能暂存末尾字节,因此一次调用不保证触发一次日志写入或回调。
69
- def write_log(*objects)
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
- @log_redactor ||= Redactor.new(@secrets)
76
- data = @log_redactor.append(data)
59
+ @transcript_redactor ||= Redactor.new(@secrets)
60
+ data = @transcript_redactor.append(data)
77
61
  return if data.empty?
78
62
  end
79
- target.respond_to?(:call) ? target.call(data) : emit(target, data)
63
+ emit(target, data)
64
+ nil
80
65
  end
81
66
 
82
- # 返回监听器列表副本,避免外部原地修改转发关系。
83
- def listeners = @listeners.dup
67
+ # 返回副本,避免外部原地修改转发关系;stdout 与其他 writer 使用相同协议。
68
+ def outputs = @outputs.dup
84
69
 
85
- # 校验所有监听器均可写后一次性替换列表,外部数组后续修改不会影响会话。
86
- def listeners=(outputs)
87
- outputs = Array(outputs)
88
- raise ArgumentError, "listeners must support write" unless outputs.all? { |output| output.respond_to?(:write) }
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
- @listeners = outputs.dup
76
+ @outputs = targets.dup
91
77
  end
92
78
 
93
- # 按各自开关将接收字节转发到 stdout 和监听器,不重复写日志。
79
+ # 原样转发协议字节,接收记录在读取时单独写入。
80
+ # @api private
94
81
  def propagate(data)
95
- emit($stdout, data) if log_stdout?
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, level:)
119
- if @secrets
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}", level:, event:)
112
+ trace("#{event} #{data.inspect}", severity: Logger::DEBUG, event:)
127
113
  end
128
114
 
129
- # 诊断与接收字节日志分开;回调元数据不含会话对象,避免格式化时意外展开原始缓冲。
130
- def trace(message, level: 1, event: :matched)
131
- return unless debug_level >= level
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 flush_log
121
+ def flush_transcript
148
122
  loop do
149
- return unless @log_redactor && log_output
123
+ return unless @transcript_redactor && transcript
150
124
 
151
- data = @log_redactor.finish
125
+ data = @transcript_redactor.finish
152
126
  return if data.empty?
153
127
 
154
- log_output.respond_to?(:call) ? log_output.call(data) : emit(log_output, data)
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}", level: 2, event: name)
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
@@ -1,28 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Expect
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.map { |connection| Session.for(connection) }
12
- @groups = patterns.groups.map { |connections, entries| [connections.map { |c| Session.for(c) }, entries] }
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.connection)
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 == CONTINUE
138
+ @deadline = next_deadline if CONTINUE.equal?(action)
137
139
  # 总期限到达时回主循环先派发已知 EOF;仅相对期限延续原有立即超时语义。
138
- return handle_timeout if action == CONTINUE_WITHOUT_RESET && expired? && !hard_expired?
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.connection).map { |pattern| pattern.call(session.connection) }
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
- @deadline = next_deadline if actions.include?(CONTINUE)
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 = !actions.include?(CONTINUE) && expired?
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 && session.reset_timeout_on_read?
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) = [CONTINUE, CONTINUE_WITHOUT_RESET].include?(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? = @deadline && Expect.monotonic >= @deadline
231
+ def expired? = !@deadline.nil? && Expect.monotonic >= @deadline
229
232
 
230
233
  # 绝对总期限不受接收数据和 continue 重置;已知 EOF 仍按原顺序派发。
231
- def hard_expired? = @hard_deadline && Expect.monotonic >= @hard_deadline
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.map(&:connection))
243
- return results.first unless action == CONTINUE && !hard_expired?
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
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Expect
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Expect
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
- # 拒绝空来源和非 Expect 对象,在任何 IO 读取之前暴露调用错误。
116
+ # 拒绝空来源和非 Session 对象,在任何 IO 读取之前暴露调用错误。
117
117
  def validate_sessions!(sessions)
118
- return if !sessions.empty? && sessions.all?(Expect)
118
+ return if !sessions.empty? && sessions.all?(Session)
119
119
 
120
- raise ArgumentError, "patterns require Expect sessions; specify from: for class-level waits"
120
+ raise ArgumentError, "patterns require Session objects; specify from: for class-level waits"
121
121
  end
122
122
  end
123
123
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Expect
3
+ module Expect
4
4
  # 单个日志字节流的过滤器。保留最长秘密长度减一的尾部,跨 write/read 分片仍可识别。
5
5
  # 掩码与原字节一起留存;重叠命中的区间取并集,已经输出的掩码不重复生成。
6
6
  # 不依赖会话或 IO;每个日志目标或诊断方向使用独立实例。
data/lib/expect/relay.rb CHANGED
@@ -1,29 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Expect
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
- outputs.each(&:restart_timeout)
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.first&.call
57
+ return [session, idle, queued] unless callback.call
57
58
  end
58
59
 
59
- result = Interaction.relay_buffer(session, @buffers, final: session.eof?) do |data|
60
- session.queue_output(data)
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 == :pending
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.delete(session)
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 = outputs
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 outputs = @sessions.flat_map(&:relay_outputs).reject(&:done?)
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.relay_outputs
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? = @deadline && Expect.monotonic >= @deadline
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
- Session.for(output.target) if output.target.is_a?(Expect)
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
- outputs.each { |output| output.check_timeout! if output.deadline && output.deadline < @deadline }
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
 
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Expect
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?(Expect)
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?(Expect)
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?(Expect) && target.closed?
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)
data/lib/expect/result.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Expect
3
+ module Expect
4
4
  # 一次等待的不可变快照;session 与 error 只引用来源和原始异常,不取得其所有权。
5
5
  # 文本和捕获值单独复制冻结,后续读取或调用者修改输入不会改变结果。
6
6
  # @!method self.new(**fields)