expect-pty 0.6.1 → 0.7.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 +29 -3
- data/README.md +102 -119
- data/docs/API.md +76 -51
- data/docs/MIGRATION.md +68 -15
- data/lib/expect/cleanup.rb +4 -3
- data/lib/expect/interaction.rb +57 -56
- data/lib/expect/logging.rb +60 -106
- data/lib/expect/matcher.rb +22 -19
- data/lib/expect/pattern.rb +2 -1
- data/lib/expect/pattern_list.rb +5 -5
- data/lib/expect/redactor.rb +1 -1
- data/lib/expect/relay.rb +20 -19
- data/lib/expect/relay_writer.rb +5 -4
- data/lib/expect/result.rb +1 -1
- data/lib/expect/session.rb +114 -68
- data/lib/expect/session_resources.rb +10 -17
- data/lib/expect/version.rb +2 -2
- data/lib/expect.rb +47 -370
- data/sig/expect.rbs +203 -279
- metadata +4 -20
- data/lib/expect/configuration.rb +0 -143
- data/lib/expect/terminal.rb +0 -102
data/lib/expect/session.rb
CHANGED
|
@@ -1,68 +1,99 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "logging"
|
|
4
|
-
require_relative "terminal"
|
|
5
4
|
require_relative "interaction"
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
module Expect
|
|
7
|
+
# 一个真实 PTY 或 IO 会话;缓冲、最近结果和所属进程都保存在本对象中。
|
|
8
|
+
# SessionResources 保存可独立回收的资源账本;Matcher、Relay 仅在操作期间借用会话状态。
|
|
9
|
+
# 日志与协议目标属于调用方,显式关闭和 GC 均不得接管这些借用对象。
|
|
10
10
|
class Session
|
|
11
11
|
include Logging
|
|
12
|
-
include Terminal
|
|
13
12
|
include Interaction
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
|
|
14
|
+
# 启动命令、最近结果及 PTY 端点的只读状态;文本结果属于不可变快照。
|
|
15
|
+
attr_reader :command, :last_result, :slave, :tty_name, :buffer_discarded_bytes
|
|
16
|
+
# 会话的默认等待期限、写入背压期限及缓冲上限。
|
|
17
|
+
attr_reader :timeout, :write_timeout, :buffer_limit
|
|
18
|
+
|
|
19
|
+
# 预建 PTY;命令、env、chdir 与 raw 模式由 spawn 显式设置。
|
|
20
|
+
def initialize(timeout: nil, write_timeout: nil, buffer_limit: nil, logger: nil, transcript: nil, outputs: [])
|
|
21
|
+
master = slave = nil
|
|
22
|
+
Cleanup.on_failure(-> { cleanup_session(master, writer: master, slave:, own: true) }) do
|
|
23
|
+
master, slave = PTY.open
|
|
24
|
+
initialize_io(master, writer: master, slave:, own: true, timeout:, write_timeout:, buffer_limit:,
|
|
25
|
+
logger:, transcript:, outputs:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
# 设置本会话后续 expect 默认使用的相对秒数,nil 表示无限等待。
|
|
30
|
+
def timeout=(value)
|
|
31
|
+
@timeout = Expect.duration(value)
|
|
32
|
+
end
|
|
21
33
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
# 设置写入遇到背压或 EINTR 时的等待秒数;成功短写不受总耗时限制。
|
|
35
|
+
def write_timeout=(value)
|
|
36
|
+
@write_timeout = Expect.duration(value)
|
|
37
|
+
end
|
|
25
38
|
|
|
26
|
-
#
|
|
39
|
+
# 校验并更新缓冲上限,立即裁剪现有尾部;非法值不改变配置和内容。
|
|
27
40
|
def buffer_limit=(value)
|
|
28
|
-
|
|
41
|
+
unless value.nil? || (value.is_a?(Integer) && value.positive?)
|
|
42
|
+
raise ArgumentError, "buffer_limit must be a positive Integer or nil"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@buffer_limit = value
|
|
29
46
|
trim_buffer
|
|
30
47
|
end
|
|
31
48
|
|
|
49
|
+
# 等待自身输入,返回 Result;匹配消费和接收重置策略只对本轮有效。
|
|
50
|
+
def expect(*patterns, timeout: self.timeout, deadline: nil, consume: true, reset_timeout_on_read: false, &)
|
|
51
|
+
Expect.__send__(:run_expect, [self], patterns, timeout, deadline:, consume:, reset_timeout_on_read:, &)
|
|
52
|
+
end
|
|
53
|
+
|
|
32
54
|
# 在新控制终端中执行命令并同步确认 exec 结果;同一会话只能启动一次。
|
|
33
|
-
def spawn(*command, env: {}, chdir: nil)
|
|
55
|
+
def spawn(*command, env: {}, chdir: nil, raw: false)
|
|
34
56
|
validate_spawn!(command)
|
|
35
57
|
|
|
36
|
-
@slave.raw! if
|
|
58
|
+
@slave.raw! if raw
|
|
37
59
|
from_child = to_parent = nil
|
|
38
60
|
Cleanup.always(-> { SessionResources.close_handles(from_child, to_parent) }) do
|
|
39
61
|
# 错误管道的写端在 exec 成功时自动关闭;父进程据此区分成功启动与 exec 前失败。
|
|
40
62
|
from_child, to_parent = IO.pipe
|
|
41
63
|
to_parent.close_on_exec = true
|
|
42
64
|
@command = command.map { |part| part.dup.freeze }.freeze
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
# 原生 fork 返回后先登记 PID,再交付线程中断或终止,避免清理时遗漏新子进程。
|
|
66
|
+
child = Thread.handle_interrupt(Object => :never) do
|
|
67
|
+
@resources.pid = fork do
|
|
68
|
+
# 子进程不继承父侧登记临界区,执行前仍可立即响应异步异常。
|
|
69
|
+
Thread.handle_interrupt(Object => :immediate) { exec_child(command, env:, chdir:, from_child:, to_parent:) }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
45
72
|
to_parent.close
|
|
46
73
|
@slave.close
|
|
47
74
|
failure = from_child.read
|
|
48
75
|
Cleanup.always(-> { hard_close }) { raise SpawnError, failure } unless failure.empty?
|
|
49
76
|
trace("spawned pid=#{child}", event: :spawned)
|
|
50
|
-
|
|
77
|
+
self
|
|
51
78
|
end
|
|
52
79
|
end
|
|
53
80
|
|
|
54
81
|
# 暴露底层读写 IO 与终端属性,供 select、终端设置及 IO 适配使用。
|
|
55
82
|
def to_io = @resources.reader
|
|
56
83
|
|
|
84
|
+
# 实际写入端点,可能不同于 to_io 返回的读端。
|
|
57
85
|
def writer = @resources.writer
|
|
58
86
|
|
|
87
|
+
# 当前读端描述符;会话关闭后为 nil。
|
|
59
88
|
def fileno = closed? ? nil : to_io.fileno
|
|
60
89
|
|
|
90
|
+
# 读端仍打开且属于终端时为 true。
|
|
61
91
|
def tty? = !closed? && to_io.tty?
|
|
62
92
|
|
|
63
93
|
# 诊断时仅显示进程和描述符状态,避免默认对象展开泄露缓冲或日志内容。
|
|
64
|
-
def inspect = "#<#{
|
|
94
|
+
def inspect = "#<#{self.class} pid=#{pid.inspect} fd=#{fileno.inspect} closed=#{closed?}>"
|
|
65
95
|
|
|
96
|
+
# 尚未完成回收的直属子进程 PID;已有 IO 会话为 nil。
|
|
66
97
|
def pid = @resources.pid
|
|
67
98
|
|
|
68
99
|
# 非阻塞回收并缓存子进程状态;未退出或仅适配 IO 时返回 nil。
|
|
@@ -73,6 +104,7 @@ class Expect
|
|
|
73
104
|
@resources.status
|
|
74
105
|
end
|
|
75
106
|
|
|
107
|
+
# 已知进程退出码;尚未回收或信号退出时为 nil。
|
|
76
108
|
def exit_code = process_status&.exitstatus
|
|
77
109
|
|
|
78
110
|
# 先刷新回收状态,再判断是否仍有未回收的子进程;不以 IO 是否关闭代替进程状态。
|
|
@@ -84,19 +116,25 @@ class Expect
|
|
|
84
116
|
# 区分会话关闭和输入结束,已关闭会话也不能继续读取。
|
|
85
117
|
def closed? = @closed || to_io.closed?
|
|
86
118
|
|
|
119
|
+
# 输入已结束或会话已关闭时为 true。
|
|
87
120
|
def eof? = @eof || closed?
|
|
88
121
|
|
|
89
122
|
# 以下访问器读取最近一次等待结果;未发生匹配时捕获组返回空数组。
|
|
90
123
|
def before = @last_result&.before
|
|
91
124
|
|
|
125
|
+
# 最近一次匹配之后的不可变字节快照。
|
|
92
126
|
def after = @last_result&.after
|
|
93
127
|
|
|
128
|
+
# 最近一次匹配命中的不可变字节快照。
|
|
94
129
|
def match = @last_result&.match
|
|
95
130
|
|
|
131
|
+
# 最近命中的模式序号;EOF、超时或尚无结果时为 nil。
|
|
96
132
|
def match_number = @last_result&.number
|
|
97
133
|
|
|
134
|
+
# 最近捕获值的不可变数组;未参与的捕获为 nil。
|
|
98
135
|
def captures = @last_result&.captures || []
|
|
99
136
|
|
|
137
|
+
# 最近的 EOF、超时事件或原始 IO 异常。
|
|
100
138
|
def error = @last_result&.error
|
|
101
139
|
|
|
102
140
|
# 返回缓冲副本,防止调用方原地修改绕过裁剪规则。
|
|
@@ -125,7 +163,7 @@ class Expect
|
|
|
125
163
|
|
|
126
164
|
begin
|
|
127
165
|
data = objects.map { |object| object.to_s.b }.join
|
|
128
|
-
trace_data(:sending, data
|
|
166
|
+
trace_data(:sending, data)
|
|
129
167
|
rescue WriteTimeout
|
|
130
168
|
# 转换或诊断中的嵌套写入不属于当前命令;此时尚未向 writer 发送任何字节。
|
|
131
169
|
raise WriteTimeout.new("write interrupted before sending data", bytes_written: 0)
|
|
@@ -150,7 +188,7 @@ class Expect
|
|
|
150
188
|
# 链式写入单个对象,返回当前会话。
|
|
151
189
|
def <<(object)
|
|
152
190
|
write(object)
|
|
153
|
-
|
|
191
|
+
self
|
|
154
192
|
end
|
|
155
193
|
|
|
156
194
|
# 委托 StringIO 处理换行、nil 和递归数组,再统一写入;返回 nil,与 Ruby puts 一致。
|
|
@@ -209,7 +247,7 @@ class Expect
|
|
|
209
247
|
end
|
|
210
248
|
|
|
211
249
|
# 通用生命周期清理:可先软关闭,ensure 中硬关闭兜底;正常完成返回 nil。
|
|
212
|
-
def close(graceful:
|
|
250
|
+
def close(graceful: false)
|
|
213
251
|
Cleanup.always(-> { hard_close }) do
|
|
214
252
|
soft_close if graceful
|
|
215
253
|
nil
|
|
@@ -217,6 +255,7 @@ class Expect
|
|
|
217
255
|
end
|
|
218
256
|
|
|
219
257
|
# 账本发布前只按局部所有权清理;发布后沿用完整关闭流程,避免两套生命周期状态。
|
|
258
|
+
# @api private
|
|
220
259
|
def cleanup_session(reader, writer:, own:, slave: nil, graceful: false)
|
|
221
260
|
if @resources
|
|
222
261
|
close(graceful:)
|
|
@@ -225,50 +264,21 @@ class Expect
|
|
|
225
264
|
end
|
|
226
265
|
end
|
|
227
266
|
|
|
228
|
-
# 统一初始化 PTY 与已有 IO 会话,复制配置并注册不直接捕获会话的资源终结器。
|
|
229
|
-
def initialize_connection(connection, reader, writer:, slave: nil, own: false, diagnostic_output: nil, **)
|
|
230
|
-
# 先登记所有权,后续校验失败也使用同一个资源对象逐个清理所属 IO。
|
|
231
|
-
@connection = connection
|
|
232
|
-
@resources = SessionResources.new(reader, writer:, slave:, own:)
|
|
233
|
-
raise ArgumentError, "reader must be a real IO" unless reader.is_a?(IO) && !reader.closed?
|
|
234
|
-
raise ArgumentError, "writer must be a real IO" unless writer.is_a?(IO) && !writer.closed?
|
|
235
|
-
|
|
236
|
-
@pty = reader.tty?
|
|
237
|
-
@slave = slave
|
|
238
|
-
@tty_name = slave.path if slave
|
|
239
|
-
@configuration = Configuration.new(**connection.class.configuration.to_h, **)
|
|
240
|
-
@buffer = "".b
|
|
241
|
-
@buffer_generation = 0
|
|
242
|
-
@buffer_discarded_bytes = 0
|
|
243
|
-
@listeners = []
|
|
244
|
-
@sequences = {}
|
|
245
|
-
@relay_outputs = []
|
|
246
|
-
@interact_inputs = {}.compare_by_identity
|
|
247
|
-
@interact_output = nil
|
|
248
|
-
@interaction_buffer = @relay_owner = @relay_callback = nil
|
|
249
|
-
@relay_history = "".b
|
|
250
|
-
@relay_history_sequences = {}
|
|
251
|
-
@secrets = @log_redactor = nil
|
|
252
|
-
@diagnostic_redactors = {}
|
|
253
|
-
@last_result = @command = nil
|
|
254
|
-
@closed = @eof = false
|
|
255
|
-
self.diagnostic_output = diagnostic_output
|
|
256
|
-
ObjectSpace.define_finalizer(self, SessionResources.finalizer(@resources))
|
|
257
|
-
end
|
|
258
|
-
|
|
259
267
|
# 开始新一轮等待时清除旧结果并应用缓冲上限,尚未消费的输入继续保留。
|
|
268
|
+
# @api private
|
|
260
269
|
def reset_result
|
|
261
270
|
@last_result = nil
|
|
262
271
|
trim_buffer
|
|
263
272
|
end
|
|
264
273
|
|
|
265
|
-
# 按字节偏移生成 before/match/after;通常只保留 after,
|
|
266
|
-
|
|
274
|
+
# 按字节偏移生成 before/match/after;通常只保留 after,consume 为 false 时不消费。
|
|
275
|
+
# @api private
|
|
276
|
+
def record_match(pattern, position, consume: true)
|
|
267
277
|
offset, length, captures = position
|
|
268
278
|
@last_result = Result.new(number: pattern.number, before: @buffer.byteslice(0, offset),
|
|
269
279
|
match: @buffer.byteslice(offset, length), after: @buffer.byteslice((offset + length)..),
|
|
270
|
-
session:
|
|
271
|
-
|
|
280
|
+
session: self, captures:)
|
|
281
|
+
if consume
|
|
272
282
|
@buffer = @last_result.after.dup
|
|
273
283
|
@buffer_generation += 1
|
|
274
284
|
end
|
|
@@ -279,11 +289,13 @@ class Expect
|
|
|
279
289
|
end
|
|
280
290
|
|
|
281
291
|
# 记录超时、EOF 或原始 IO 异常,保留当前缓冲快照并清除旧匹配及捕获组。
|
|
292
|
+
# @api private
|
|
282
293
|
def record_error(error)
|
|
283
|
-
@last_result = Result.new(error:, before: buffer, session:
|
|
294
|
+
@last_result = Result.new(error:, before: buffer, session: self, captures: [])
|
|
284
295
|
end
|
|
285
296
|
|
|
286
297
|
# 输入结束时将剩余缓冲放入 before 并清空,尝试回收但不终止仍活跃的子进程。
|
|
298
|
+
# @api private
|
|
287
299
|
def record_eof
|
|
288
300
|
process_status
|
|
289
301
|
record_error(:eof)
|
|
@@ -292,6 +304,7 @@ class Expect
|
|
|
292
304
|
end
|
|
293
305
|
|
|
294
306
|
# 先将读取字节交给匹配或转接缓冲,再记录日志;日志失败也能恢复输入。
|
|
307
|
+
# @api private
|
|
295
308
|
def read_available(propagate: true, buffer: @buffer, trim: true)
|
|
296
309
|
return nil if eof?
|
|
297
310
|
|
|
@@ -319,16 +332,50 @@ class Expect
|
|
|
319
332
|
data = data.b
|
|
320
333
|
buffer << data
|
|
321
334
|
trim_buffer if trim
|
|
322
|
-
trace_data(:received, data
|
|
323
|
-
trace_data(:buffer, @buffer, level: 3) if debug_level >= 3
|
|
335
|
+
trace_data(:received, data)
|
|
324
336
|
# 仅在真实读取时记录日志,后续匹配或人工转接重用缓冲时不会重复记录。
|
|
325
|
-
|
|
337
|
+
write_transcript(data)
|
|
326
338
|
propagate(data) if propagate
|
|
327
339
|
data
|
|
328
340
|
end
|
|
329
341
|
|
|
330
342
|
private
|
|
331
343
|
|
|
344
|
+
# 初始化前先登记所有权;参数校验失败也能释放已取得的端点。
|
|
345
|
+
def initialize_io(reader, writer:, slave: nil, own: false, timeout: nil, write_timeout: nil,
|
|
346
|
+
buffer_limit: nil, logger: nil, transcript: nil, outputs: [])
|
|
347
|
+
@resources = SessionResources.new(reader, writer:, slave:, own:)
|
|
348
|
+
raise ArgumentError, "reader must be a real IO" unless reader.is_a?(IO) && !reader.closed?
|
|
349
|
+
raise ArgumentError, "writer must be a real IO" unless writer.is_a?(IO) && !writer.closed?
|
|
350
|
+
|
|
351
|
+
@pty = reader.tty?
|
|
352
|
+
@slave = slave
|
|
353
|
+
@tty_name = slave.path if slave
|
|
354
|
+
@buffer = "".b
|
|
355
|
+
@buffer_generation = 0
|
|
356
|
+
@buffer_discarded_bytes = 0
|
|
357
|
+
@outputs = []
|
|
358
|
+
@sequences = {}
|
|
359
|
+
@pending_writes = []
|
|
360
|
+
@interact_inputs = {}.compare_by_identity
|
|
361
|
+
@interact_output = nil
|
|
362
|
+
@interaction_buffer = @relay_owner = @relay_callback = nil
|
|
363
|
+
@relay_history = "".b
|
|
364
|
+
@relay_history_sequences = {}
|
|
365
|
+
@secrets = @transcript_redactor = nil
|
|
366
|
+
@diagnostic_redactors = {}
|
|
367
|
+
@logger = @transcript = nil
|
|
368
|
+
@last_result = @command = nil
|
|
369
|
+
@closed = @eof = false
|
|
370
|
+
self.timeout = timeout
|
|
371
|
+
self.write_timeout = write_timeout
|
|
372
|
+
self.buffer_limit = buffer_limit
|
|
373
|
+
self.logger = logger
|
|
374
|
+
self.transcript = transcript
|
|
375
|
+
self.outputs = outputs
|
|
376
|
+
ObjectSpace.define_finalizer(self, @resources.method(:finalize))
|
|
377
|
+
end
|
|
378
|
+
|
|
332
379
|
# 参数校验先于任何进程和终端修改。
|
|
333
380
|
def validate_spawn!(command)
|
|
334
381
|
raise SpawnError, "cannot reuse a spawned session" if @command
|
|
@@ -422,7 +469,7 @@ class Expect
|
|
|
422
469
|
end
|
|
423
470
|
end
|
|
424
471
|
@interact_output = nil
|
|
425
|
-
@
|
|
472
|
+
@pending_writes&.clear
|
|
426
473
|
@relay_history&.clear
|
|
427
474
|
@relay_callback = nil
|
|
428
475
|
status = close_child(timeout:, term_timeout:, force:)
|
|
@@ -432,7 +479,7 @@ class Expect
|
|
|
432
479
|
begin
|
|
433
480
|
cleanup.call { flush_diagnostics }
|
|
434
481
|
ensure
|
|
435
|
-
cleanup.call { self.
|
|
482
|
+
cleanup.call { self.transcript = nil }
|
|
436
483
|
end
|
|
437
484
|
# 用本次流程的完成状态判断异常传播,不能误把调用者 rescue 中的异常当成当前错误。
|
|
438
485
|
raise failure if failure && completed
|
|
@@ -476,9 +523,10 @@ class Expect
|
|
|
476
523
|
end
|
|
477
524
|
end
|
|
478
525
|
|
|
526
|
+
# 只结束读取方向并冲刷接收记录;EOF 不代表子进程已经退出或写端已经关闭。
|
|
479
527
|
def mark_eof
|
|
480
528
|
@eof = true
|
|
481
|
-
|
|
529
|
+
flush_transcript
|
|
482
530
|
flush_diagnostics(:received)
|
|
483
531
|
nil
|
|
484
532
|
end
|
|
@@ -503,6 +551,4 @@ class Expect
|
|
|
503
551
|
@resources.reap
|
|
504
552
|
end
|
|
505
553
|
end
|
|
506
|
-
|
|
507
|
-
private_constant :Session
|
|
508
554
|
end
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
#
|
|
3
|
+
module Expect
|
|
4
|
+
# 独立保存句柄和 PID 所有权,让终结器无需直接捕获会话即可清理遗弃资源。
|
|
5
5
|
# IO 是否关闭与子进程是否退出分别记录;不能仅凭句柄状态清空 PID 或伪造退出状态。
|
|
6
6
|
# @api private
|
|
7
7
|
class SessionResources
|
|
8
|
-
#
|
|
9
|
-
attr_accessor :status
|
|
8
|
+
# 缓存已取得的状态;被外部回收的进程仍保留未知状态,不伪造成功。
|
|
9
|
+
attr_accessor :status
|
|
10
|
+
# own 控制句柄关闭责任,owner 控制进程回收责任;fork 后两者不能混为一谈。
|
|
10
11
|
attr_reader :pid, :reader, :writer, :slave, :owner, :own
|
|
11
12
|
|
|
12
13
|
# 记录创建资源的进程;fork 后的副本不能向父进程拥有的子进程发信号。
|
|
@@ -59,29 +60,21 @@ class Expect
|
|
|
59
60
|
status
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
# GC
|
|
63
|
-
|
|
63
|
+
# GC 兜底关闭所属句柄并终止尚存活的子进程;不执行用户代码或软关闭等待。
|
|
64
|
+
# 可直接用绑定到资源对象的 Method 注册终结器,忽略 Ruby 传来的被回收对象 ID。
|
|
65
|
+
def finalize(_object_id = nil)
|
|
64
66
|
return unless owner == Process.pid
|
|
65
67
|
|
|
66
68
|
begin
|
|
67
69
|
close_handles
|
|
68
70
|
ensure
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
ensure
|
|
72
|
-
# 每个阶段独立收尾;句柄或日志关闭失败不能跳过进程回收。
|
|
73
|
-
finalize_child
|
|
74
|
-
end
|
|
71
|
+
# 句柄关闭失败不能跳过进程回收。
|
|
72
|
+
finalize_child
|
|
75
73
|
end
|
|
76
74
|
rescue IOError, SystemCallError
|
|
77
75
|
nil
|
|
78
76
|
end
|
|
79
77
|
|
|
80
|
-
# 构造只持有资源对象的终结回调,避免闭包中的 self 绑定到会话而妨碍回收。
|
|
81
|
-
def self.finalizer(resources)
|
|
82
|
-
proc { resources.finalize }
|
|
83
|
-
end
|
|
84
|
-
|
|
85
78
|
private
|
|
86
79
|
|
|
87
80
|
# GC 只做一次非阻塞回收和最多两次信号尝试;失败也把等待交给 detach,不运行用户回调。
|
data/lib/expect/version.rb
CHANGED