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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +1 -1
- data/README.zh-CN.md +1 -1
- data/docs/API.md +19 -6
- data/docs/MIGRATION.md +13 -1
- data/lib/expect/child_process.rb +120 -0
- data/lib/expect/interaction.rb +5 -5
- data/lib/expect/logging.rb +6 -3
- data/lib/expect/matcher.rb +6 -2
- data/lib/expect/pattern.rb +34 -1
- data/lib/expect/pty.rb +204 -2
- data/lib/expect/relay/writer.rb +90 -0
- data/lib/expect/resource_owner.rb +1 -0
- data/lib/expect/responder.rb +3 -2
- data/lib/expect/session.rb +21 -79
- data/lib/expect/session_factory.rb +35 -0
- data/lib/expect/version.rb +1 -1
- data/sig/expect.rbs +10 -7
- metadata +19 -4
- data/lib/expect/relay_writer.rb +0 -90
- data/lib/expect.rb +0 -167
|
@@ -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
|
data/lib/expect/responder.rb
CHANGED
|
@@ -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,
|
|
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
|
data/lib/expect/session.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
61
|
-
Cleanup.
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
-
#
|
|
448
|
-
def exec_child(command,
|
|
449
|
-
|
|
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
|
-
|
|
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
|
data/lib/expect/version.rb
CHANGED
data/sig/expect.rbs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 公开 Session 接口;内部匹配与转接协议在源码中标记为 @api private。
|
|
2
2
|
module Expect
|
|
3
|
-
type duration = Numeric
|
|
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.
|
|
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-
|
|
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/
|
|
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
|
data/lib/expect/relay_writer.rb
DELETED
|
@@ -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
|