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
data/lib/expect.rb
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "pty"
|
|
4
|
-
require "io/console"
|
|
5
|
-
require "stringio"
|
|
6
|
-
require_relative "expect/version"
|
|
7
|
-
require_relative "expect/result"
|
|
8
|
-
require_relative "expect/resource_owner"
|
|
9
|
-
require_relative "expect/pattern"
|
|
10
|
-
require_relative "expect/pattern_list"
|
|
11
|
-
require_relative "expect/matcher"
|
|
12
|
-
require_relative "expect/cleanup"
|
|
13
|
-
require_relative "expect/session"
|
|
14
|
-
require_relative "expect/responder"
|
|
15
|
-
|
|
16
|
-
# PTY 会话的创建、匹配与转接入口;可变状态只属于返回的 Session。
|
|
17
|
-
module Expect
|
|
18
|
-
# 重置相对期限后继续等待。
|
|
19
|
-
CONTINUE = :continue
|
|
20
|
-
# 保留原相对期限继续等待。
|
|
21
|
-
CONTINUE_WITHOUT_RESET = :continue_without_reset
|
|
22
|
-
# 单轮非阻塞读写的最大字节数。
|
|
23
|
-
# @api private
|
|
24
|
-
READ_SIZE = 16_384
|
|
25
|
-
|
|
26
|
-
# PTY 创建后命令启动失败。
|
|
27
|
-
class SpawnError < StandardError; end
|
|
28
|
-
|
|
29
|
-
# 拒绝同源递归转接及接收交付回调中的同源递归读取,防止数据归属和顺序被破坏。
|
|
30
|
-
class ReentrancyError < StandardError; end
|
|
31
|
-
|
|
32
|
-
# 本次写入已经确认交付的进度;其他嵌套写入的异常保留在 cause。
|
|
33
|
-
class WriteTimeout < IOError
|
|
34
|
-
# 本次 write 已被底层接受的字节数。
|
|
35
|
-
attr_reader :bytes_written
|
|
36
|
-
|
|
37
|
-
# 构造包含已交付字节数的背压超时。
|
|
38
|
-
def initialize(message = "write timed out", bytes_written: 0)
|
|
39
|
-
@bytes_written = bytes_written
|
|
40
|
-
super(message)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
class << self
|
|
45
|
-
# 创建并启动 Session;有块时返回块结果,并按 graceful 策略关闭所属资源。
|
|
46
|
-
# raw 只控制本次子进程 PTY;日志与输出对象始终借用。
|
|
47
|
-
def spawn(*command, env: {}, chdir: nil, raw: false, graceful: false,
|
|
48
|
-
timeout: nil, write_timeout: nil, buffer_limit: nil, logger: nil, transcript: nil, outputs: [])
|
|
49
|
-
session = nil
|
|
50
|
-
spawned = false
|
|
51
|
-
cleanup = lambda do
|
|
52
|
-
if session && (block_given? || !spawned)
|
|
53
|
-
session.cleanup_session(nil, writer: nil, own: false, graceful: spawned && graceful)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
Cleanup.always(cleanup) do
|
|
57
|
-
# 先持有对象再初始化,构造完成到工厂接管之间不能留下无人负责关闭的窗口。
|
|
58
|
-
session = Session.allocate
|
|
59
|
-
session.__send__(:initialize, timeout:, write_timeout:, buffer_limit:, logger:, transcript:, outputs:)
|
|
60
|
-
session.spawn(*command, env:, chdir:, raw:)
|
|
61
|
-
spawned = true
|
|
62
|
-
block_given? ? yield(session) : session
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# 适配真实 IO;own 只决定读写端点的关闭责任,日志与输出目标仍由调用者管理。
|
|
67
|
-
# 有块时返回块结果;初始化失败也清理已取得的所属端点。
|
|
68
|
-
def open(io, writer: io, own: false, graceful: false,
|
|
69
|
-
timeout: nil, write_timeout: nil, buffer_limit: nil, logger: nil, transcript: nil, outputs: [])
|
|
70
|
-
session = nil
|
|
71
|
-
initialized = false
|
|
72
|
-
cleanup = lambda do
|
|
73
|
-
if session && (block_given? || !initialized)
|
|
74
|
-
session.cleanup_session(io, writer:, own:, graceful: initialized && graceful)
|
|
75
|
-
elsif !session && own
|
|
76
|
-
ResourceOwner.close_handles(io, writer)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
Cleanup.always(cleanup) do
|
|
80
|
-
session = Session.allocate
|
|
81
|
-
session.__send__(:initialize_io, io, writer:, own:, timeout:, write_timeout:, buffer_limit:,
|
|
82
|
-
logger:, transcript:, outputs:)
|
|
83
|
-
initialized = true
|
|
84
|
-
block_given? ? yield(session) : session
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# 按 outputs 建立转发图,返回引发停止的 Session;总期限到达返回 nil。
|
|
89
|
-
def interconnect(*sessions, timeout: nil)
|
|
90
|
-
raise ArgumentError, "interconnect requires Session objects" unless sessions.any? && sessions.all?(Session)
|
|
91
|
-
|
|
92
|
-
Relay.new(sessions, timeout).run
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# 共同等待指定来源,返回不可变 Result。consume 控制本轮是否消费匹配文本。
|
|
96
|
-
# reset_timeout_on_read 仅重置相对期限,deadline 始终是不可延长的总期限。
|
|
97
|
-
def expect(*patterns, from: [], timeout: nil, deadline: nil, consume: true, reset_timeout_on_read: false, &)
|
|
98
|
-
run_expect(from, patterns, timeout, deadline:, consume:, reset_timeout_on_read:, &)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# 返回回调继续控制符,reset_timeout 为 false 时保留原相对期限。
|
|
102
|
-
def continue(reset_timeout: true) = reset_timeout ? CONTINUE : CONTINUE_WITHOUT_RESET
|
|
103
|
-
|
|
104
|
-
# 当前单调时钟秒数;供跨多次等待共享 deadline。
|
|
105
|
-
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
106
|
-
|
|
107
|
-
# 转换有限非负秒数,nil 表示无相对期限。
|
|
108
|
-
def duration(value)
|
|
109
|
-
return nil if value.nil?
|
|
110
|
-
|
|
111
|
-
number = Float(value)
|
|
112
|
-
raise ArgumentError, "duration must be finite and nonnegative" unless number.finite? && number >= 0
|
|
113
|
-
|
|
114
|
-
number
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# 返回可读 Session,不消费输入;去重、排除已关闭来源,并保留声明顺序。
|
|
118
|
-
def readable_sessions(*sessions, timeout: 0)
|
|
119
|
-
timeout = duration(timeout)
|
|
120
|
-
raise ArgumentError, "readable_sessions requires Session objects" unless sessions.all?(Session)
|
|
121
|
-
|
|
122
|
-
active = sessions.uniq(&:object_id).reject(&:closed?)
|
|
123
|
-
return [] if active.empty?
|
|
124
|
-
|
|
125
|
-
deadline = timeout && (monotonic + timeout)
|
|
126
|
-
polled = false
|
|
127
|
-
ready = nil
|
|
128
|
-
loop do
|
|
129
|
-
remaining = deadline && [deadline - monotonic, 0].max
|
|
130
|
-
return [] if polled && remaining&.zero?
|
|
131
|
-
|
|
132
|
-
begin
|
|
133
|
-
ready = IO.select(active.map(&:to_io), nil, nil, remaining)
|
|
134
|
-
break
|
|
135
|
-
rescue Errno::EINTR
|
|
136
|
-
polled = true
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
return [] unless ready
|
|
140
|
-
|
|
141
|
-
select_ready_sessions(active, ready.first)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
private
|
|
145
|
-
|
|
146
|
-
# 按对象身份归属就绪描述符,不能用 IO 的值相等规则合并来源。
|
|
147
|
-
def select_ready_sessions(sessions, readable)
|
|
148
|
-
by_io = readable.each_with_object({}.compare_by_identity) { |io, index| index[io] = true }
|
|
149
|
-
sessions.select { |session| by_io.key?(session.to_io) }
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# 声明完成后固定模式;有参数块保留调用方 self,无参数块使用模式 DSL。
|
|
153
|
-
def run_expect(sessions, patterns, timeout, deadline:, consume:, reset_timeout_on_read:,
|
|
154
|
-
match_by_position: false, &block)
|
|
155
|
-
timeout = duration(timeout)
|
|
156
|
-
deadline = Float(deadline) unless deadline.nil?
|
|
157
|
-
raise ArgumentError, "deadline must be finite" if deadline && !deadline.finite?
|
|
158
|
-
raise ArgumentError, "provide patterns or a pattern block, not both" if block_given? && !patterns.empty?
|
|
159
|
-
|
|
160
|
-
pattern_list = PatternList.new(sessions, patterns)
|
|
161
|
-
if block
|
|
162
|
-
block.parameters.empty? ? pattern_list.instance_exec(&block) : block.call(pattern_list)
|
|
163
|
-
end
|
|
164
|
-
Matcher.new(pattern_list, timeout, deadline:, consume:, reset_timeout_on_read:, match_by_position:).run
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|