expect-pty 0.8.2 → 0.9.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.
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expect
4
+ class Relay
5
+ # 向单个转接目标写入数据,维护发送进度与期限;恢复时不重放已成功写出的前缀。
6
+ # 只借用目标,不 dup 描述符,也不负责读取;所有就绪等待统一交给 Relay 调度。
7
+ # @api private
8
+ class Writer
9
+ # target 始终借用;deadline 是本轮背压预算,不表示整块数据已交付。
10
+ attr_reader :target, :deadline
11
+
12
+ # data 在本游标存活期间由上层保持不变,offset 始终以实际交付的字节数计量。
13
+ def initialize(target, data)
14
+ @target = target
15
+ @data = data
16
+ @offset = 0
17
+ @flushed = false
18
+ restart_timeout
19
+ end
20
+
21
+ # 初次排队或重新进入 interconnect 时开始新的写入预算;普通短写不会刷新期限。
22
+ # 只有 Expect 目标提供 write_timeout,原生 IO 和自定义目标只受转接总期限约束。
23
+ def restart_timeout
24
+ period = target.write_timeout if target.is_a?(Session)
25
+ @deadline = period && (Expect.monotonic + period)
26
+ end
27
+
28
+ # 真实 IO 可参与共同 select;自定义可写对象返回 nil,沿用其同步 write/flush 协议。
29
+ def io
30
+ return target.writer if target.is_a?(Session)
31
+
32
+ target if target.is_a?(IO)
33
+ end
34
+
35
+ # 数据交付完但 flush 失败仍未完成;重试只补 flush,不重新发送已接受的数据。
36
+ def done? = @offset == @data.bytesize && @flushed
37
+
38
+ # 每轮每目标至多一次写入;真实 IO 永不在此等待,交给 Relay 的共同 select。
39
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 游标、短写、flush 和期限按单个目标推进。
40
+ def advance(check_timeout: true)
41
+ return false if done?
42
+ raise IOError, "closed Expect session" if target.is_a?(Session) && target.closed?
43
+
44
+ if @offset < @data.bytesize
45
+ bytes = @data.byteslice(@offset, READ_SIZE)
46
+ count = io ? io.write_nonblock(bytes, exception: false) : target.write(bytes)
47
+ if io && count == :wait_writable
48
+ check_timeout! if check_timeout
49
+ return false
50
+ end
51
+ unless count.is_a?(Integer) && count.positive? && count <= bytes.bytesize
52
+ raise IOError, "write must return the number of accepted bytes"
53
+ end
54
+
55
+ @offset += count
56
+ # 先登记底层已接受的进度,再通知诊断;logger 失败后恢复只补未发送后缀。
57
+ trace_delivery(bytes.byteslice(0, count)) if target.is_a?(Session)
58
+ end
59
+ if @offset == @data.bytesize
60
+ # write_nonblock 已直接写到底层;普通 write 对象仍遵守自身 flush 协议。
61
+ target.flush if !io && target.respond_to?(:flush)
62
+ @flushed = true
63
+ end
64
+ true
65
+ rescue Errno::EINTR
66
+ # 未获得写入计数就不移动游标;先检查原期限,再让共同循环轮询输入并重试。
67
+ check_timeout! if check_timeout
68
+ true
69
+ end
70
+
71
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
72
+
73
+ # 保留本数据块已交付的进度,便于调用方区分未写入和部分写入;不清空恢复用的游标。
74
+ def check_timeout!
75
+ return unless deadline && Expect.monotonic >= deadline
76
+
77
+ raise WriteTimeout.new("relay target write timed out", bytes_written: @offset)
78
+ end
79
+
80
+ private
81
+
82
+ # 诊断可嵌套写入;外层只报告本游标的进度,原超时及其内层进度保留在 cause。
83
+ def trace_delivery(data)
84
+ target.__send__(:trace_data, :sending, data)
85
+ rescue WriteTimeout
86
+ raise WriteTimeout.new("relay interrupted by a diagnostic timeout", bytes_written: @offset)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -108,4 +108,5 @@ module Expect
108
108
  @pid = nil
109
109
  end
110
110
  end
111
+ private_constant :ResourceOwner
111
112
  end
@@ -31,7 +31,8 @@ module Expect
31
31
  # 等待完成模式或 EOF,同时自动处理提示;timeout 沿用会话默认值,应答不重置期限。
32
32
  # 完成块使用 PatternList DSL;Result 只描述最后一次匹配,全文请用会话 transcript 保存。
33
33
  # 每次等待快照规则,回调修改规则仅影响后续等待;不接管或关闭会话。
34
- def expect(session, *patterns, timeout: (session.timeout if session.is_a?(Session)), deadline: nil, &block)
34
+ def expect(session, *patterns, timeout: (session.timeout if session.is_a?(Session)), deadline: nil,
35
+ search_window: nil, &block)
35
36
  raise ArgumentError, "responder requires a Session" unless session.is_a?(Session)
36
37
 
37
38
  raise ArgumentError, "provide patterns or a pattern block, not both" if block && !patterns.empty?
@@ -39,7 +40,7 @@ module Expect
39
40
  rules = @rules.dup.freeze
40
41
  Expect.__send__(
41
42
  :run_expect, [session], [], timeout, deadline:, consume: true,
42
- reset_timeout_on_read: false, match_by_position: true
43
+ reset_timeout_on_read: false, match_by_position: true, search_window:
43
44
  ) do |list|
44
45
  patterns.each do |pattern|
45
46
  case pattern
@@ -48,34 +48,25 @@ module Expect
48
48
  end
49
49
 
50
50
  # 等待自身输入,返回 Result;匹配消费和接收重置策略只对本轮有效。
51
- def expect(*patterns, timeout: self.timeout, deadline: nil, consume: true, reset_timeout_on_read: false, &)
52
- Expect.__send__(:run_expect, [self], patterns, timeout, deadline:, consume:, reset_timeout_on_read:, &)
51
+ def expect(*patterns, timeout: self.timeout, deadline: nil, consume: true, reset_timeout_on_read: false,
52
+ search_window: nil, &)
53
+ Expect.__send__(:run_expect, [self], patterns, timeout,
54
+ deadline:, consume:, reset_timeout_on_read:, search_window:, &)
53
55
  end
54
56
 
55
57
  # 在新控制终端中执行命令并同步确认 exec 结果;同一会话只能启动一次。
56
- def spawn(*command, env: {}, chdir: nil, raw: false)
58
+ def spawn(*command, env: {}, chdir: nil, raw: false, shell: false)
57
59
  validate_spawn!(command)
60
+ raise ArgumentError, "shell must be true or false" unless [true, false].include?(shell)
61
+ raise ArgumentError, "shell requires one command string" if shell && command.size != 1
58
62
 
59
63
  @slave.raw! if raw
60
- from_child = to_parent = nil
61
- Cleanup.always(-> { ResourceOwner.close_handles(from_child, to_parent) }) do
62
- # 错误管道的写端在 exec 成功时自动关闭;父进程据此区分成功启动与 exec 前失败。
63
- # 管道返回到多重赋值之间也属于接管区间,确保 ensure 始终能找到两个端点。
64
- Thread.handle_interrupt(Object => :never) { from_child, to_parent = IO.pipe }
65
- to_parent.close_on_exec = true
66
- @command = command.map { |part| part.dup.freeze }.freeze
67
- # 原生 fork 返回后先登记 PID,再交付线程中断或终止,避免清理时遗漏新子进程。
68
- child = Thread.handle_interrupt(Object => :never) do
69
- @resources.pid = fork do
70
- # 子进程不继承父侧登记临界区,执行前仍可立即响应异步异常。
71
- Thread.handle_interrupt(Object => :immediate) { exec_child(command, env:, chdir:, from_child:, to_parent:) }
72
- end
64
+ @command = command.map { |part| part.dup.freeze }.freeze
65
+ Cleanup.on_failure(-> { hard_close }) do
66
+ child = @child_process.spawn(command, env:, chdir:, shell:) do |from_child, to_parent|
67
+ exec_child(command, env:, chdir:, from_child:, to_parent:, shell:)
73
68
  end
74
- to_parent.close
75
- @slave.close
76
- failure = from_child.read
77
- Cleanup.always(-> { hard_close }) { raise SpawnError, failure } unless failure.empty?
78
- trace("spawned pid=#{child}", event: :spawned)
69
+ trace(event: :spawned) { "spawned pid=#{child}" }
79
70
  self
80
71
  end
81
72
  end
@@ -100,10 +91,7 @@ module Expect
100
91
 
101
92
  # 非阻塞回收并缓存子进程状态;未退出或仅适配 IO 时返回 nil。
102
93
  def process_status
103
- @resources.reap
104
- rescue Errno::EINTR
105
- # 单次轮询被中断时状态仍未知;wait/close 会在原期限内继续,不在这里无限重试。
106
- @resources.status
94
+ @child_process.status
107
95
  end
108
96
 
109
97
  # 已知进程退出码;尚未回收或信号退出时为 nil。
@@ -182,7 +170,7 @@ module Expect
182
170
 
183
171
  begin
184
172
  # 单对象不需要中间数组与 join 副本;to_s、诊断和部分写入仍经过同一条路径。
185
- data = objects.size == 1 ? objects.first.to_s.b : objects.map { |object| object.to_s.b }.join
173
+ data = objects.size == 1 ? objects.first.to_s.b : objects.map { |object| object.to_s.b }.join("") # rubocop:disable Style/RedundantArgument -- 显式空分隔符隔离全局 $,。
186
174
  check_writable!
187
175
  trace_data(:sending, data)
188
176
  rescue WriteTimeout
@@ -328,7 +316,7 @@ module Expect
328
316
  end
329
317
  # 诊断回调可能嵌套等待;恢复本次结果后再交给正式模式回调,不能返回内层等待的结果。
330
318
  result = @last_result
331
- trace("matched pattern #{pattern.number}")
319
+ trace { "matched pattern #{pattern.number}" }
332
320
  @last_result = result
333
321
  end
334
322
 
@@ -400,6 +388,7 @@ module Expect
400
388
  def initialize_io(reader, writer:, slave: nil, own: false, timeout: nil, write_timeout: nil,
401
389
  buffer_limit: nil, logger: nil, transcript: nil, outputs: [])
402
390
  @resources = ResourceOwner.new(reader, writer:, slave:, own:)
391
+ @child_process = ChildProcess.new(@resources)
403
392
  raise ArgumentError, "reader must be a real IO" unless reader.is_a?(IO) && !reader.closed?
404
393
  raise ArgumentError, "writer must be a real IO" unless writer.is_a?(IO) && !writer.closed?
405
394
 
@@ -444,28 +433,9 @@ module Expect
444
433
  raise ArgumentError, "command is empty" if command.first.empty?
445
434
  end
446
435
 
447
- # 子进程独占控制终端;成功 exec 关闭错误管道,失败时回传后立即退出。
448
- def exec_child(command, env:, chdir:, from_child:, to_parent:)
449
- from_child.close
450
- Process.setsid
451
- # 创建独立进程会话后重新打开 slave,使它成为子进程的控制终端。
452
- File.open(@tty_name, File::RDWR) do |terminal|
453
- # 重定向操作系统的标准描述符;即使宿主替换过 Ruby 标准流,也能正确连接子进程。
454
- # rubocop:disable Style/GlobalStdStream
455
- STDIN.reopen(terminal)
456
- STDOUT.reopen(terminal)
457
- STDERR.reopen(terminal)
458
- # rubocop:enable Style/GlobalStdStream
459
- end
460
- @resources.close_handles
461
- Dir.chdir(chdir) if chdir
462
- exec(env, *command, close_others: true)
463
- rescue Exception => error # rubocop:disable Lint/RescueException -- 子进程回传启动异常后立即退出。
464
- begin
465
- to_parent.write("#{error.class}: #{error.message}")
466
- ensure
467
- exit! 127
468
- end
436
+ # 保留 Session 子类的启动钩子;进程对象管理控制终端和错误协议。
437
+ def exec_child(command, **)
438
+ @child_process.exec_child(command, **)
469
439
  end
470
440
 
471
441
  # EINTR 未确认交付时不移动游标;其他写入计数必须落在当前块范围内。
@@ -559,28 +529,9 @@ module Expect
559
529
  wait_for_child(1, signal: "KILL")
560
530
  end
561
531
 
562
- # 每阶段只计算一次期限;回收或信号被中断后仍沿用剩余预算,零预算也先做一次尝试。
532
+ # Session 的 sleep 钩子保留给受控时钟;期限与信号状态属于进程对象。
563
533
  def wait_for_child(period, signal: nil)
564
- deadline = period && (Expect.monotonic + period)
565
- loop do
566
- status = process_status
567
- return status if status || !pid || @resources.owner != Process.pid
568
-
569
- begin
570
- signal_child(signal) if signal
571
- signal = nil
572
- rescue Errno::EINTR
573
- # 下轮先回收再重试信号,避免在无限等待或持续中断时忙等。
574
- nil
575
- end
576
- # ESRCH 后可能已完成回收;即使预算耗尽,也要返回刚获得的状态。
577
- return process_status unless pid
578
-
579
- remaining = deadline && (deadline - Expect.monotonic)
580
- return nil if remaining && remaining <= 0
581
-
582
- sleep(remaining ? [0.01, remaining].min : 0.01)
583
- end
534
+ @child_process.wait(period, signal:) { |pause| sleep(pause) }
584
535
  end
585
536
 
586
537
  # 只结束读取方向并冲刷接收记录;EOF 不代表子进程已经退出或写端已经关闭。
@@ -603,14 +554,5 @@ module Expect
603
554
  # 丢弃的前缀使当前窗口与已转发历史之间不再连续。
604
555
  @relay_history.clear
605
556
  end
606
-
607
- # 仅由资源创建者向仍未回收的子进程发送信号;若进程刚好退出,则尝试回收。
608
- def signal_child(signal)
609
- return unless pid && @resources.owner == Process.pid
610
-
611
- Process.kill(signal, pid)
612
- rescue Errno::ESRCH
613
- @resources.reap
614
- end
615
557
  end
616
558
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expect
4
+ # 统一管理部分构造对象;调用方只在受保护的移交区间接收已初始化会话。
5
+ # @api private
6
+ module SessionFactory
7
+ def self.pty(**)
8
+ session = nil
9
+ Cleanup.on_failure(-> { session&.cleanup_session(nil, writer: nil, own: false) }) do
10
+ session = Session.allocate
11
+ Thread.handle_interrupt(Object => :immediate) { session.__send__(:initialize, **) }
12
+ session
13
+ end
14
+ end
15
+
16
+ def self.io(reader, writer:, own:, **)
17
+ session = nil
18
+ cleanup = lambda do
19
+ if session
20
+ session.cleanup_session(reader, writer:, own:)
21
+ elsif own
22
+ ResourceOwner.close_handles(reader, writer)
23
+ end
24
+ end
25
+ Cleanup.on_failure(cleanup) do
26
+ session = Session.allocate
27
+ Thread.handle_interrupt(Object => :immediate) do
28
+ session.__send__(:initialize_io, reader, writer:, own:, **)
29
+ end
30
+ session
31
+ end
32
+ end
33
+ end
34
+ private_constant :SessionFactory
35
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Expect
4
4
  # Gem 与库共用的版本号;独立文件使 gemspec 无需加载完整会话实现。
5
- VERSION = "0.8.2"
5
+ VERSION = "0.9.0"
6
6
  end
data/sig/expect.rbs CHANGED
@@ -1,6 +1,6 @@
1
1
  # 公开 Session 接口;内部匹配与转接协议在源码中标记为 @api private。
2
2
  module Expect
3
- type duration = Numeric | String
3
+ type duration = Numeric
4
4
  type pattern = String | Regexp | :eof | :timeout
5
5
  type result_error = :eof | :timeout | Exception | nil
6
6
  type result_fields = { number: Integer?, error: result_error, match: String?, before: String?, after: String?, session: Session?, captures: Array[String?] }
@@ -21,6 +21,9 @@ module Expect
21
21
  CONTINUE_WITHOUT_RESET: :continue_without_reset
22
22
 
23
23
  class SpawnError < StandardError
24
+ attr_reader errno: Integer?
25
+
26
+ def initialize: (?String message, ?errno: Integer?) -> void
24
27
  end
25
28
 
26
29
  class ReentrancyError < StandardError
@@ -33,12 +36,12 @@ module Expect
33
36
  end
34
37
 
35
38
  def self.spawn: (
36
- *String command, ?env: Hash[String, String?], ?chdir: String?, ?raw: bool, ?graceful: bool,
39
+ *String command, ?env: Hash[String, String?], ?chdir: String?, ?raw: bool, ?shell: bool, ?graceful: bool,
37
40
  ?timeout: duration?, ?write_timeout: duration?, ?buffer_limit: Integer?,
38
41
  ?logger: _Logger?, ?transcript: _Writer?, ?outputs: Array[_Writer]
39
42
  ) -> Session
40
43
  | [A] (
41
- *String command, ?env: Hash[String, String?], ?chdir: String?, ?raw: bool, ?graceful: bool,
44
+ *String command, ?env: Hash[String, String?], ?chdir: String?, ?raw: bool, ?shell: bool, ?graceful: bool,
42
45
  ?timeout: duration?, ?write_timeout: duration?, ?buffer_limit: Integer?,
43
46
  ?logger: _Logger?, ?transcript: _Writer?, ?outputs: Array[_Writer]
44
47
  ) { (Session) -> A } -> A
@@ -66,7 +69,7 @@ module Expect
66
69
 
67
70
  def self.expect: (
68
71
  *pattern patterns, ?from: (Session | Array[Session]), ?timeout: duration?, ?deadline: duration?,
69
- ?consume: bool, ?reset_timeout_on_read: bool
72
+ ?consume: bool, ?reset_timeout_on_read: bool, ?search_window: Integer?
70
73
  ) ?{ (PatternList) -> untyped } -> Result
71
74
 
72
75
  class Session
@@ -75,10 +78,10 @@ module Expect
75
78
  ?logger: _Logger?, ?transcript: _Writer?, ?outputs: Array[_Writer]
76
79
  ) -> void
77
80
 
78
- def spawn: (*String command, ?env: Hash[String, String?], ?chdir: String?, ?raw: bool) -> Session
81
+ def spawn: (*String command, ?env: Hash[String, String?], ?chdir: String?, ?raw: bool, ?shell: bool) -> Session
79
82
 
80
83
  def expect: (
81
- *pattern patterns, ?timeout: duration?, ?deadline: duration?, ?consume: bool, ?reset_timeout_on_read: bool
84
+ *pattern patterns, ?timeout: duration?, ?deadline: duration?, ?consume: bool, ?reset_timeout_on_read: bool, ?search_window: Integer?
82
85
  ) ?{ (PatternList) -> untyped } -> Result
83
86
 
84
87
  def after: () -> String?
@@ -272,7 +275,7 @@ module Expect
272
275
  def on: ((String | Regexp) pattern, ?reply: String?, ?limit: Integer?)
273
276
  ?{ (Session) -> untyped } -> Responder
274
277
 
275
- def expect: (Session session, *pattern patterns, ?timeout: duration?, ?deadline: duration?)
278
+ def expect: (Session session, *pattern patterns, ?timeout: duration?, ?deadline: duration?, ?search_window: Integer?)
276
279
  ?{ (PatternList) -> untyped } -> Result
277
280
 
278
281
  def inspect: () -> 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.8.2
4
+ version: 0.9.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-11 00:00:00.000000000 Z
10
+ date: 2026-10-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: io-console
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.6'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: logger
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -64,7 +78,7 @@ files:
64
78
  - docs/API.md
65
79
  - docs/COMPATIBILITY.md
66
80
  - docs/MIGRATION.md
67
- - lib/expect.rb
81
+ - lib/expect/child_process.rb
68
82
  - lib/expect/cleanup.rb
69
83
  - lib/expect/interaction.rb
70
84
  - lib/expect/literal_prefix.rb
@@ -75,11 +89,12 @@ files:
75
89
  - lib/expect/pty.rb
76
90
  - lib/expect/redactor.rb
77
91
  - lib/expect/relay.rb
78
- - lib/expect/relay_writer.rb
92
+ - lib/expect/relay/writer.rb
79
93
  - lib/expect/resource_owner.rb
80
94
  - lib/expect/responder.rb
81
95
  - lib/expect/result.rb
82
96
  - lib/expect/session.rb
97
+ - lib/expect/session_factory.rb
83
98
  - lib/expect/version.rb
84
99
  - sig/expect.rbs
85
100
  homepage: https://github.com/gatework/expect-ruby
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Expect
4
- # 向单个转接目标写入数据,维护发送进度与期限;恢复时不重放已成功写出的前缀。
5
- # 只借用目标,不 dup 描述符,也不负责读取;所有就绪等待统一交给 Relay 调度。
6
- # @api private
7
- class RelayWriter
8
- # target 始终借用;deadline 是本轮背压预算,不表示整块数据已交付。
9
- attr_reader :target, :deadline
10
-
11
- # data 在本游标存活期间由上层保持不变,offset 始终以实际交付的字节数计量。
12
- def initialize(target, data)
13
- @target = target
14
- @data = data
15
- @offset = 0
16
- @flushed = false
17
- restart_timeout
18
- end
19
-
20
- # 初次排队或重新进入 interconnect 时开始新的写入预算;普通短写不会刷新期限。
21
- # 只有 Expect 目标提供 write_timeout,原生 IO 和自定义目标只受转接总期限约束。
22
- def restart_timeout
23
- period = target.write_timeout if target.is_a?(Session)
24
- @deadline = period && (Expect.monotonic + period)
25
- end
26
-
27
- # 真实 IO 可参与共同 select;自定义可写对象返回 nil,沿用其同步 write/flush 协议。
28
- def io
29
- return target.writer if target.is_a?(Session)
30
-
31
- target if target.is_a?(IO)
32
- end
33
-
34
- # 数据交付完但 flush 失败仍未完成;重试只补 flush,不重新发送已接受的数据。
35
- def done? = @offset == @data.bytesize && @flushed
36
-
37
- # 每轮每目标至多一次写入;真实 IO 永不在此等待,交给 Relay 的共同 select。
38
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 游标、短写、flush 和期限按单个目标推进。
39
- def advance(check_timeout: true)
40
- return false if done?
41
- raise IOError, "closed Expect session" if target.is_a?(Session) && target.closed?
42
-
43
- if @offset < @data.bytesize
44
- bytes = @data.byteslice(@offset, READ_SIZE)
45
- count = io ? io.write_nonblock(bytes, exception: false) : target.write(bytes)
46
- if io && count == :wait_writable
47
- check_timeout! if check_timeout
48
- return false
49
- end
50
- unless count.is_a?(Integer) && count.positive? && count <= bytes.bytesize
51
- raise IOError, "write must return the number of accepted bytes"
52
- end
53
-
54
- @offset += count
55
- # 先登记底层已接受的进度,再通知诊断;logger 失败后恢复只补未发送后缀。
56
- trace_delivery(bytes.byteslice(0, count)) if target.is_a?(Session)
57
- end
58
- if @offset == @data.bytesize
59
- # write_nonblock 已直接写到底层;普通 write 对象仍遵守自身 flush 协议。
60
- target.flush if !io && target.respond_to?(:flush)
61
- @flushed = true
62
- end
63
- true
64
- rescue Errno::EINTR
65
- # 未获得写入计数就不移动游标;先检查原期限,再让共同循环轮询输入并重试。
66
- check_timeout! if check_timeout
67
- true
68
- end
69
-
70
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
71
-
72
- # 保留本数据块已交付的进度,便于调用方区分未写入和部分写入;不清空恢复用的游标。
73
- def check_timeout!
74
- return unless deadline && Expect.monotonic >= deadline
75
-
76
- raise WriteTimeout.new("relay target write timed out", bytes_written: @offset)
77
- end
78
-
79
- private
80
-
81
- # 诊断可嵌套写入;外层只报告本游标的进度,原超时及其内层进度保留在 cause。
82
- def trace_delivery(data)
83
- target.__send__(:trace_data, :sending, data)
84
- rescue WriteTimeout
85
- raise WriteTimeout.new("relay interrupted by a diagnostic timeout", bytes_written: @offset)
86
- end
87
- end
88
-
89
- private_constant :RelayWriter
90
- end