expect-pty 0.8.2 → 0.9.0

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.
data/lib/expect.rb DELETED
@@ -1,167 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pty"
4
- require "io/console"
5
- require "stringio"
6
- require_relative "expect/version"
7
- require_relative "expect/result"
8
- require_relative "expect/resource_owner"
9
- require_relative "expect/pattern"
10
- require_relative "expect/pattern_list"
11
- require_relative "expect/matcher"
12
- require_relative "expect/cleanup"
13
- require_relative "expect/session"
14
- require_relative "expect/responder"
15
-
16
- # PTY 会话的创建、匹配与转接入口;可变状态只属于返回的 Session。
17
- module Expect
18
- # 重置相对期限后继续等待。
19
- CONTINUE = :continue
20
- # 保留原相对期限继续等待。
21
- CONTINUE_WITHOUT_RESET = :continue_without_reset
22
- # 单轮非阻塞读写的最大字节数。
23
- # @api private
24
- READ_SIZE = 16_384
25
-
26
- # PTY 创建后命令启动失败。
27
- class SpawnError < StandardError; end
28
-
29
- # 拒绝同源递归转接及接收交付回调中的同源递归读取,防止数据归属和顺序被破坏。
30
- class ReentrancyError < StandardError; end
31
-
32
- # 本次写入已经确认交付的进度;其他嵌套写入的异常保留在 cause。
33
- class WriteTimeout < IOError
34
- # 本次 write 已被底层接受的字节数。
35
- attr_reader :bytes_written
36
-
37
- # 构造包含已交付字节数的背压超时。
38
- def initialize(message = "write timed out", bytes_written: 0)
39
- @bytes_written = bytes_written
40
- super(message)
41
- end
42
- end
43
-
44
- class << self
45
- # 创建并启动 Session;有块时返回块结果,并按 graceful 策略关闭所属资源。
46
- # raw 只控制本次子进程 PTY;日志与输出对象始终借用。
47
- def spawn(*command, env: {}, chdir: nil, raw: false, graceful: false,
48
- timeout: nil, write_timeout: nil, buffer_limit: nil, logger: nil, transcript: nil, outputs: [])
49
- session = nil
50
- spawned = false
51
- cleanup = lambda do
52
- if session && (block_given? || !spawned)
53
- session.cleanup_session(nil, writer: nil, own: false, graceful: spawned && graceful)
54
- end
55
- end
56
- Cleanup.always(cleanup) do
57
- # 先持有对象再初始化,构造完成到工厂接管之间不能留下无人负责关闭的窗口。
58
- session = Session.allocate
59
- session.__send__(:initialize, timeout:, write_timeout:, buffer_limit:, logger:, transcript:, outputs:)
60
- session.spawn(*command, env:, chdir:, raw:)
61
- spawned = true
62
- block_given? ? yield(session) : session
63
- end
64
- end
65
-
66
- # 适配真实 IO;own 只决定读写端点的关闭责任,日志与输出目标仍由调用者管理。
67
- # 有块时返回块结果;初始化失败也清理已取得的所属端点。
68
- def open(io, writer: io, own: false, graceful: false,
69
- timeout: nil, write_timeout: nil, buffer_limit: nil, logger: nil, transcript: nil, outputs: [])
70
- session = nil
71
- initialized = false
72
- cleanup = lambda do
73
- if session && (block_given? || !initialized)
74
- session.cleanup_session(io, writer:, own:, graceful: initialized && graceful)
75
- elsif !session && own
76
- ResourceOwner.close_handles(io, writer)
77
- end
78
- end
79
- Cleanup.always(cleanup) do
80
- session = Session.allocate
81
- session.__send__(:initialize_io, io, writer:, own:, timeout:, write_timeout:, buffer_limit:,
82
- logger:, transcript:, outputs:)
83
- initialized = true
84
- block_given? ? yield(session) : session
85
- end
86
- end
87
-
88
- # 按 outputs 建立转发图,返回引发停止的 Session;总期限到达返回 nil。
89
- def interconnect(*sessions, timeout: nil)
90
- raise ArgumentError, "interconnect requires Session objects" unless sessions.any? && sessions.all?(Session)
91
-
92
- Relay.new(sessions, timeout).run
93
- end
94
-
95
- # 共同等待指定来源,返回不可变 Result。consume 控制本轮是否消费匹配文本。
96
- # reset_timeout_on_read 仅重置相对期限,deadline 始终是不可延长的总期限。
97
- def expect(*patterns, from: [], timeout: nil, deadline: nil, consume: true, reset_timeout_on_read: false, &)
98
- run_expect(from, patterns, timeout, deadline:, consume:, reset_timeout_on_read:, &)
99
- end
100
-
101
- # 返回回调继续控制符,reset_timeout 为 false 时保留原相对期限。
102
- def continue(reset_timeout: true) = reset_timeout ? CONTINUE : CONTINUE_WITHOUT_RESET
103
-
104
- # 当前单调时钟秒数;供跨多次等待共享 deadline。
105
- def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
106
-
107
- # 转换有限非负秒数,nil 表示无相对期限。
108
- def duration(value)
109
- return nil if value.nil?
110
-
111
- number = Float(value)
112
- raise ArgumentError, "duration must be finite and nonnegative" unless number.finite? && number >= 0
113
-
114
- number
115
- end
116
-
117
- # 返回可读 Session,不消费输入;去重、排除已关闭来源,并保留声明顺序。
118
- def readable_sessions(*sessions, timeout: 0)
119
- timeout = duration(timeout)
120
- raise ArgumentError, "readable_sessions requires Session objects" unless sessions.all?(Session)
121
-
122
- active = sessions.uniq(&:object_id).reject(&:closed?)
123
- return [] if active.empty?
124
-
125
- deadline = timeout && (monotonic + timeout)
126
- polled = false
127
- ready = nil
128
- loop do
129
- remaining = deadline && [deadline - monotonic, 0].max
130
- return [] if polled && remaining&.zero?
131
-
132
- begin
133
- ready = IO.select(active.map(&:to_io), nil, nil, remaining)
134
- break
135
- rescue Errno::EINTR
136
- polled = true
137
- end
138
- end
139
- return [] unless ready
140
-
141
- select_ready_sessions(active, ready.first)
142
- end
143
-
144
- private
145
-
146
- # 按对象身份归属就绪描述符,不能用 IO 的值相等规则合并来源。
147
- def select_ready_sessions(sessions, readable)
148
- by_io = readable.each_with_object({}.compare_by_identity) { |io, index| index[io] = true }
149
- sessions.select { |session| by_io.key?(session.to_io) }
150
- end
151
-
152
- # 声明完成后固定模式;有参数块保留调用方 self,无参数块使用模式 DSL。
153
- def run_expect(sessions, patterns, timeout, deadline:, consume:, reset_timeout_on_read:,
154
- match_by_position: false, &block)
155
- timeout = duration(timeout)
156
- deadline = Float(deadline) unless deadline.nil?
157
- raise ArgumentError, "deadline must be finite" if deadline && !deadline.finite?
158
- raise ArgumentError, "provide patterns or a pattern block, not both" if block_given? && !patterns.empty?
159
-
160
- pattern_list = PatternList.new(sessions, patterns)
161
- if block
162
- block.parameters.empty? ? pattern_list.instance_exec(&block) : block.call(pattern_list)
163
- end
164
- Matcher.new(pattern_list, timeout, deadline:, consume:, reset_timeout_on_read:, match_by_position:).run
165
- end
166
- end
167
- end