Class: Expect
- Inherits:
-
Object
- Object
- Expect
- Extended by:
- Forwardable
- Defined in:
- lib/expect.rb,
lib/expect/relay.rb,
lib/expect/result.rb,
lib/expect/logging.rb,
lib/expect/matcher.rb,
lib/expect/pattern.rb,
lib/expect/version.rb,
lib/expect/redactor.rb,
lib/expect/terminal.rb,
lib/expect/interaction.rb,
lib/expect/pattern_list.rb,
lib/expect/relay_writer.rb,
lib/expect/configuration.rb,
lib/expect/session_resources.rb
Overview
为会话补充人工接管和多路 IO 转接;核心会话定义位于 lib/expect.rb。
Defined Under Namespace
Classes: Configuration, Matcher, Pattern, PatternList, Redactor, ReentrancyError, Result, SessionResources, SpawnError, WriteTimeout
Constant Summary collapse
- CONTINUE =
回调控制符:分别表示重置期限后继续,或保留原期限继续。
:continue- CONTINUE_WITHOUT_RESET =
:continue_without_reset- READ_SIZE =
16_384- VERSION =
Gem 与库共用的版本号;独立文件使 gemspec 无需加载完整会话实现。
"0.5.3"
Instance Attribute Summary collapse
-
#buffer_discarded_bytes ⇒ Object
readonly
Returns the value of attribute buffer_discarded_bytes.
-
#command ⇒ Object
readonly
Returns the value of attribute command.
-
#diagnostic_output ⇒ Object
读取当前日志目标,可能为库打开的文件、借用的 IO、回调或 nil。.
-
#last_result ⇒ Object
readonly
Returns the value of attribute last_result.
-
#log_output ⇒ Object
读取当前日志目标,可能为库打开的文件、借用的 IO、回调或 nil。.
-
#slave ⇒ Object
readonly
Returns the value of attribute slave.
-
#tty_name ⇒ Object
readonly
Returns the value of attribute tty_name.
Class Method Summary collapse
-
.configuration ⇒ Object
读取冻结的默认配置;子类未单独配置时继承父类快照。.
-
.configure ⇒ Object
基于旧快照构造可修改副本,全部赋值与配置块成功后才发布,异常时保留原配置。.
-
.continue(reset_timeout: true) ⇒ Object
返回继续等待的控制符,reset_timeout 决定是否重新计算匹配期限。.
-
.duration(value) ⇒ Object
将秒数转换为有限的非负数,nil 表示无限;供配置和单次操作共用校验。.
-
.expect ⇒ Object
进行多会话匹配,返回命中的模式序号,超时、EOF 或读取错误返回 nil。.
-
.expect_result(*patterns, from: [], timeout: configuration.timeout, deadline: nil) ⇒ Object
多会话等待的完整结果入口;from: 提供默认来源,块内可分别指定每个模式的来源。.
-
.interconnect(*sessions, timeout: nil) ⇒ Object
按各会话 listeners 建立转发图,处理转义、EOF 和总期限,返回引发停止的会话或 nil。.
-
.monotonic ⇒ Object
读取不受系统时间调整影响的单调时钟,所有相对超时共用此计时基准。.
-
.open(io, writer: io, own: false) ⇒ Object
适配已有 IO;own: true 接管关闭责任,初始化失败也释放接管的读写端。.
-
.readable_sessions(*sessions, timeout: 0) ⇒ Object
等待并返回可读会话,不消费输入;去重并忽略已关闭会话,默认非阻塞。.
-
.spawn(*command, env: {}, chdir: nil) ⇒ Object
创建并启动会话;有块时返回块结果并确保关闭,无块时由调用方负责生命周期。.
Instance Method Summary collapse
-
#<<(object) ⇒ Object
链式写入单个对象,返回当前会话。.
- #after ⇒ Object
-
#alive? ⇒ Boolean
先刷新回收状态,再判断是否仍有未回收的子进程;不以 IO 是否关闭代替进程状态。.
-
#before ⇒ Object
以下访问器读取最近一次等待结果;未发生匹配时捕获组返回空数组。.
-
#buffer ⇒ Object
返回缓冲副本,防止调用方原地修改绕过裁剪规则。.
-
#buffer=(value) ⇒ Object
复制并替换原始字节缓冲,应用当前上限;调用方后续修改原字符串不会影响会话。.
-
#buffer_limit=(value) ⇒ Object
校验并更新缓冲上限后,立即裁剪已接收的内容;校验失败不改变旧缓冲。.
- #captures ⇒ Object
-
#clear_buffer ⇒ Object
移交旧缓冲并换上新的空字节串,供显式清空或人工转接接管数据。.
-
#close(graceful: graceful_close?) ) ⇒ Object
通用生命周期清理:可先软关闭,ensure 中硬关闭兜底;正常完成返回 nil。.
-
#closed? ⇒ Boolean
区分会话关闭和输入结束,已关闭会话也不能继续读取。.
-
#continue(reset_timeout: true) ⇒ Object
供实例回调返回继续控制符,语义与 Expect.continue 相同。.
- #eof? ⇒ Boolean
- #error ⇒ Object
- #exit_code ⇒ Object
-
#expect ⇒ Object
在当前会话等待文本或事件,返回模式序号或 nil。.
-
#expect_result(*patterns, timeout: self.timeout, deadline: nil) ⇒ Object
使用会话默认超时构造一次等待,返回含匹配内容、来源和错误的 Result。.
- #fileno ⇒ Object
-
#hard_close(timeout: 0.2) ⇒ Object
立即关闭句柄,再分阶段等待、TERM、KILL;不收集剩余输出,返回已回收状态或 nil。.
-
#initialize(*command, env: {}, chdir: nil) ⇒ Expect
constructor
创建 PTY,可立即启动命令,也可先让调用方配置 slave;构造失败时释放全部新句柄。.
-
#inspect ⇒ Object
诊断时仅显示进程和描述符状态,避免默认对象展开泄露缓冲或日志内容。.
-
#interact(input: $stdin, escape: nil, output: nil, timeout: nil) ⇒ Object
临时将输入、会话和输出相连,实现人工接管;结束时恢复双方监听器、日志开关和转义设置。.
-
#listeners ⇒ Object
返回监听器列表副本,避免外部原地修改转发关系。.
-
#listeners=(outputs) ⇒ Object
校验所有监听器均可写后一次性替换列表,外部数组后续修改不会影响会话。.
-
#log_to(target = nil, mode: "a", &block) ⇒ Object
打开追加/覆盖日志文件,或注册接收字节的日志块;同一次只能指定一种目标。.
- #match ⇒ Object
- #match_number ⇒ Object
-
#on_sequence(sequence, &block) ⇒ Object
注册字面、正则转义或 :eof 事件;回调用闭包保存上下文,nil/false 停止,其余值继续。.
-
#pending_output? ⇒ Boolean
超时或异常后仍有未交付的数据;重新 interconnect 同一源会话可继续发送。.
- #pid ⇒ Object
-
#process_status ⇒ Object
非阻塞回收并缓存子进程状态;未退出或仅适配 IO 时返回 nil。.
-
#puts(*objects) ⇒ Object
委托 StringIO 处理换行、nil 和递归数组,再统一写入;返回 nil,与 Ruby puts 一致。.
-
#redact(*secrets) ⇒ Object
秘密仅作用于本会话的日志和诊断,不改写匹配、stdout 显示或 listeners 的协议字节。 先校验全部值再发布;注册是追加操作,应在首次通信前完成,不能追溯已交付的日志。.
-
#send_slow(*objects, delay:) ⇒ Object
逐字符延迟发送,同时收集回复,适配输入处理较慢的交互程序;返回写入字节数。.
-
#soft_close(timeout: 15, term_timeout: 1) ⇒ Object
先在自然退出期限内收集尾部输出,再关闭句柄并最多发送 TERM;不会发送 KILL。 尚未退出时返回 nil 并保留 PID,调用方可以继续等待或随后硬关闭。.
-
#spawn(*command, env: {}, chdir: nil) ⇒ Object
在新控制终端中执行命令并同步确认 exec 结果;同一会话只能启动一次。 rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- PTY 子进程启动与失败回传共用一次生命周期。.
-
#stty(*modes) ⇒ Object
查询可恢复的终端模式字符串,或通过系统 stty 设置模式;参数按数组传递,不经 shell。 辅助进程与管道独立记账,不能覆盖主会话 PID;失败时保留原异常并有界回收。.
-
#to_io ⇒ Object
暴露底层读写 IO 与终端属性,供 select、终端设置及 IO 适配使用。.
- #tty? ⇒ Boolean
-
#wait(timeout: nil) ⇒ Object
轮询回收状态直到进程退出或期限到达;返回 Process::Status 或 nil,超时不丢弃 PID。.
-
#winsize ⇒ Object
读取终端的 [行数, 列数];底层并非终端或句柄已关闭时保留原生 IO 异常。.
-
#winsize=(size) ⇒ Object
更新终端尺寸,由内核通知前台进程。.
-
#write(*objects) ⇒ Object
按 Ruby to_s 规则原样写入所有字节,返回字节数;背压等待受 write_timeout 限制。 rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 写入、背压排空和同一期限必须同步推进。.
-
#write_log(*objects) ⇒ Object
向当前日志目标补写内容,支持 IO 和回调,不发送给子进程或监听器。 启用脱敏后可能暂存末尾字节,因此一次调用不保证触发一次日志写入或回调。.
- #writer ⇒ Object
Constructor Details
#initialize(*command, env: {}, chdir: nil) ⇒ Expect
创建 PTY,可立即启动命令,也可先让调用方配置 slave;构造失败时释放全部新句柄。
184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/expect.rb', line 184 def initialize(*command, env: {}, chdir: nil, **) master, slave = PTY.open initialize_session(master, writer: master, slave: slave, own: true, **) @tty_name = slave.path spawn(*command, env: env, chdir: chdir) unless command.empty? initialized = true rescue Exception # rubocop:disable Lint/RescueException -- 构造异常时也要关闭已创建的资源并回收已启动的子进程。 failed = true raise ensure cleanup(failed:) { cleanup_session(master, writer: master, slave: slave, own: true) } unless initialized end |
Instance Attribute Details
#buffer_discarded_bytes ⇒ Object (readonly)
Returns the value of attribute buffer_discarded_bytes.
174 175 176 |
# File 'lib/expect.rb', line 174 def buffer_discarded_bytes @buffer_discarded_bytes end |
#command ⇒ Object (readonly)
Returns the value of attribute command.
174 175 176 |
# File 'lib/expect.rb', line 174 def command @command end |
#diagnostic_output ⇒ Object
读取当前日志目标,可能为库打开的文件、借用的 IO、回调或 nil。
9 10 11 |
# File 'lib/expect/logging.rb', line 9 def diagnostic_output @diagnostic_output end |
#last_result ⇒ Object (readonly)
Returns the value of attribute last_result.
174 175 176 |
# File 'lib/expect.rb', line 174 def last_result @last_result end |
#log_output ⇒ Object
读取当前日志目标,可能为库打开的文件、借用的 IO、回调或 nil。
9 10 11 |
# File 'lib/expect/logging.rb', line 9 def log_output @log_output end |
#slave ⇒ Object (readonly)
Returns the value of attribute slave.
174 175 176 |
# File 'lib/expect.rb', line 174 def slave @slave end |
#tty_name ⇒ Object (readonly)
Returns the value of attribute tty_name.
174 175 176 |
# File 'lib/expect.rb', line 174 def tty_name @tty_name end |
Class Method Details
.configuration ⇒ Object
读取冻结的默认配置;子类未单独配置时继承父类快照。
41 42 43 44 45 46 |
# File 'lib/expect.rb', line 41 def configuration return @configuration if defined?(@configuration) return superclass.configuration unless self == Expect CONFIGURATION_MUTEX.synchronize { @configuration ||= Configuration.new.freeze } end |
.configure ⇒ Object
基于旧快照构造可修改副本,全部赋值与配置块成功后才发布,异常时保留原配置。
49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/expect.rb', line 49 def configure(**) raise ThreadError, "nested configure is not supported" if CONFIGURATION_MUTEX.owned? # 初始化默认快照后,将整个读改写过程串行化,避免并发配置丢失更新。 configuration CONFIGURATION_MUTEX.synchronize do updated = Configuration.new(**configuration.to_h, **) yield updated if block_given? @configuration = updated.freeze end end |
.continue(reset_timeout: true) ⇒ Object
返回继续等待的控制符,reset_timeout 决定是否重新计算匹配期限。
109 |
# File 'lib/expect.rb', line 109 def continue(reset_timeout: true) = reset_timeout ? CONTINUE : CONTINUE_WITHOUT_RESET |
.duration(value) ⇒ Object
将秒数转换为有限的非负数,nil 表示无限;供配置和单次操作共用校验。
115 116 117 118 119 120 121 122 |
# File 'lib/expect.rb', line 115 def duration(value) return nil if value.nil? number = Float(value) raise ArgumentError, "duration must be finite and nonnegative" unless number.finite? && number >= 0 number end |
.expect ⇒ Object
进行多会话匹配,返回命中的模式序号,超时、EOF 或读取错误返回 nil。
101 |
# File 'lib/expect.rb', line 101 def expect(...) = expect_result(...).number |
.expect_result(*patterns, from: [], timeout: configuration.timeout, deadline: nil) ⇒ Object
多会话等待的完整结果入口;from: 提供默认来源,块内可分别指定每个模式的来源。
104 105 106 |
# File 'lib/expect.rb', line 104 def expect_result(*patterns, from: [], timeout: configuration.timeout, deadline: nil, &) run_expect(from, patterns, timeout, deadline: deadline, &) end |
.interconnect(*sessions, timeout: nil) ⇒ Object
按各会话 listeners 建立转发图,处理转义、EOF 和总期限,返回引发停止的会话或 nil。
81 82 83 84 85 86 87 |
# File 'lib/expect/interaction.rb', line 81 def self.interconnect(*sessions, timeout: nil) raise ArgumentError, "interconnect requires Expect sessions" if sessions.empty? || sessions.any? do |session| !session.is_a?(Expect) end Relay.new(sessions, timeout).run end |
.monotonic ⇒ Object
读取不受系统时间调整影响的单调时钟,所有相对超时共用此计时基准。
112 |
# File 'lib/expect.rb', line 112 def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC) |
.open(io, writer: io, own: false) ⇒ Object
适配已有 IO;own: true 接管关闭责任,初始化失败也释放接管的读写端。
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/expect.rb', line 81 def open(io, writer: io, own: false, **) session = allocate session.__send__(:initialize_session, io, writer: writer, own: own, **) initialized = true return session unless block_given? yield session rescue Exception # rubocop:disable Lint/RescueException -- 初始化和块异常均须保留,清理失败不能替换原始原因。 failed = true raise ensure if session && (block_given? || !initialized) session.__send__(:cleanup, failed: failed) do session.__send__(:cleanup_session, io, writer: writer, own: own, graceful: initialized && session.graceful_close?) end end end |
.readable_sessions(*sessions, timeout: 0) ⇒ Object
等待并返回可读会话,不消费输入;去重并忽略已关闭会话,默认非阻塞。
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/expect.rb', line 125 def readable_sessions(*sessions, timeout: 0) timeout = duration(timeout) raise ArgumentError, "readable_sessions requires Expect sessions" unless sessions.all?(Expect) active = sessions.uniq.reject(&:closed?) return [] if active.empty? deadline = timeout && (monotonic + timeout) polled = false ready = nil loop do remaining = deadline && [deadline - monotonic, 0].max return [] if polled && remaining&.zero? begin ready = IO.select(active.map(&:to_io), nil, nil, remaining) break rescue Errno::EINTR polled = true end end return [] unless ready active.select { |session| ready.first.include?(session.to_io) } end |
.spawn(*command, env: {}, chdir: nil) ⇒ Object
创建并启动会话;有块时返回块结果并确保关闭,无块时由调用方负责生命周期。
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/expect.rb', line 62 def spawn(*command, env: {}, chdir: nil, **) session = new(**) session.spawn(*command, env: env, chdir: chdir) spawned = true return session unless block_given? yield session rescue Exception # rubocop:disable Lint/RescueException -- 记录本次作用域的失败,清理后原样传播,包括非 StandardError 异常。 failed = true raise ensure if session && (block_given? || !spawned) session.__send__(:cleanup, failed: failed) do session.close(graceful: spawned && session.graceful_close?) end end end |
Instance Method Details
#<<(object) ⇒ Object
链式写入单个对象,返回当前会话。
384 385 386 387 |
# File 'lib/expect.rb', line 384 def <<(object) write(object) self end |
#after ⇒ Object
301 |
# File 'lib/expect.rb', line 301 def after = @last_result&.after |
#alive? ⇒ Boolean
先刷新回收状态,再判断是否仍有未回收的子进程;不以 IO 是否关闭代替进程状态。
288 289 290 291 |
# File 'lib/expect.rb', line 288 def alive? process_status !pid.nil? end |
#before ⇒ Object
以下访问器读取最近一次等待结果;未发生匹配时捕获组返回空数组。
299 |
# File 'lib/expect.rb', line 299 def before = @last_result&.before |
#buffer ⇒ Object
返回缓冲副本,防止调用方原地修改绕过裁剪规则。
312 |
# File 'lib/expect.rb', line 312 def buffer = @buffer.dup |
#buffer=(value) ⇒ Object
复制并替换原始字节缓冲,应用当前上限;调用方后续修改原字符串不会影响会话。
315 316 317 318 319 320 321 |
# File 'lib/expect.rb', line 315 def buffer=(value) raise ArgumentError, "buffer must be a String" unless value.is_a?(String) @buffer = value.b @buffer_generation += 1 trim_buffer end |
#buffer_limit=(value) ⇒ Object
校验并更新缓冲上限后,立即裁剪已接收的内容;校验失败不改变旧缓冲。
177 178 179 180 181 |
# File 'lib/expect.rb', line 177 def buffer_limit=(value) @configuration.buffer_limit = value trim_buffer value end |
#captures ⇒ Object
307 |
# File 'lib/expect.rb', line 307 def captures = @last_result&.captures || [] |
#clear_buffer ⇒ Object
移交旧缓冲并换上新的空字节串,供显式清空或人工转接接管数据。
324 325 326 327 328 329 |
# File 'lib/expect.rb', line 324 def clear_buffer previous = @buffer @buffer = "".b @buffer_generation += 1 previous end |
#close(graceful: graceful_close?) ) ⇒ Object
通用生命周期清理:可先软关闭,ensure 中硬关闭兜底;正常完成返回 nil。
445 446 447 448 449 450 451 452 453 |
# File 'lib/expect.rb', line 445 def close(graceful: graceful_close?) soft_close if graceful nil rescue Exception # rubocop:disable Lint/RescueException -- 软关闭的原始异常在硬关闭兜底后继续传播。 failed = true raise ensure cleanup(failed:) { hard_close } end |
#closed? ⇒ Boolean
区分会话关闭和输入结束,已关闭会话也不能继续读取。
294 |
# File 'lib/expect.rb', line 294 def closed? = @closed || to_io.closed? |
#continue(reset_timeout: true) ⇒ Object
供实例回调返回继续控制符,语义与 Expect.continue 相同。
261 |
# File 'lib/expect.rb', line 261 def continue(reset_timeout: true) = Expect.continue(reset_timeout: reset_timeout) |
#eof? ⇒ Boolean
296 |
# File 'lib/expect.rb', line 296 def eof? = @eof || closed? |
#error ⇒ Object
309 |
# File 'lib/expect.rb', line 309 def error = @last_result&.error |
#exit_code ⇒ Object
285 |
# File 'lib/expect.rb', line 285 def exit_code = process_status&.exitstatus |
#expect ⇒ Object
在当前会话等待文本或事件,返回模式序号或 nil。
253 |
# File 'lib/expect.rb', line 253 def expect(...) = expect_result(...).number |
#expect_result(*patterns, timeout: self.timeout, deadline: nil) ⇒ Object
使用会话默认超时构造一次等待,返回含匹配内容、来源和错误的 Result。
256 257 258 |
# File 'lib/expect.rb', line 256 def expect_result(*patterns, timeout: self.timeout, deadline: nil, &) self.class.__send__(:run_expect, [self], patterns, timeout, deadline: deadline, &) end |
#fileno ⇒ Object
268 |
# File 'lib/expect.rb', line 268 def fileno = closed? ? nil : to_io.fileno |
#hard_close(timeout: 0.2) ⇒ Object
立即关闭句柄,再分阶段等待、TERM、KILL;不收集剩余输出,返回已回收状态或 nil。
437 438 439 440 441 442 |
# File 'lib/expect.rb', line 437 def hard_close(timeout: 0.2) period = Expect.duration(timeout) raise ArgumentError, "hard_close timeout must be finite" unless period close_resources(timeout: period, term_timeout: period, force: true) end |
#inspect ⇒ Object
诊断时仅显示进程和描述符状态,避免默认对象展开泄露缓冲或日志内容。
273 |
# File 'lib/expect.rb', line 273 def inspect = "#<#{self.class} pid=#{pid.inspect} fd=#{fileno.inspect} closed=#{closed?}>" |
#interact(input: $stdin, escape: nil, output: nil, timeout: nil) ⇒ Object
临时将输入、会话和输出相连,实现人工接管;结束时恢复双方监听器、日志开关和转义设置。
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/expect/interaction.rb', line 53 def interact(input: $stdin, escape: nil, output: nil, timeout: nil) source = interact_source(input) output ||= input.equal?($stdin) ? $stdout : input saved_self = [listeners, log_stdout, log_listeners] saved_source = [source.listeners, source.log_stdout, source.log_listeners, source.sequences.dup] terminal_state = prepare_interact_terminal(source) display = interact_display(source, output, terminal_state) # 临时建立“用户输入 -> 子进程 -> 显示输出”的双向连接,原监听关系在 ensure 中恢复。 self.listeners = [display] self.log_stdout = false self.log_listeners = true source.listeners = [self] source.log_stdout = false source.log_listeners = true source.on_sequence(escape) if escape Expect.interconnect(self, source, timeout: timeout) ensure begin if saved_self self.listeners, self.log_stdout, self.log_listeners = saved_self source.listeners, source.log_stdout, source.log_listeners, source.sequences = saved_source end ensure restore_interact_terminal(terminal_state) end end |
#listeners ⇒ Object
返回监听器列表副本,避免外部原地修改转发关系。
81 |
# File 'lib/expect/logging.rb', line 81 def listeners = @listeners.dup |
#listeners=(outputs) ⇒ Object
校验所有监听器均可写后一次性替换列表,外部数组后续修改不会影响会话。
84 85 86 87 88 89 |
# File 'lib/expect/logging.rb', line 84 def listeners=(outputs) outputs = Array(outputs) raise ArgumentError, "listeners must support write" unless outputs.all? { |output| output.respond_to?(:write) } @listeners = outputs.dup end |
#log_to(target = nil, mode: "a", &block) ⇒ Object
打开追加/覆盖日志文件,或注册接收字节的日志块;同一次只能指定一种目标。
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/expect/logging.rb', line 47 def log_to(target = nil, mode: "a", &block) raise ArgumentError, "provide a log target or a block, not both" if block && target target = block if block if target.respond_to?(:to_path) || target.is_a?(String) raise ArgumentError, "log mode must be a or w" unless %w[a w].include?(mode) # 先交付旧过滤尾部,再允许新路径截断;同一文件不能在截断后被旧句柄写回。 flush_log # 库打开的文件由 SessionResources 持有,替换日志或关闭会话时释放;外部 IO 只借用。 replace_log(File.open(target, "#{mode}b", 0o600), owned: true) else raise ArgumentError, "provide a log target or a block" unless target self.log_output = target end end |
#match ⇒ Object
303 |
# File 'lib/expect.rb', line 303 def match = @last_result&.match |
#match_number ⇒ Object
305 |
# File 'lib/expect.rb', line 305 def match_number = @last_result&.number |
#on_sequence(sequence, &block) ⇒ Object
注册字面、正则转义或 :eof 事件;回调用闭包保存上下文,nil/false 停止,其余值继续。
40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/expect/interaction.rb', line 40 def on_sequence(sequence, &block) key = case sequence when :eof, Regexp then sequence when String then sequence.b.freeze else raise ArgumentError, "sequence must be a String, Regexp or :eof" end raise ArgumentError, "escape sequence must not be empty" if key == "" @sequences[key] = block self end |
#pending_output? ⇒ Boolean
超时或异常后仍有未交付的数据;重新 interconnect 同一源会话可继续发送。
90 |
# File 'lib/expect/interaction.rb', line 90 def pending_output? = @relay_outputs.any? { |output| !output.done? } |
#pid ⇒ Object
275 |
# File 'lib/expect.rb', line 275 def pid = @resources.pid |
#process_status ⇒ Object
非阻塞回收并缓存子进程状态;未退出或仅适配 IO 时返回 nil。
278 279 280 281 282 283 |
# File 'lib/expect.rb', line 278 def process_status @resources.reap rescue Errno::EINTR # 单次轮询被中断时状态仍未知;wait/close 会在原期限内继续,不在这里无限重试。 @resources.status end |
#puts(*objects) ⇒ Object
委托 StringIO 处理换行、nil 和递归数组,再统一写入;返回 nil,与 Ruby puts 一致。
390 391 392 393 394 395 |
# File 'lib/expect.rb', line 390 def puts(*objects) output = StringIO.new("".b) output.puts(*objects) write(output.string) nil end |
#redact(*secrets) ⇒ Object
秘密仅作用于本会话的日志和诊断,不改写匹配、stdout 显示或 listeners 的协议字节。 先校验全部值再发布;注册是追加操作,应在首次通信前完成,不能追溯已交付的日志。
26 27 28 29 30 31 32 33 34 35 |
# File 'lib/expect/logging.rb', line 26 def redact(*secrets) unless secrets.any? && secrets.all? { |secret| secret.is_a?(String) && !secret.empty? } raise ArgumentError, "secrets must be nonempty Strings" end @secrets = ((@secrets || []) + secrets.map { |secret| secret.b.freeze }).uniq.freeze @log_redactor.patterns = @secrets if @log_redactor @diagnostic_redactors&.each_value { |redactor| redactor.patterns = @secrets } self end |
#send_slow(*objects, delay:) ⇒ Object
逐字符延迟发送,同时收集回复,适配输入处理较慢的交互程序;返回写入字节数。
398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/expect.rb', line 398 def send_slow(*objects, delay:) pause = Expect.duration(delay) raise ArgumentError, "delay is required" unless pause count = 0 objects.each do |object| object.to_s.each_char do |character| sleep(pause) if pause.positive? count += write(character) read_available if !eof? && to_io.wait_readable(0) end end count end |
#soft_close(timeout: 15, term_timeout: 1) ⇒ Object
先在自然退出期限内收集尾部输出,再关闭句柄并最多发送 TERM;不会发送 KILL。 尚未退出时返回 nil 并保留 PID,调用方可以继续等待或随后硬关闭。
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 |
# File 'lib/expect.rb', line 420 def soft_close(timeout: 15, term_timeout: 1) period = Expect.duration(timeout) term_timeout = Expect.duration(term_timeout) raise ArgumentError, "term_timeout must be finite" unless term_timeout deadline = period && (Expect.monotonic + period) until eof? remaining = deadline && [deadline - Expect.monotonic, 0].max break if remaining&.zero? || !to_io.wait_readable(remaining) read_available end close_resources(timeout: deadline ? [deadline - Expect.monotonic, 0].max : nil, term_timeout: term_timeout, force: false) end |
#spawn(*command, env: {}, chdir: nil) ⇒ Object
在新控制终端中执行命令并同步确认 exec 结果;同一会话只能启动一次。 rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- PTY 子进程启动与失败回传共用一次生命周期。
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/expect.rb', line 199 def spawn(*command, env: {}, chdir: nil) raise SpawnError, "cannot reuse a spawned session" if @command raise SpawnError, "only a new PTY session can spawn" unless @slave && !@slave.closed? && !closed? raise ArgumentError, "command is required" if command.empty? raise ArgumentError, "command arguments must be strings" unless command.all? do |part| part.is_a?(String) && !part.include?("\0") end raise ArgumentError, "command is empty" if command.first.empty? @slave.raw! if raw_pty? # 错误管道的写端在 exec 成功时自动关闭;父进程据此区分成功启动与 exec 前失败。 from_child, to_parent = IO.pipe to_parent.close_on_exec = true @command = command.map { |part| part.dup.freeze }.freeze child = fork do from_child.close Process.setsid # 创建独立进程会话后重新打开 slave,使它成为子进程的控制终端。 File.open(@tty_name, File::RDWR) do |terminal| # 重定向操作系统的标准描述符;即使宿主替换过 Ruby 标准流,也能正确连接子进程。 # rubocop:disable Style/GlobalStdStream STDIN.reopen(terminal) STDOUT.reopen(terminal) STDERR.reopen(terminal) # rubocop:enable Style/GlobalStdStream end @resources.close_handles Dir.chdir(chdir) if chdir exec(env, *command, close_others: true) rescue Exception => error # rubocop:disable Lint/RescueException -- 子进程回传启动异常后立即退出。 begin to_parent.write("#{error.class}: #{error.message}") ensure exit! 127 end end @resources.pid = child to_parent.close @slave.close failure = from_child.read unless failure.empty? hard_close raise SpawnError, failure end trace("spawned pid=#{child}", event: :spawned) self ensure from_child&.close unless from_child&.closed? to_parent&.close unless to_parent&.closed? end |
#stty(*modes) ⇒ Object
查询可恢复的终端模式字符串,或通过系统 stty 设置模式;参数按数组传递,不经 shell。 辅助进程与管道独立记账,不能覆盖主会话 PID;失败时保留原异常并有界回收。
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/expect/terminal.rb', line 9 def stty(*modes) return "" unless tty? modes = modes.flat_map { |mode| Shellwords.split(mode.to_s) } modes = ["-g"] if modes.empty? reader, sink = IO.pipe resources = SessionResources.new(reader, writer: sink, own: true) resources.pid = Process.spawn("stty", *modes, in: to_io, out: sink, err: sink) sink.close output = reader.read status = wait_stty(resources.pid) resources.pid = nil raise IOError, "stty failed: #{output.strip}" unless status.success? output.strip rescue Errno::ENOENT failed = true raise IOError, "stty executable not found in PATH; install the system terminal utilities" rescue Exception # rubocop:disable Lint/RescueException -- 中断同样先回收辅助进程,再原样传播;不读取调用者的旧 $!。 failed = true raise ensure cleanup(failed:) { cleanup_stty(resources, reader, sink) } end |
#to_io ⇒ Object
暴露底层读写 IO 与终端属性,供 select、终端设置及 IO 适配使用。
264 |
# File 'lib/expect.rb', line 264 def to_io = @resources.reader |
#tty? ⇒ Boolean
270 |
# File 'lib/expect.rb', line 270 def tty? = !closed? && to_io.tty? |
#wait(timeout: nil) ⇒ Object
轮询回收状态直到进程退出或期限到达;返回 Process::Status 或 nil,超时不丢弃 PID。
414 415 416 |
# File 'lib/expect.rb', line 414 def wait(timeout: nil) wait_for_child(Expect.duration(timeout)) end |
#winsize ⇒ Object
读取终端的 [行数, 列数];底层并非终端或句柄已关闭时保留原生 IO 异常。
35 |
# File 'lib/expect/terminal.rb', line 35 def winsize = to_io.winsize |
#winsize=(size) ⇒ Object
更新终端尺寸,由内核通知前台进程。
38 39 40 |
# File 'lib/expect/terminal.rb', line 38 def winsize=(size) to_io.winsize = size end |
#write(*objects) ⇒ Object
按 Ruby to_s 规则原样写入所有字节,返回字节数;背压等待受 write_timeout 限制。 rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 写入、背压排空和同一期限必须同步推进。
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 |
# File 'lib/expect.rb', line 333 def write(*objects) raise IOError, "closed Expect session" if closed? || writer.closed? data = objects.map { |object| object.to_s.b }.join trace_data(:sending, data, level: 2) if debug_level >= 2 deadline = write_timeout && (Expect.monotonic + write_timeout) offset = 0 while offset < data.bytesize begin chunk = data.byteslice(offset, READ_SIZE) count = writer.write_nonblock(chunk, exception: false) rescue Errno::EINTR raise WriteTimeout.new(bytes_written: offset) if deadline && Expect.monotonic >= deadline next end if count == :wait_writable raise WriteTimeout.new(bytes_written: offset) if deadline && Expect.monotonic >= deadline remaining = deadline && [deadline - Expect.monotonic, 0].max # 子进程也可能因输出管道填满而停止读取;等可写时同时排空它的输出,避免双向死锁。 readers = eof? ? [] : [to_io] begin ready = IO.select(readers, [writer], nil, remaining) raise WriteTimeout.new(bytes_written: offset) unless ready if ready[0].include?(to_io) begin read_available rescue WriteTimeout # 日志或监听器可嵌套写入;对外报告本次写入进度,原异常通过 cause 保留。 raise WriteTimeout.new("write interrupted by an output timeout", bytes_written: offset) end end rescue Errno::EINTR next end else unless count.is_a?(Integer) && count.positive? && count <= chunk.bytesize raise IOError, "write must return the number of accepted bytes" end offset += count end end data.bytesize end |
#write_log(*objects) ⇒ Object
向当前日志目标补写内容,支持 IO 和回调,不发送给子进程或监听器。 启用脱敏后可能暂存末尾字节,因此一次调用不保证触发一次日志写入或回调。
67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/expect/logging.rb', line 67 def write_log(*objects) target = log_output return unless target data = objects.map { |object| object.to_s.b }.join if @secrets @log_redactor ||= Redactor.new(@secrets) data = @log_redactor.append(data) return if data.empty? end target.respond_to?(:call) ? target.call(data) : emit(target, data) end |
#writer ⇒ Object
266 |
# File 'lib/expect.rb', line 266 def writer = @resources.writer |