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