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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -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 -209
  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 +97 -73
  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 +10 -2
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +333 -520
  22. data/sig/expect.rbs +355 -0
  23. metadata +13 -106
  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 -154
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -456
  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 -319
  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/initialization_failure_test.rb +0 -92
  62. data/test/integration/README.md +0 -109
  63. data/test/integration/ssh_scripts.rb +0 -94
  64. data/test/interact_test.rb +0 -253
  65. data/test/interconnect_test.rb +0 -425
  66. data/test/io_test.rb +0 -321
  67. data/test/kibitz_test.rb +0 -45
  68. data/test/lifecycle_contract_test.rb +0 -71
  69. data/test/literal_scan_test.rb +0 -97
  70. data/test/matching_test.rb +0 -211
  71. data/test/multi_session_test.rb +0 -66
  72. data/test/ownership_sequence_test.rb +0 -208
  73. data/test/pattern_offset_test.rb +0 -43
  74. data/test/process_interruption_test.rb +0 -296
  75. data/test/process_test.rb +0 -291
  76. data/test/redactor_test.rb +0 -183
  77. data/test/relay_recovery_test.rb +0 -451
  78. data/test/relay_reentrancy_test.rb +0 -159
  79. data/test/release_test.rb +0 -375
  80. data/test/ruby_api_test.rb +0 -515
  81. data/test/scan_reuse_test.rb +0 -109
  82. data/test/script_logging_test.rb +0 -124
  83. data/test/support/interact_probe.rb +0 -125
  84. data/test/support/kibitz_probe.rb +0 -177
  85. data/test/support/script_probe.rb +0 -158
  86. data/test/terminal_cleanup_test.rb +0 -349
  87. data/test/test_helper.rb +0 -58
  88. data/test/timeout_test.rb +0 -289
  89. 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
- session = new(**)
64
- session.spawn(*command, env: env, chdir: chdir)
65
- spawned = true
66
- return session unless block_given?
67
-
68
- yield session
69
- rescue Exception # rubocop:disable Lint/RescueException -- 记录本次作用域的失败,清理后原样传播,包括非 StandardError 异常。
70
- failed = true
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
- session = allocate
83
- session.__send__(:initialize_session, io, writer: writer, own: own, **)
84
- initialized = true
85
- return session unless block_given?
86
-
87
- yield session
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
- # 进行多会话匹配,返回命中的模式序号,超时、EOF 或读取错误返回 nil。
101
- 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
102
123
 
103
124
  # 多会话等待的完整结果入口;from: 提供默认来源,块内可分别指定每个模式的来源。
104
- def expect_result(*patterns, from: [], timeout: configuration.timeout, deadline: nil, &)
105
- 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:, &)
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.select { |session| ready.first.include?(session.to_io) }
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.validate!, timeout, deadline: deadline).run
202
+ Matcher.new(pattern_list, timeout, deadline:).run
165
203
  end
166
204
  end
167
205
 
168
206
  extend Forwardable
169
207
 
170
- # 普通属性委托给会话独立配置;缓冲上限的 setter 还需立即裁剪现有缓冲。
171
- def_delegators :@configuration, *Configuration::ATTRIBUTES, *Configuration::PREDICATES
172
- def_delegators :@configuration, *(Configuration::ATTRIBUTES - [:buffer_limit]).map { |name| :"#{name}=" }
173
-
174
- attr_reader :command, :last_result, :slave, :tty_name, :buffer_discarded_bytes
175
-
176
- # 校验并更新缓冲上限后,立即裁剪已接收的内容;校验失败不改变旧缓冲。
177
- def buffer_limit=(value)
178
- @configuration.buffer_limit = value
179
- trim_buffer
180
- value
181
- end
182
-
183
- # 创建 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] 已创建的用户会话。
184
443
  def initialize(*command, env: {}, chdir: nil, **)
185
- master, slave = PTY.open
186
- initialize_session(master, writer: master, slave: slave, own: true, **)
187
- @tty_name = slave.path
188
- spawn(*command, env: env, chdir: chdir) unless command.empty?
189
- initialized = true
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
- # 轮询回收状态直到进程退出或期限到达;返回 Process::Status 或 nil,超时不丢弃 PID。
414
- def wait(timeout: nil)
415
- wait_for_child(Expect.duration(timeout))
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
- # 先在自然退出期限内收集尾部输出,再关闭句柄并最多发送 TERM;不会发送 KILL。
419
- # 尚未退出时返回 nil 并保留 PID,调用方可以继续等待或随后硬关闭。
420
- def soft_close(timeout: 15, term_timeout: 1)
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
- deadline = period && (Expect.monotonic + period)
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
- close_resources(timeout: period, term_timeout: period, force: true)
442
- end
468
+ attr_reader :session
443
469
 
444
- # 通用生命周期清理:可先软关闭,ensure 中硬关闭兜底;正常完成返回 nil。
445
- def close(graceful: graceful_close?)
446
- soft_close if graceful
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 @resources
460
- close(graceful: graceful)
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"