expect-pty 0.5.3 → 0.6.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 +28 -0
- data/README.md +28 -19
- data/docs/API.md +88 -0
- data/docs/MIGRATION.md +15 -0
- data/lib/expect/cleanup.rb +33 -0
- data/lib/expect/configuration.rb +87 -66
- data/lib/expect/interaction.rb +226 -209
- data/lib/expect/logging.rb +157 -156
- data/lib/expect/matcher.rb +30 -29
- data/lib/expect/pattern.rb +8 -2
- data/lib/expect/pattern_list.rb +35 -5
- data/lib/expect/redactor.rb +11 -4
- data/lib/expect/relay.rb +97 -73
- data/lib/expect/relay_writer.rb +2 -1
- data/lib/expect/result.rb +32 -8
- data/lib/expect/session.rb +508 -0
- data/lib/expect/session_resources.rb +10 -2
- data/lib/expect/terminal.rb +78 -78
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +333 -520
- data/sig/expect.rbs +355 -0
- metadata +13 -106
- data/.rubocop.yml +0 -66
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -38
- data/benchmark/matching.rb +0 -98
- data/benchmark/redactor.rb +0 -64
- data/benchmark/relay.rb +0 -48
- data/benchmark/scaling.rb +0 -70
- data/benchmark/send_slow.rb +0 -41
- data/benchmark/support.rb +0 -114
- data/docs/COMPATIBILITY.md +0 -98
- data/docs/INTERNAL_CONTRACTS.md +0 -154
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -456
- data/examples/dialogue.rb +0 -30
- data/examples/kibitz/README.md +0 -81
- data/examples/kibitz/kibitz.rb +0 -142
- data/examples/kibitz/test_kibitz.rb +0 -37
- data/examples/ssh_auto.rb +0 -94
- data/examples/ssh_interact.rb +0 -159
- data/examples/ssh_login.rb +0 -64
- data/expect-pty.gemspec +0 -33
- data/script/ci +0 -121
- data/script/release.rb +0 -319
- data/test/buffer_accounting_test.rb +0 -83
- data/test/cleanup_test.rb +0 -251
- data/test/compare_upstream.rb +0 -157
- data/test/configuration_test.rb +0 -133
- data/test/deadline_test.rb +0 -237
- data/test/diagnostics_test.rb +0 -353
- data/test/edge_case_test.rb +0 -262
- data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
- data/test/fixtures/ssh_scripts/02_output.sh +0 -5
- data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
- data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
- data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
- data/test/initialization_failure_test.rb +0 -92
- data/test/integration/README.md +0 -109
- data/test/integration/ssh_scripts.rb +0 -94
- data/test/interact_test.rb +0 -253
- data/test/interconnect_test.rb +0 -425
- data/test/io_test.rb +0 -321
- data/test/kibitz_test.rb +0 -45
- data/test/lifecycle_contract_test.rb +0 -71
- data/test/literal_scan_test.rb +0 -97
- data/test/matching_test.rb +0 -211
- data/test/multi_session_test.rb +0 -66
- data/test/ownership_sequence_test.rb +0 -208
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_interruption_test.rb +0 -296
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/relay_reentrancy_test.rb +0 -159
- data/test/release_test.rb +0 -375
- data/test/ruby_api_test.rb +0 -515
- data/test/scan_reuse_test.rb +0 -109
- data/test/script_logging_test.rb +0 -124
- data/test/support/interact_probe.rb +0 -125
- data/test/support/kibitz_probe.rb +0 -177
- data/test/support/script_probe.rb +0 -158
- data/test/terminal_cleanup_test.rb +0 -349
- data/test/test_helper.rb +0 -58
- data/test/timeout_test.rb +0 -289
- data/test/write_contract_test.rb +0 -105
data/lib/expect.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "pty"
|
|
4
4
|
require "io/console"
|
|
5
|
-
require "io/wait"
|
|
6
5
|
require "stringio"
|
|
7
6
|
require "forwardable"
|
|
8
7
|
require_relative "expect/version"
|
|
@@ -12,24 +11,33 @@ require_relative "expect/session_resources"
|
|
|
12
11
|
require_relative "expect/pattern"
|
|
13
12
|
require_relative "expect/pattern_list"
|
|
14
13
|
require_relative "expect/matcher"
|
|
14
|
+
require_relative "expect/cleanup"
|
|
15
|
+
require_relative "expect/session"
|
|
15
16
|
|
|
16
17
|
# 自动化交互会话:可以拥有一个 PTY 子进程,也可以适配已有可 select 的 IO。
|
|
17
18
|
# 缓冲和匹配统一保留原始字节,配置、匹配结果与资源生命周期分别管理。
|
|
18
19
|
class Expect
|
|
19
20
|
# 回调控制符:分别表示重置期限后继续,或保留原期限继续。
|
|
20
21
|
CONTINUE = :continue
|
|
22
|
+
# 继续等待但不重置相对期限。
|
|
21
23
|
CONTINUE_WITHOUT_RESET = :continue_without_reset
|
|
24
|
+
# 单轮非阻塞读写的最大字节数。
|
|
25
|
+
# @api private
|
|
22
26
|
READ_SIZE = 16_384
|
|
23
27
|
CONFIGURATION_MUTEX = Mutex.new
|
|
24
28
|
private_constant :CONFIGURATION_MUTEX
|
|
25
29
|
|
|
30
|
+
# PTY 创建后命令启动失败。
|
|
26
31
|
class SpawnError < StandardError; end
|
|
32
|
+
# 同一个来源不能同时交给两个 Relay。
|
|
27
33
|
class ReentrancyError < StandardError; end
|
|
28
34
|
|
|
29
35
|
# 已被底层接受的字节不可撤回;调用方可据此只处理尚未写出的后缀。
|
|
30
36
|
class WriteTimeout < IOError
|
|
31
37
|
attr_reader :bytes_written
|
|
32
38
|
|
|
39
|
+
# 记录本次已经确认交付的字节数,不推测异常写入是否产生副作用。
|
|
40
|
+
# @param bytes_written [Integer] 已交付字节数。
|
|
33
41
|
def initialize(message = "write timed out", bytes_written: 0)
|
|
34
42
|
@bytes_written = bytes_written
|
|
35
43
|
super(message)
|
|
@@ -38,6 +46,7 @@ class Expect
|
|
|
38
46
|
|
|
39
47
|
class << self
|
|
40
48
|
# 读取冻结的默认配置;子类未单独配置时继承父类快照。
|
|
49
|
+
# @return [Expect::Configuration] 冻结的类级默认快照。
|
|
41
50
|
def configuration
|
|
42
51
|
return @configuration if defined?(@configuration)
|
|
43
52
|
return superclass.configuration unless self == Expect
|
|
@@ -46,6 +55,9 @@ class Expect
|
|
|
46
55
|
end
|
|
47
56
|
|
|
48
57
|
# 基于旧快照构造可修改副本,全部赋值与配置块成功后才发布,异常时保留原配置。
|
|
58
|
+
# 配置关键字覆盖默认值,未知键抛出 ArgumentError。
|
|
59
|
+
# @yieldparam configuration [Expect::Configuration] 发布前可修改的副本。
|
|
60
|
+
# @return [Expect::Configuration] 新发布的冻结快照。
|
|
49
61
|
def configure(**)
|
|
50
62
|
raise ThreadError, "nested configure is not supported" if CONFIGURATION_MUTEX.owned?
|
|
51
63
|
|
|
@@ -59,59 +71,77 @@ class Expect
|
|
|
59
71
|
end
|
|
60
72
|
|
|
61
73
|
# 创建并启动会话;有块时返回块结果并确保关闭,无块时由调用方负责生命周期。
|
|
74
|
+
# @param command [Array<String>] 命令及参数;单字符串遵循 Ruby 自动 shell 语义,多参数按 argv 执行。
|
|
75
|
+
# @param env [Hash<String, String, nil>] 子进程环境覆盖。
|
|
76
|
+
# @param chdir [String, nil] 子进程工作目录。
|
|
77
|
+
# @yieldparam connection [Expect] 自动关闭的用户会话。
|
|
78
|
+
# @return [Expect, Object] 无块返回会话,有块返回块结果。
|
|
62
79
|
def spawn(*command, env: {}, chdir: nil, **)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
raise
|
|
72
|
-
ensure
|
|
73
|
-
if session && (block_given? || !spawned)
|
|
74
|
-
session.__send__(:cleanup, failed: failed) do
|
|
75
|
-
session.close(graceful: spawned && session.graceful_close?)
|
|
76
|
-
end
|
|
80
|
+
connection = nil
|
|
81
|
+
spawned = false
|
|
82
|
+
cleanup = -> { connection&.close(graceful: spawned && connection.graceful_close?) if block_given? || !spawned }
|
|
83
|
+
Cleanup.always(cleanup) do
|
|
84
|
+
connection = new(**)
|
|
85
|
+
connection.spawn(*command, env:, chdir:)
|
|
86
|
+
spawned = true
|
|
87
|
+
block_given? ? yield(connection) : connection
|
|
77
88
|
end
|
|
78
89
|
end
|
|
79
90
|
|
|
80
|
-
# 适配已有 IO;own: true
|
|
91
|
+
# 适配已有 IO;own: true 接管关闭责任,初始化失败也释放所属端点。
|
|
92
|
+
# @param io [IO] 可 select 的真实读端。
|
|
93
|
+
# @param writer [IO] 写端,默认与读端相同。
|
|
94
|
+
# @param own [Boolean] 是否取得 IO 关闭责任。
|
|
95
|
+
# @yieldparam connection [Expect] 自动关闭的用户会话。
|
|
96
|
+
# @return [Expect, Object] 无块返回会话,有块返回块结果。
|
|
81
97
|
def open(io, writer: io, own: false, **)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
rescue Exception # rubocop:disable Lint/RescueException -- 初始化和块异常均须保留,清理失败不能替换原始原因。
|
|
89
|
-
failed = true
|
|
90
|
-
raise
|
|
91
|
-
ensure
|
|
92
|
-
if session && (block_given? || !initialized)
|
|
93
|
-
session.__send__(:cleanup, failed: failed) do
|
|
94
|
-
session.__send__(:cleanup_session, io, writer: writer, own: own,
|
|
95
|
-
graceful: initialized && session.graceful_close?)
|
|
98
|
+
connection = nil
|
|
99
|
+
initialized = false
|
|
100
|
+
cleanup = lambda do
|
|
101
|
+
if connection && (block_given? || !initialized)
|
|
102
|
+
connection.__send__(:cleanup_session, io, writer:, own:,
|
|
103
|
+
graceful: initialized && connection.graceful_close?)
|
|
96
104
|
end
|
|
97
105
|
end
|
|
106
|
+
Cleanup.always(cleanup) do
|
|
107
|
+
connection = allocate
|
|
108
|
+
connection.__send__(:initialize_session, io, writer:, own:, **)
|
|
109
|
+
initialized = true
|
|
110
|
+
block_given? ? yield(connection) : connection
|
|
111
|
+
end
|
|
98
112
|
end
|
|
99
113
|
|
|
100
|
-
#
|
|
101
|
-
|
|
114
|
+
# 按 listeners 建立转发图,返回引发停止的用户会话或 nil。
|
|
115
|
+
# @param connections [Array<Expect>] 转接来源,不能为空。
|
|
116
|
+
# @param timeout [Numeric, nil] 总等待秒数,nil 无限。
|
|
117
|
+
# @return [Expect, nil] 停止来源;总期限到达返回 nil。
|
|
118
|
+
def interconnect(*connections, timeout: nil)
|
|
119
|
+
raise ArgumentError, "interconnect requires Expect sessions" unless connections.any? && connections.all?(Expect)
|
|
120
|
+
|
|
121
|
+
Relay.new(connections.map { |connection| Session.for(connection) }, timeout).run&.connection
|
|
122
|
+
end
|
|
102
123
|
|
|
103
124
|
# 多会话等待的完整结果入口;from: 提供默认来源,块内可分别指定每个模式的来源。
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
# @param patterns [Array<String, Regexp, Symbol>] 文本模式、:eof 或 :timeout。
|
|
126
|
+
# @param timeout [Numeric, nil] 相对秒数,nil 无限,0 非阻塞轮询。
|
|
127
|
+
# @param deadline [Numeric, nil] 单调时钟绝对期限,不被回调延长。
|
|
128
|
+
# @yieldparam patterns [Expect::PatternList] 有参数块接收构建器;无参数块以 DSL 执行。
|
|
129
|
+
# @return [Expect::Result] 本次等待的不可变快照。
|
|
130
|
+
def expect(*patterns, from: [], timeout: configuration.timeout, deadline: nil, &)
|
|
131
|
+
run_expect(from, patterns, timeout, deadline:, &)
|
|
106
132
|
end
|
|
107
133
|
|
|
108
134
|
# 返回继续等待的控制符,reset_timeout 决定是否重新计算匹配期限。
|
|
135
|
+
# @return [Symbol] 重置期限或保持期限的继续控制符。
|
|
109
136
|
def continue(reset_timeout: true) = reset_timeout ? CONTINUE : CONTINUE_WITHOUT_RESET
|
|
110
137
|
|
|
111
138
|
# 读取不受系统时间调整影响的单调时钟,所有相对超时共用此计时基准。
|
|
139
|
+
# @return [Float] 当前单调时钟秒数。
|
|
112
140
|
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
113
141
|
|
|
114
142
|
# 将秒数转换为有限的非负数,nil 表示无限;供配置和单次操作共用校验。
|
|
143
|
+
# @param value [Numeric, String, nil] 可转换为有限非负秒数的值。
|
|
144
|
+
# @return [Float, nil] 校验后的秒数。
|
|
115
145
|
def duration(value)
|
|
116
146
|
return nil if value.nil?
|
|
117
147
|
|
|
@@ -122,11 +152,13 @@ class Expect
|
|
|
122
152
|
end
|
|
123
153
|
|
|
124
154
|
# 等待并返回可读会话,不消费输入;去重并忽略已关闭会话,默认非阻塞。
|
|
155
|
+
# @param sessions [Array<Expect>] 待读取来源。
|
|
156
|
+
# @return [Array<Expect>] 就绪来源,保持声明顺序。
|
|
125
157
|
def readable_sessions(*sessions, timeout: 0)
|
|
126
158
|
timeout = duration(timeout)
|
|
127
159
|
raise ArgumentError, "readable_sessions requires Expect sessions" unless sessions.all?(Expect)
|
|
128
160
|
|
|
129
|
-
active = sessions.uniq.reject(&:closed?)
|
|
161
|
+
active = sessions.uniq(&:object_id).reject(&:closed?)
|
|
130
162
|
return [] if active.empty?
|
|
131
163
|
|
|
132
164
|
deadline = timeout && (monotonic + timeout)
|
|
@@ -145,11 +177,17 @@ class Expect
|
|
|
145
177
|
end
|
|
146
178
|
return [] unless ready
|
|
147
179
|
|
|
148
|
-
active
|
|
180
|
+
select_ready_sessions(active, ready.first)
|
|
149
181
|
end
|
|
150
182
|
|
|
151
183
|
private
|
|
152
184
|
|
|
185
|
+
# 就绪描述符按对象身份归属会话,不能由 IO 子类的值相等规则替代。
|
|
186
|
+
def select_ready_sessions(sessions, readable)
|
|
187
|
+
by_io = readable.each_with_object({}.compare_by_identity) { |io, index| index[io] = true }
|
|
188
|
+
sessions.select { |session| by_io.key?(session.to_io) }
|
|
189
|
+
end
|
|
190
|
+
|
|
153
191
|
# 先完成模式声明再启动引擎;无参数块支持简洁 DSL,有参数块保留调用方 self。
|
|
154
192
|
def run_expect(sessions, patterns, timeout, deadline: nil, &block)
|
|
155
193
|
timeout = duration(timeout)
|
|
@@ -161,509 +199,284 @@ class Expect
|
|
|
161
199
|
if block
|
|
162
200
|
block.parameters.empty? ? pattern_list.instance_exec(&block) : block.call(pattern_list)
|
|
163
201
|
end
|
|
164
|
-
Matcher.new(pattern_list
|
|
202
|
+
Matcher.new(pattern_list, timeout, deadline:).run
|
|
165
203
|
end
|
|
166
204
|
end
|
|
167
205
|
|
|
168
206
|
extend Forwardable
|
|
169
207
|
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
#
|
|
208
|
+
# @!method <<(object)
|
|
209
|
+
# 执行操作并返回当前用户会话,支持链式使用。
|
|
210
|
+
# @return [Expect]
|
|
211
|
+
# @!method after
|
|
212
|
+
# 读取最近结果中的字节快照;尚无结果时为 nil。
|
|
213
|
+
# @return [String, nil]
|
|
214
|
+
# @!method alive?
|
|
215
|
+
# 查询当前会话状态;输入结束、进程退出和关闭分别记录。
|
|
216
|
+
# @return [Boolean]
|
|
217
|
+
# @!method before
|
|
218
|
+
# 读取最近结果中的字节快照;尚无结果时为 nil。
|
|
219
|
+
# @return [String, nil]
|
|
220
|
+
# @!method buffer
|
|
221
|
+
# 取得接收字节;buffer 返回副本,clear_buffer 移交并清空现有内容。
|
|
222
|
+
# @return [String]
|
|
223
|
+
# @!method buffer=(value)
|
|
224
|
+
# 校验并替换当前设置;缓冲和集合采用副本,IO 与回调只借用。
|
|
225
|
+
# @return [void]
|
|
226
|
+
# @!method buffer_discarded_bytes
|
|
227
|
+
# 读取因匹配窗口上限裁剪而丢弃的累计字节数。
|
|
228
|
+
# @return [Integer]
|
|
229
|
+
# @!method buffer_limit
|
|
230
|
+
# 读取会话独立配置;更改类级默认值不会追溯影响已建立会话。
|
|
231
|
+
# @return [Integer, nil]
|
|
232
|
+
# @!method buffer_limit=(value)
|
|
233
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
234
|
+
# @return [void]
|
|
235
|
+
# @!method captures
|
|
236
|
+
# 读取最近捕获组;未参与的组为 nil,尚无结果时为空数组。
|
|
237
|
+
# @return [Array<String, nil>]
|
|
238
|
+
# @!method clear_buffer
|
|
239
|
+
# 取得接收字节;buffer 返回副本,clear_buffer 移交并清空现有内容。
|
|
240
|
+
# @return [String]
|
|
241
|
+
# @!method close(graceful: graceful_close?)
|
|
242
|
+
# 关闭所属资源并回收子进程;graceful 启用先收尾输出,硬关闭始终兜底。
|
|
243
|
+
# @return [nil]
|
|
244
|
+
# @!method closed?
|
|
245
|
+
# 查询当前会话状态;输入结束、进程退出和关闭分别记录。
|
|
246
|
+
# @return [Boolean]
|
|
247
|
+
# @!method command
|
|
248
|
+
# 读取已启动命令的冻结参数快照,未启动时为 nil。
|
|
249
|
+
# @return [Array<String>, nil]
|
|
250
|
+
# @!method debug_level
|
|
251
|
+
# 读取会话独立配置;更改类级默认值不会追溯影响已建立会话。
|
|
252
|
+
# @return [Integer]
|
|
253
|
+
# @!method debug_level=(value)
|
|
254
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
255
|
+
# @return [void]
|
|
256
|
+
# @!method diagnostic_output
|
|
257
|
+
# 读取借用的诊断目标;nil 使用 stderr。
|
|
258
|
+
# @return [#info, #write, Proc, nil]
|
|
259
|
+
# @!method diagnostic_output=(value)
|
|
260
|
+
# 校验并替换当前设置;缓冲和集合采用副本,IO 与回调只借用。
|
|
261
|
+
# @return [void]
|
|
262
|
+
# @!method eof?
|
|
263
|
+
# 查询当前会话状态;输入结束、进程退出和关闭分别记录。
|
|
264
|
+
# @return [Boolean]
|
|
265
|
+
# @!method error
|
|
266
|
+
# 读取最近 EOF、超时或原始 IO 错误,匹配成功时为 nil。
|
|
267
|
+
# @return [Symbol, Exception, nil]
|
|
268
|
+
# @!method exit_code
|
|
269
|
+
# 读取对应进程、IO 或匹配属性;尚无可用值时为 nil。
|
|
270
|
+
# @return [Integer, nil]
|
|
271
|
+
# @!method fileno
|
|
272
|
+
# 读取对应进程、IO 或匹配属性;尚无可用值时为 nil。
|
|
273
|
+
# @return [Integer, nil]
|
|
274
|
+
# @!method graceful_close=(value)
|
|
275
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
276
|
+
# @return [void]
|
|
277
|
+
# @!method graceful_close?
|
|
278
|
+
# 按 Ruby 真值规则查询此布尔配置。
|
|
279
|
+
# @return [Boolean]
|
|
280
|
+
# @!method hard_close(timeout: 0.2)
|
|
281
|
+
# 等待或回收子进程,未取得真实状态时返回 nil;期限以秒计。
|
|
282
|
+
# @return [Process::Status, nil]
|
|
283
|
+
# @!method interact(input: $stdin, escape: nil, output: nil, timeout: nil)
|
|
284
|
+
# 临时转接输入和输出,退出时恢复终端与监听设置;超时返回 nil。
|
|
285
|
+
# @return [Expect, nil]
|
|
286
|
+
# @!method last_result
|
|
287
|
+
# 读取最近一次等待的不可变结果。
|
|
288
|
+
# @return [Expect::Result, nil]
|
|
289
|
+
# @!method listeners
|
|
290
|
+
# 读取监听目标数组的副本;监听器只借用,不随会话关闭。
|
|
291
|
+
# @return [Array<#write>]
|
|
292
|
+
# @!method listeners=(value)
|
|
293
|
+
# 校验并替换当前设置;缓冲和集合采用副本,IO 与回调只借用。
|
|
294
|
+
# @return [void]
|
|
295
|
+
# @!method log_listeners=(value)
|
|
296
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
297
|
+
# @return [void]
|
|
298
|
+
# @!method log_listeners?
|
|
299
|
+
# 按 Ruby 真值规则查询此布尔配置。
|
|
300
|
+
# @return [Boolean]
|
|
301
|
+
# @!method log_output
|
|
302
|
+
# 读取当前接收日志目标,不包含发送数据。
|
|
303
|
+
# @return [#write, Proc, nil]
|
|
304
|
+
# @!method log_output=(value)
|
|
305
|
+
# 校验并替换当前设置;缓冲和集合采用副本,IO 与回调只借用。
|
|
306
|
+
# @return [void]
|
|
307
|
+
# @!method log_stdout=(value)
|
|
308
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
309
|
+
# @return [void]
|
|
310
|
+
# @!method log_stdout?
|
|
311
|
+
# 按 Ruby 真值规则查询此布尔配置。
|
|
312
|
+
# @return [Boolean]
|
|
313
|
+
# @!method log_to(target = nil, mode: "a", &block)
|
|
314
|
+
# 设置借用日志目标或打开所属日志文件;返回目标。
|
|
315
|
+
# @return [#write, Proc]
|
|
316
|
+
# @yieldparam bytes [String] 实际读取的字节。
|
|
317
|
+
# @yieldreturn [Object] 回调返回值不影响匹配。
|
|
318
|
+
# @!method match
|
|
319
|
+
# 读取最近结果中的字节快照;尚无结果时为 nil。
|
|
320
|
+
# @return [String, nil]
|
|
321
|
+
# @!method match_number
|
|
322
|
+
# 读取对应进程、IO 或匹配属性;尚无可用值时为 nil。
|
|
323
|
+
# @return [Integer, nil]
|
|
324
|
+
# @!method on_sequence(sequence, &block)
|
|
325
|
+
# 执行操作并返回当前用户会话,支持链式使用。
|
|
326
|
+
# @return [Expect]
|
|
327
|
+
# @yield 转义匹配完成后运行,nil/false 停止,其余返回值继续。
|
|
328
|
+
# @yieldreturn [Object] 是否继续转接。
|
|
329
|
+
# @!method pending_output?
|
|
330
|
+
# 查询当前会话状态;输入结束、进程退出和关闭分别记录。
|
|
331
|
+
# @return [Boolean]
|
|
332
|
+
# @!method pid
|
|
333
|
+
# 读取对应进程、IO 或匹配属性;尚无可用值时为 nil。
|
|
334
|
+
# @return [Integer, nil]
|
|
335
|
+
# @!method preserve_buffer=(value)
|
|
336
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
337
|
+
# @return [void]
|
|
338
|
+
# @!method preserve_buffer?
|
|
339
|
+
# 按 Ruby 真值规则查询此布尔配置。
|
|
340
|
+
# @return [Boolean]
|
|
341
|
+
# @!method process_status
|
|
342
|
+
# 非阻塞回收直属子进程并读取真实状态;未知时保留 nil。
|
|
343
|
+
# @return [Process::Status, nil]
|
|
344
|
+
# @!method puts(*objects)
|
|
345
|
+
# 按 Ruby puts 语义转换换行、nil 和数组后写入。
|
|
346
|
+
# @return [nil]
|
|
347
|
+
# @!method raw_pty=(value)
|
|
348
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
349
|
+
# @return [void]
|
|
350
|
+
# @!method raw_pty?
|
|
351
|
+
# 按 Ruby 真值规则查询此布尔配置。
|
|
352
|
+
# @return [Boolean]
|
|
353
|
+
# @!method raw_terminal=(value)
|
|
354
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
355
|
+
# @return [void]
|
|
356
|
+
# @!method raw_terminal?
|
|
357
|
+
# 按 Ruby 真值规则查询此布尔配置。
|
|
358
|
+
# @return [Boolean]
|
|
359
|
+
# @!method redact(*secrets)
|
|
360
|
+
# 执行操作并返回当前用户会话,支持链式使用。
|
|
361
|
+
# @return [Expect]
|
|
362
|
+
# @!method reset_timeout_on_read=(value)
|
|
363
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
364
|
+
# @return [void]
|
|
365
|
+
# @!method reset_timeout_on_read?
|
|
366
|
+
# 按 Ruby 真值规则查询此布尔配置。
|
|
367
|
+
# @return [Boolean]
|
|
368
|
+
# @!method send_slow(*objects, delay:)
|
|
369
|
+
# 返回已写字节数;背压超时抛出 WriteTimeout 并保留 bytes_written。
|
|
370
|
+
# @return [Integer]
|
|
371
|
+
# @!method slave
|
|
372
|
+
# 读取 PTY slave;适配已有 IO 时为 nil,启动后句柄已关闭。
|
|
373
|
+
# @return [IO, nil]
|
|
374
|
+
# @!method soft_close(timeout: 15, term_timeout: 1)
|
|
375
|
+
# 等待或回收子进程,未取得真实状态时返回 nil;期限以秒计。
|
|
376
|
+
# @return [Process::Status, nil]
|
|
377
|
+
# @!method spawn(*command, env: {}, chdir: nil)
|
|
378
|
+
# 执行操作并返回当前用户会话,支持链式使用。
|
|
379
|
+
# @return [Expect]
|
|
380
|
+
# @!method stty(*modes)
|
|
381
|
+
# 查询或设置终端模式;非 TTY 返回空字符串,失败抛出 IO 错误。
|
|
382
|
+
# @return [String]
|
|
383
|
+
# @!method timeout
|
|
384
|
+
# 读取会话独立配置;更改类级默认值不会追溯影响已建立会话。
|
|
385
|
+
# @return [Numeric, nil]
|
|
386
|
+
# @!method timeout=(value)
|
|
387
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
388
|
+
# @return [void]
|
|
389
|
+
# @!method to_io
|
|
390
|
+
# 取得底层读端或写端,不复制描述符。
|
|
391
|
+
# @return [IO]
|
|
392
|
+
# @!method tty?
|
|
393
|
+
# 查询当前会话状态;输入结束、进程退出和关闭分别记录。
|
|
394
|
+
# @return [Boolean]
|
|
395
|
+
# @!method tty_name
|
|
396
|
+
# 读取 PTY 终端路径;适配已有 IO 时为 nil。
|
|
397
|
+
# @return [String, nil]
|
|
398
|
+
# @!method wait(timeout: nil)
|
|
399
|
+
# 等待或回收子进程,未取得真实状态时返回 nil;期限以秒计。
|
|
400
|
+
# @return [Process::Status, nil]
|
|
401
|
+
# @!method winsize
|
|
402
|
+
# 读取终端行列数,保留原生 IO 错误。
|
|
403
|
+
# @return [Array<Integer>]
|
|
404
|
+
# @!method winsize=(value)
|
|
405
|
+
# 校验并替换当前设置;缓冲和集合采用副本,IO 与回调只借用。
|
|
406
|
+
# @return [void]
|
|
407
|
+
# @!method write(*objects)
|
|
408
|
+
# 返回已写字节数;背压超时抛出 WriteTimeout 并保留 bytes_written。
|
|
409
|
+
# @return [Integer]
|
|
410
|
+
# @!method write_log(*objects)
|
|
411
|
+
# 向当前接收日志补写数据;返回目标调用结果,过滤暂存时可为 nil。
|
|
412
|
+
# @return [Object]
|
|
413
|
+
# @!method write_timeout
|
|
414
|
+
# 读取会话独立配置;更改类级默认值不会追溯影响已建立会话。
|
|
415
|
+
# @return [Numeric, nil]
|
|
416
|
+
# @!method write_timeout=(value)
|
|
417
|
+
# 校验并设置会话配置;非法数值不改变当前值,布尔值遵循 Ruby 真值。
|
|
418
|
+
# @return [void]
|
|
419
|
+
# @!method writer
|
|
420
|
+
# 取得底层读端或写端,不复制描述符。
|
|
421
|
+
# @return [IO]
|
|
422
|
+
# @!method inspect
|
|
423
|
+
# 返回不含缓冲、命令或秘密内容的安全诊断摘要。
|
|
424
|
+
# @return [String]
|
|
425
|
+
# 用户会话只委托稳定接口,内核读写、转接标记和资源账本不出现在公开方法中。
|
|
426
|
+
def_delegators :@session,
|
|
427
|
+
:<<, :after, :alive?, :before, :buffer, :buffer=, :buffer_discarded_bytes, :buffer_limit,
|
|
428
|
+
:buffer_limit=, :captures, :clear_buffer, :close, :closed?, :command, :debug_level,
|
|
429
|
+
:debug_level=, :diagnostic_output, :diagnostic_output=, :eof?, :error, :exit_code, :fileno,
|
|
430
|
+
:graceful_close=, :graceful_close?, :hard_close, :interact, :last_result,
|
|
431
|
+
:listeners, :listeners=, :log_listeners=, :log_listeners?, :log_output,
|
|
432
|
+
:log_output=, :log_stdout=, :log_stdout?, :log_to, :match, :match_number,
|
|
433
|
+
:on_sequence, :pending_output?, :pid, :preserve_buffer=, :preserve_buffer?,
|
|
434
|
+
:process_status, :puts, :raw_pty=, :raw_pty?, :raw_terminal=,
|
|
435
|
+
:raw_terminal?, :redact, :reset_timeout_on_read=,
|
|
436
|
+
:reset_timeout_on_read?, :send_slow, :slave, :soft_close, :spawn, :stty, :timeout, :timeout=,
|
|
437
|
+
:to_io, :tty?, :tty_name, :wait, :winsize, :winsize=, :write, :write_log, :write_timeout,
|
|
438
|
+
:write_timeout=, :writer, :inspect
|
|
439
|
+
|
|
440
|
+
# 创建 PTY,可立即执行命令;未完成初始化时按局部所有权释放全部句柄。
|
|
441
|
+
# @param command [Array<String>] 可选启动命令及参数。
|
|
442
|
+
# @return [Expect] 已创建的用户会话。
|
|
184
443
|
def initialize(*command, env: {}, chdir: nil, **)
|
|
185
|
-
master
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
rescue Exception # rubocop:disable Lint/RescueException -- 构造异常时也要关闭已创建的资源并回收已启动的子进程。
|
|
191
|
-
failed = true
|
|
192
|
-
raise
|
|
193
|
-
ensure
|
|
194
|
-
cleanup(failed:) { cleanup_session(master, writer: master, slave: slave, own: true) } unless initialized
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# 在新控制终端中执行命令并同步确认 exec 结果;同一会话只能启动一次。
|
|
198
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- PTY 子进程启动与失败回传共用一次生命周期。
|
|
199
|
-
def spawn(*command, env: {}, chdir: nil)
|
|
200
|
-
raise SpawnError, "cannot reuse a spawned session" if @command
|
|
201
|
-
raise SpawnError, "only a new PTY session can spawn" unless @slave && !@slave.closed? && !closed?
|
|
202
|
-
raise ArgumentError, "command is required" if command.empty?
|
|
203
|
-
raise ArgumentError, "command arguments must be strings" unless command.all? do |part|
|
|
204
|
-
part.is_a?(String) && !part.include?("\0")
|
|
205
|
-
end
|
|
206
|
-
raise ArgumentError, "command is empty" if command.first.empty?
|
|
207
|
-
|
|
208
|
-
@slave.raw! if raw_pty?
|
|
209
|
-
# 错误管道的写端在 exec 成功时自动关闭;父进程据此区分成功启动与 exec 前失败。
|
|
210
|
-
from_child, to_parent = IO.pipe
|
|
211
|
-
to_parent.close_on_exec = true
|
|
212
|
-
@command = command.map { |part| part.dup.freeze }.freeze
|
|
213
|
-
child = fork do
|
|
214
|
-
from_child.close
|
|
215
|
-
Process.setsid
|
|
216
|
-
# 创建独立进程会话后重新打开 slave,使它成为子进程的控制终端。
|
|
217
|
-
File.open(@tty_name, File::RDWR) do |terminal|
|
|
218
|
-
# 重定向操作系统的标准描述符;即使宿主替换过 Ruby 标准流,也能正确连接子进程。
|
|
219
|
-
# rubocop:disable Style/GlobalStdStream
|
|
220
|
-
STDIN.reopen(terminal)
|
|
221
|
-
STDOUT.reopen(terminal)
|
|
222
|
-
STDERR.reopen(terminal)
|
|
223
|
-
# rubocop:enable Style/GlobalStdStream
|
|
224
|
-
end
|
|
225
|
-
@resources.close_handles
|
|
226
|
-
Dir.chdir(chdir) if chdir
|
|
227
|
-
exec(env, *command, close_others: true)
|
|
228
|
-
rescue Exception => error # rubocop:disable Lint/RescueException -- 子进程回传启动异常后立即退出。
|
|
229
|
-
begin
|
|
230
|
-
to_parent.write("#{error.class}: #{error.message}")
|
|
231
|
-
ensure
|
|
232
|
-
exit! 127
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
@resources.pid = child
|
|
236
|
-
to_parent.close
|
|
237
|
-
@slave.close
|
|
238
|
-
failure = from_child.read
|
|
239
|
-
unless failure.empty?
|
|
240
|
-
hard_close
|
|
241
|
-
raise SpawnError, failure
|
|
242
|
-
end
|
|
243
|
-
trace("spawned pid=#{child}", event: :spawned)
|
|
244
|
-
self
|
|
245
|
-
ensure
|
|
246
|
-
from_child&.close unless from_child&.closed?
|
|
247
|
-
to_parent&.close unless to_parent&.closed?
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
251
|
-
|
|
252
|
-
# 在当前会话等待文本或事件,返回模式序号或 nil。
|
|
253
|
-
def expect(...) = expect_result(...).number
|
|
254
|
-
|
|
255
|
-
# 使用会话默认超时构造一次等待,返回含匹配内容、来源和错误的 Result。
|
|
256
|
-
def expect_result(*patterns, timeout: self.timeout, deadline: nil, &)
|
|
257
|
-
self.class.__send__(:run_expect, [self], patterns, timeout, deadline: deadline, &)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
# 供实例回调返回继续控制符,语义与 Expect.continue 相同。
|
|
261
|
-
def continue(reset_timeout: true) = Expect.continue(reset_timeout: reset_timeout)
|
|
262
|
-
|
|
263
|
-
# 暴露底层读写 IO 与终端属性,供 select、终端设置及 IO 适配使用。
|
|
264
|
-
def to_io = @resources.reader
|
|
265
|
-
|
|
266
|
-
def writer = @resources.writer
|
|
267
|
-
|
|
268
|
-
def fileno = closed? ? nil : to_io.fileno
|
|
269
|
-
|
|
270
|
-
def tty? = !closed? && to_io.tty?
|
|
271
|
-
|
|
272
|
-
# 诊断时仅显示进程和描述符状态,避免默认对象展开泄露缓冲或日志内容。
|
|
273
|
-
def inspect = "#<#{self.class} pid=#{pid.inspect} fd=#{fileno.inspect} closed=#{closed?}>"
|
|
274
|
-
|
|
275
|
-
def pid = @resources.pid
|
|
276
|
-
|
|
277
|
-
# 非阻塞回收并缓存子进程状态;未退出或仅适配 IO 时返回 nil。
|
|
278
|
-
def process_status
|
|
279
|
-
@resources.reap
|
|
280
|
-
rescue Errno::EINTR
|
|
281
|
-
# 单次轮询被中断时状态仍未知;wait/close 会在原期限内继续,不在这里无限重试。
|
|
282
|
-
@resources.status
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def exit_code = process_status&.exitstatus
|
|
286
|
-
|
|
287
|
-
# 先刷新回收状态,再判断是否仍有未回收的子进程;不以 IO 是否关闭代替进程状态。
|
|
288
|
-
def alive?
|
|
289
|
-
process_status
|
|
290
|
-
!pid.nil?
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
# 区分会话关闭和输入结束,已关闭会话也不能继续读取。
|
|
294
|
-
def closed? = @closed || to_io.closed?
|
|
295
|
-
|
|
296
|
-
def eof? = @eof || closed?
|
|
297
|
-
|
|
298
|
-
# 以下访问器读取最近一次等待结果;未发生匹配时捕获组返回空数组。
|
|
299
|
-
def before = @last_result&.before
|
|
300
|
-
|
|
301
|
-
def after = @last_result&.after
|
|
302
|
-
|
|
303
|
-
def match = @last_result&.match
|
|
304
|
-
|
|
305
|
-
def match_number = @last_result&.number
|
|
306
|
-
|
|
307
|
-
def captures = @last_result&.captures || []
|
|
308
|
-
|
|
309
|
-
def error = @last_result&.error
|
|
310
|
-
|
|
311
|
-
# 返回缓冲副本,防止调用方原地修改绕过裁剪规则。
|
|
312
|
-
def buffer = @buffer.dup
|
|
313
|
-
|
|
314
|
-
# 复制并替换原始字节缓冲,应用当前上限;调用方后续修改原字符串不会影响会话。
|
|
315
|
-
def buffer=(value)
|
|
316
|
-
raise ArgumentError, "buffer must be a String" unless value.is_a?(String)
|
|
317
|
-
|
|
318
|
-
@buffer = value.b
|
|
319
|
-
@buffer_generation += 1
|
|
320
|
-
trim_buffer
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# 移交旧缓冲并换上新的空字节串,供显式清空或人工转接接管数据。
|
|
324
|
-
def clear_buffer
|
|
325
|
-
previous = @buffer
|
|
326
|
-
@buffer = "".b
|
|
327
|
-
@buffer_generation += 1
|
|
328
|
-
previous
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# 按 Ruby to_s 规则原样写入所有字节,返回字节数;背压等待受 write_timeout 限制。
|
|
332
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 写入、背压排空和同一期限必须同步推进。
|
|
333
|
-
def write(*objects)
|
|
334
|
-
raise IOError, "closed Expect session" if closed? || writer.closed?
|
|
335
|
-
|
|
336
|
-
data = objects.map { |object| object.to_s.b }.join
|
|
337
|
-
trace_data(:sending, data, level: 2) if debug_level >= 2
|
|
338
|
-
deadline = write_timeout && (Expect.monotonic + write_timeout)
|
|
339
|
-
offset = 0
|
|
340
|
-
while offset < data.bytesize
|
|
341
|
-
begin
|
|
342
|
-
chunk = data.byteslice(offset, READ_SIZE)
|
|
343
|
-
count = writer.write_nonblock(chunk, exception: false)
|
|
344
|
-
rescue Errno::EINTR
|
|
345
|
-
raise WriteTimeout.new(bytes_written: offset) if deadline && Expect.monotonic >= deadline
|
|
346
|
-
|
|
347
|
-
next
|
|
348
|
-
end
|
|
349
|
-
if count == :wait_writable
|
|
350
|
-
raise WriteTimeout.new(bytes_written: offset) if deadline && Expect.monotonic >= deadline
|
|
351
|
-
|
|
352
|
-
remaining = deadline && [deadline - Expect.monotonic, 0].max
|
|
353
|
-
# 子进程也可能因输出管道填满而停止读取;等可写时同时排空它的输出,避免双向死锁。
|
|
354
|
-
readers = eof? ? [] : [to_io]
|
|
355
|
-
begin
|
|
356
|
-
ready = IO.select(readers, [writer], nil, remaining)
|
|
357
|
-
raise WriteTimeout.new(bytes_written: offset) unless ready
|
|
358
|
-
|
|
359
|
-
if ready[0].include?(to_io)
|
|
360
|
-
begin
|
|
361
|
-
read_available
|
|
362
|
-
rescue WriteTimeout
|
|
363
|
-
# 日志或监听器可嵌套写入;对外报告本次写入进度,原异常通过 cause 保留。
|
|
364
|
-
raise WriteTimeout.new("write interrupted by an output timeout", bytes_written: offset)
|
|
365
|
-
end
|
|
366
|
-
end
|
|
367
|
-
rescue Errno::EINTR
|
|
368
|
-
next
|
|
369
|
-
end
|
|
370
|
-
else
|
|
371
|
-
unless count.is_a?(Integer) && count.positive? && count <= chunk.bytesize
|
|
372
|
-
raise IOError, "write must return the number of accepted bytes"
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
offset += count
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
data.bytesize
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
382
|
-
|
|
383
|
-
# 链式写入单个对象,返回当前会话。
|
|
384
|
-
def <<(object)
|
|
385
|
-
write(object)
|
|
386
|
-
self
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
# 委托 StringIO 处理换行、nil 和递归数组,再统一写入;返回 nil,与 Ruby puts 一致。
|
|
390
|
-
def puts(*objects)
|
|
391
|
-
output = StringIO.new("".b)
|
|
392
|
-
output.puts(*objects)
|
|
393
|
-
write(output.string)
|
|
394
|
-
nil
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
# 逐字符延迟发送,同时收集回复,适配输入处理较慢的交互程序;返回写入字节数。
|
|
398
|
-
def send_slow(*objects, delay:)
|
|
399
|
-
pause = Expect.duration(delay)
|
|
400
|
-
raise ArgumentError, "delay is required" unless pause
|
|
401
|
-
|
|
402
|
-
count = 0
|
|
403
|
-
objects.each do |object|
|
|
404
|
-
object.to_s.each_char do |character|
|
|
405
|
-
sleep(pause) if pause.positive?
|
|
406
|
-
count += write(character)
|
|
407
|
-
read_available if !eof? && to_io.wait_readable(0)
|
|
408
|
-
end
|
|
444
|
+
master = slave = nil
|
|
445
|
+
Cleanup.on_failure(-> { cleanup_session(master, writer: master, slave:, own: true) }) do
|
|
446
|
+
master, slave = PTY.open
|
|
447
|
+
initialize_session(master, writer: master, slave:, own: true, **)
|
|
448
|
+
spawn(*command, env:, chdir:) unless command.empty?
|
|
409
449
|
end
|
|
410
|
-
count
|
|
411
450
|
end
|
|
412
451
|
|
|
413
|
-
#
|
|
414
|
-
|
|
415
|
-
|
|
452
|
+
# 返回不可变结果;模式块无参数时使用 DSL,有参数时保留调用者 self。
|
|
453
|
+
# @param patterns [Array<String, Regexp, Symbol>] 文本模式、:eof 或 :timeout。
|
|
454
|
+
# @param timeout [Numeric, nil] 相对秒数,nil 无限,0 非阻塞轮询。
|
|
455
|
+
# @param deadline [Numeric, nil] 单调时钟绝对期限,不被回调延长。
|
|
456
|
+
# @yieldparam patterns [Expect::PatternList] 有参数块接收构建器;无参数块以 DSL 执行。
|
|
457
|
+
# @return [Expect::Result] 本次等待的不可变快照。
|
|
458
|
+
def expect(*patterns, timeout: self.timeout, deadline: nil, &)
|
|
459
|
+
self.class.__send__(:run_expect, [self], patterns, timeout, deadline:, &)
|
|
416
460
|
end
|
|
417
461
|
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
def
|
|
421
|
-
period = Expect.duration(timeout)
|
|
422
|
-
term_timeout = Expect.duration(term_timeout)
|
|
423
|
-
raise ArgumentError, "term_timeout must be finite" unless term_timeout
|
|
462
|
+
# 返回回调继续控制符,可选择保持原期限。
|
|
463
|
+
# @return [Symbol] 重置期限或保持期限的继续控制符。
|
|
464
|
+
def continue(reset_timeout: true) = Expect.continue(reset_timeout:)
|
|
424
465
|
|
|
425
|
-
|
|
426
|
-
until eof?
|
|
427
|
-
remaining = deadline && [deadline - Expect.monotonic, 0].max
|
|
428
|
-
break if remaining&.zero? || !to_io.wait_readable(remaining)
|
|
429
|
-
|
|
430
|
-
read_available
|
|
431
|
-
end
|
|
432
|
-
close_resources(timeout: deadline ? [deadline - Expect.monotonic, 0].max : nil,
|
|
433
|
-
term_timeout: term_timeout, force: false)
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
# 立即关闭句柄,再分阶段等待、TERM、KILL;不收集剩余输出,返回已回收状态或 nil。
|
|
437
|
-
def hard_close(timeout: 0.2)
|
|
438
|
-
period = Expect.duration(timeout)
|
|
439
|
-
raise ArgumentError, "hard_close timeout must be finite" unless period
|
|
466
|
+
private
|
|
440
467
|
|
|
441
|
-
|
|
442
|
-
end
|
|
468
|
+
attr_reader :session
|
|
443
469
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
nil
|
|
448
|
-
rescue Exception # rubocop:disable Lint/RescueException -- 软关闭的原始异常在硬关闭兜底后继续传播。
|
|
449
|
-
failed = true
|
|
450
|
-
raise
|
|
451
|
-
ensure
|
|
452
|
-
cleanup(failed:) { hard_close }
|
|
470
|
+
def initialize_session(reader, **)
|
|
471
|
+
@session = Session.allocate
|
|
472
|
+
@session.initialize_connection(self, reader, **)
|
|
453
473
|
end
|
|
454
474
|
|
|
455
|
-
private
|
|
456
|
-
|
|
457
|
-
# 账本发布前只按局部所有权清理;发布后沿用完整关闭流程,避免两套生命周期状态。
|
|
458
475
|
def cleanup_session(reader, writer:, own:, slave: nil, graceful: false)
|
|
459
|
-
if @
|
|
460
|
-
|
|
476
|
+
if @session
|
|
477
|
+
@session.cleanup_session(reader, writer:, own:, slave:, graceful:)
|
|
461
478
|
elsif own
|
|
462
479
|
SessionResources.close_handles(reader, writer, slave)
|
|
463
480
|
end
|
|
464
481
|
end
|
|
465
|
-
|
|
466
|
-
# 仅在本次生命周期已有异常时抑制常规清理错误;调用者 rescue 中的旧异常不算本次失败。
|
|
467
|
-
def cleanup(failed:)
|
|
468
|
-
yield
|
|
469
|
-
rescue IOError, SystemCallError
|
|
470
|
-
raise unless failed
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
# 共用的进程关闭流程;force 控制是否允许 KILL,只有资源创建者能够操作直属子进程。
|
|
474
|
-
def close_resources(timeout:, term_timeout:, force:)
|
|
475
|
-
failure = nil
|
|
476
|
-
# 预期的清理错误延后传播,保证其余所属资源和直属子进程仍能完成清理。
|
|
477
|
-
cleanup = lambda do |&step|
|
|
478
|
-
step.call
|
|
479
|
-
rescue IOError, SystemCallError => error
|
|
480
|
-
failure ||= error
|
|
481
|
-
nil
|
|
482
|
-
end
|
|
483
|
-
# IO 关闭与进程退出独立记录:软关闭可能已经 closed?,但仍保留活跃 PID。
|
|
484
|
-
cleanup.call { @resources.close_handles }
|
|
485
|
-
@closed = true
|
|
486
|
-
@interact_inputs&.delete_if do |_io, input|
|
|
487
|
-
cleanup.call do
|
|
488
|
-
input.close(graceful: false)
|
|
489
|
-
true
|
|
490
|
-
end
|
|
491
|
-
end
|
|
492
|
-
@interact_output = nil
|
|
493
|
-
@relay_outputs&.clear
|
|
494
|
-
@relay_history&.clear
|
|
495
|
-
@relay_callback = nil
|
|
496
|
-
status = close_child(timeout: timeout, term_timeout: term_timeout, force: force)
|
|
497
|
-
completed = true
|
|
498
|
-
status
|
|
499
|
-
ensure
|
|
500
|
-
begin
|
|
501
|
-
cleanup.call { flush_diagnostics }
|
|
502
|
-
ensure
|
|
503
|
-
cleanup.call { self.log_output = nil }
|
|
504
|
-
end
|
|
505
|
-
# 用本次流程的完成状态判断异常传播,不能误把调用者 rescue 中的异常当成当前错误。
|
|
506
|
-
raise failure if failure && completed
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
# 句柄清理失败不改变进程策略;未回收 PID 保留给重复关闭或终结器继续处理。
|
|
510
|
-
def close_child(timeout:, term_timeout:, force:)
|
|
511
|
-
return process_status unless @resources.owner == Process.pid && pid
|
|
512
|
-
|
|
513
|
-
status = wait(timeout: timeout)
|
|
514
|
-
return status if status || !pid
|
|
515
|
-
|
|
516
|
-
status = wait_for_child(term_timeout, signal: "TERM")
|
|
517
|
-
return status if status || !pid
|
|
518
|
-
return unless force
|
|
519
|
-
|
|
520
|
-
wait_for_child(1, signal: "KILL")
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
# 每阶段只计算一次期限;回收或信号被中断后仍沿用剩余预算,零预算也先做一次尝试。
|
|
524
|
-
def wait_for_child(period, signal: nil)
|
|
525
|
-
deadline = period && (Expect.monotonic + period)
|
|
526
|
-
loop do
|
|
527
|
-
status = process_status
|
|
528
|
-
return status if status || !pid || @resources.owner != Process.pid
|
|
529
|
-
|
|
530
|
-
begin
|
|
531
|
-
signal_child(signal) if signal
|
|
532
|
-
signal = nil
|
|
533
|
-
rescue Errno::EINTR
|
|
534
|
-
# 下轮先回收再重试信号,避免在无限等待或持续中断时忙等。
|
|
535
|
-
nil
|
|
536
|
-
end
|
|
537
|
-
# ESRCH 后可能已完成回收;即使预算耗尽,也要返回刚获得的状态。
|
|
538
|
-
return process_status unless pid
|
|
539
|
-
|
|
540
|
-
remaining = deadline && (deadline - Expect.monotonic)
|
|
541
|
-
return nil if remaining && remaining <= 0
|
|
542
|
-
|
|
543
|
-
sleep(remaining ? [0.01, remaining].min : 0.01)
|
|
544
|
-
end
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
# 统一初始化 PTY 与已有 IO 会话,复制配置并注册不直接捕获会话的资源终结器。
|
|
548
|
-
def initialize_session(reader, writer:, slave: nil, own: false, diagnostic_output: nil, **)
|
|
549
|
-
# 先登记所有权,后续校验失败也使用同一个资源对象逐个清理所属 IO。
|
|
550
|
-
@resources = SessionResources.new(reader, writer: writer, slave: slave, own: own)
|
|
551
|
-
raise ArgumentError, "reader must be a real IO" unless reader.is_a?(IO) && !reader.closed?
|
|
552
|
-
raise ArgumentError, "writer must be a real IO" unless writer.is_a?(IO) && !writer.closed?
|
|
553
|
-
|
|
554
|
-
@pty = reader.tty?
|
|
555
|
-
@slave = slave
|
|
556
|
-
@configuration = Configuration.new(**self.class.configuration.to_h, **)
|
|
557
|
-
@buffer = "".b
|
|
558
|
-
@buffer_generation = 0
|
|
559
|
-
@buffer_discarded_bytes = 0
|
|
560
|
-
@listeners = []
|
|
561
|
-
@sequences = {}
|
|
562
|
-
@relay_outputs = []
|
|
563
|
-
@closed = @eof = false
|
|
564
|
-
self.diagnostic_output = diagnostic_output
|
|
565
|
-
ObjectSpace.define_finalizer(self, SessionResources.finalizer(@resources))
|
|
566
|
-
end
|
|
567
|
-
|
|
568
|
-
# 开始新一轮等待时清除旧结果并应用缓冲上限,尚未消费的输入继续保留。
|
|
569
|
-
def reset_result
|
|
570
|
-
@last_result = nil
|
|
571
|
-
trim_buffer
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
# 按字节偏移生成 before/match/after;通常只保留 after,preserve_buffer 开启时不消费。
|
|
575
|
-
def record_match(pattern, position)
|
|
576
|
-
offset, length, captures = position
|
|
577
|
-
@last_result = Result.new(number: pattern.number, before: @buffer.byteslice(0, offset),
|
|
578
|
-
match: @buffer.byteslice(offset, length), after: @buffer.byteslice((offset + length)..),
|
|
579
|
-
session: self, captures: captures)
|
|
580
|
-
unless preserve_buffer?
|
|
581
|
-
@buffer = @last_result.after.dup
|
|
582
|
-
@buffer_generation += 1
|
|
583
|
-
end
|
|
584
|
-
# 诊断回调可能嵌套等待;恢复本次结果后再交给正式模式回调,不能返回内层等待的结果。
|
|
585
|
-
result = @last_result
|
|
586
|
-
trace("matched pattern #{pattern.number}")
|
|
587
|
-
@last_result = result
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
# 记录超时、EOF 或原始 IO 异常,保留当前缓冲快照并清除旧匹配及捕获组。
|
|
591
|
-
def record_error(error)
|
|
592
|
-
@last_result = Result.new(error: error, before: buffer, session: self, captures: [])
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
# 输入结束时将剩余缓冲放入 before 并清空,尝试回收但不终止仍活跃的子进程。
|
|
596
|
-
def record_eof
|
|
597
|
-
process_status
|
|
598
|
-
record_error(:eof)
|
|
599
|
-
clear_buffer
|
|
600
|
-
@last_result
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
# 先将读取字节交给匹配或转接缓冲,再记录日志;日志失败也能恢复输入。
|
|
604
|
-
def read_available(propagate: true, buffer: @buffer, trim: true)
|
|
605
|
-
return nil if eof?
|
|
606
|
-
|
|
607
|
-
# 写入背压也会读取;转接期间统一交给转义处理器,不能直接转发或另存匹配缓冲。
|
|
608
|
-
if @interaction_buffer
|
|
609
|
-
buffer = @interaction_buffer
|
|
610
|
-
propagate = false
|
|
611
|
-
trim = false
|
|
612
|
-
end
|
|
613
|
-
|
|
614
|
-
begin
|
|
615
|
-
data = to_io.read_nonblock(READ_SIZE, exception: false)
|
|
616
|
-
rescue Errno::EIO
|
|
617
|
-
# 某些系统用 PTY 的 EIO 表示对端关闭;普通 IO 的同类错误仍按异常处理。
|
|
618
|
-
raise unless @pty
|
|
619
|
-
|
|
620
|
-
return mark_eof
|
|
621
|
-
rescue EOFError
|
|
622
|
-
return mark_eof
|
|
623
|
-
end
|
|
624
|
-
return nil if data == :wait_readable
|
|
625
|
-
|
|
626
|
-
return mark_eof if data.nil?
|
|
627
|
-
|
|
628
|
-
data = data.b
|
|
629
|
-
buffer << data
|
|
630
|
-
trim_buffer if trim
|
|
631
|
-
trace_data(:received, data, level: 2) if debug_level >= 2
|
|
632
|
-
trace_data(:buffer, @buffer, level: 3) if debug_level >= 3
|
|
633
|
-
# 仅在真实读取时记录日志,后续匹配或人工转接重用缓冲时不会重复记录。
|
|
634
|
-
write_log(data)
|
|
635
|
-
propagate(data) if propagate
|
|
636
|
-
data
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
def mark_eof
|
|
640
|
-
@eof = true
|
|
641
|
-
flush_log
|
|
642
|
-
flush_diagnostics(:received)
|
|
643
|
-
nil
|
|
644
|
-
end
|
|
645
|
-
|
|
646
|
-
# 缓冲超过上限时只保留最新尾部字节,不对编码做隐式修改。
|
|
647
|
-
def trim_buffer
|
|
648
|
-
limit = buffer_limit
|
|
649
|
-
return unless limit && @buffer.bytesize > limit
|
|
650
|
-
|
|
651
|
-
# 只累计匹配窗口裁剪,消费、清空及转接交接不算丢弃;关闭后仍可读取累计值。
|
|
652
|
-
@buffer_discarded_bytes += @buffer.bytesize - limit
|
|
653
|
-
@buffer = @buffer.byteslice(-limit, limit)
|
|
654
|
-
@buffer_generation += 1
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
# 仅由资源创建者向仍未回收的子进程发送信号;若进程刚好退出,则尝试回收。
|
|
658
|
-
def signal_child(signal)
|
|
659
|
-
return unless pid && @resources.owner == Process.pid
|
|
660
|
-
|
|
661
|
-
Process.kill(signal, pid)
|
|
662
|
-
rescue Errno::ESRCH
|
|
663
|
-
@resources.reap
|
|
664
|
-
end
|
|
665
482
|
end
|
|
666
|
-
|
|
667
|
-
require_relative "expect/logging"
|
|
668
|
-
require_relative "expect/terminal"
|
|
669
|
-
require_relative "expect/interaction"
|