expect-pty 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +22 -9
- data/docs/COMPATIBILITY.md +23 -1
- data/docs/RELEASING.md +21 -8
- data/docs/VERIFICATION.md +65 -1
- data/expect-pty.gemspec +1 -1
- data/lib/expect/configuration.rb +7 -0
- data/lib/expect/interaction.rb +249 -0
- data/lib/expect/{engine.rb → matcher.rb} +40 -5
- data/lib/expect/pattern.rb +12 -20
- data/lib/expect/relay.rb +133 -0
- data/lib/expect/relay_writer.rb +66 -0
- data/lib/expect/result.rb +2 -0
- data/lib/expect/{resources.rb → session_resources.rb} +3 -3
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +105 -30
- data/script/ci +3 -4
- data/script/release.rb +22 -10
- data/test/edge_case_test.rb +101 -22
- data/test/interact_test.rb +102 -0
- data/test/interconnect_test.rb +203 -4
- data/test/io_test.rb +102 -0
- data/test/matching_test.rb +33 -0
- data/test/relay_recovery_test.rb +414 -0
- data/test/release_test.rb +67 -1
- data/test/support/script_probe.rb +1 -0
- metadata +9 -6
- data/lib/expect/interconnect.rb +0 -170
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: expect-pty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- expect-pty contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-26 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: Automate interactive programs with exact and regexp matching, Ruby blocks,
|
|
13
13
|
multi-session waits, logging and terminal interaction.
|
|
@@ -34,13 +34,15 @@ files:
|
|
|
34
34
|
- expect-pty.gemspec
|
|
35
35
|
- lib/expect.rb
|
|
36
36
|
- lib/expect/configuration.rb
|
|
37
|
-
- lib/expect/
|
|
38
|
-
- lib/expect/
|
|
37
|
+
- lib/expect/interaction.rb
|
|
38
|
+
- lib/expect/matcher.rb
|
|
39
39
|
- lib/expect/pattern.rb
|
|
40
40
|
- lib/expect/pattern_list.rb
|
|
41
41
|
- lib/expect/pty.rb
|
|
42
|
-
- lib/expect/
|
|
42
|
+
- lib/expect/relay.rb
|
|
43
|
+
- lib/expect/relay_writer.rb
|
|
43
44
|
- lib/expect/result.rb
|
|
45
|
+
- lib/expect/session_resources.rb
|
|
44
46
|
- lib/expect/version.rb
|
|
45
47
|
- script/ci
|
|
46
48
|
- script/release.rb
|
|
@@ -61,6 +63,7 @@ files:
|
|
|
61
63
|
- test/matching_test.rb
|
|
62
64
|
- test/multi_session_test.rb
|
|
63
65
|
- test/process_test.rb
|
|
66
|
+
- test/relay_recovery_test.rb
|
|
64
67
|
- test/release_test.rb
|
|
65
68
|
- test/ruby_api_test.rb
|
|
66
69
|
- test/script_logging_test.rb
|
|
@@ -91,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
91
94
|
- !ruby/object:Gem::Version
|
|
92
95
|
version: '0'
|
|
93
96
|
requirements: []
|
|
94
|
-
rubygems_version: 4.0.
|
|
97
|
+
rubygems_version: 4.0.12
|
|
95
98
|
specification_version: 4
|
|
96
99
|
summary: Ruby PTY automation with the Expect.pm interaction model
|
|
97
100
|
test_files: []
|
data/lib/expect/interconnect.rb
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# 为会话补充人工接管和多路 IO 转接;核心会话定义位于 lib/expect.rb。
|
|
4
|
-
class Expect
|
|
5
|
-
# 注册字面、正则转义或 :eof 事件;回调用闭包保存上下文,nil/false 停止,其余值继续。
|
|
6
|
-
def on_sequence(sequence, &block)
|
|
7
|
-
key = case sequence
|
|
8
|
-
when :eof, Regexp then sequence
|
|
9
|
-
when String then sequence.b.freeze
|
|
10
|
-
else raise ArgumentError, "sequence must be a String, Regexp or :eof"
|
|
11
|
-
end
|
|
12
|
-
raise ArgumentError, "escape sequence must not be empty" if key == ""
|
|
13
|
-
|
|
14
|
-
@sequences[key] = block
|
|
15
|
-
self
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# 临时将输入、会话和输出相连,实现人工接管;结束时恢复双方监听器、日志开关和转义设置。
|
|
19
|
-
def interact(input: $stdin, escape: nil, output: nil, timeout: nil)
|
|
20
|
-
source = input.is_a?(Expect) ? input : Expect.open(input)
|
|
21
|
-
output ||= input.equal?($stdin) ? $stdout : input
|
|
22
|
-
saved_self = [listeners, log_stdout, log_listeners]
|
|
23
|
-
saved_source = [source.listeners, source.log_stdout, source.log_listeners, source.sequences.dup]
|
|
24
|
-
# 临时建立“用户输入 -> 子进程 -> 显示输出”的双向连接,原监听关系在 ensure 中恢复。
|
|
25
|
-
self.listeners = [output]
|
|
26
|
-
self.log_stdout = false
|
|
27
|
-
self.log_listeners = true
|
|
28
|
-
source.listeners = [self]
|
|
29
|
-
source.log_stdout = false
|
|
30
|
-
source.log_listeners = true
|
|
31
|
-
source.on_sequence(escape) if escape
|
|
32
|
-
Expect.interconnect(self, source, timeout: timeout)
|
|
33
|
-
ensure
|
|
34
|
-
if saved_self
|
|
35
|
-
self.listeners, self.log_stdout, self.log_listeners = saved_self
|
|
36
|
-
source.listeners, source.log_stdout, source.log_listeners, source.sequences = saved_source
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# 按各会话 listeners 建立转发图,处理转义、EOF 和总期限,返回引发停止的会话或 nil。
|
|
41
|
-
def self.interconnect(*sessions, timeout: nil)
|
|
42
|
-
raise ArgumentError, "interconnect requires Expect sessions" if sessions.empty? || sessions.any? do |session|
|
|
43
|
-
!session.is_a?(Expect)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
period = duration(timeout)
|
|
47
|
-
deadline = period && (monotonic + period)
|
|
48
|
-
active = sessions.uniq
|
|
49
|
-
# buffers 保存尚未转发的字节,histories 保存正则跨读取所需的已转发前缀。
|
|
50
|
-
# 接管原匹配缓冲时只转发,日志已在实际读取时记录,不能重复写入。
|
|
51
|
-
buffers = active.to_h { |session| [session, session.clear_buffer] }
|
|
52
|
-
histories = active.to_h { |session| [session, "".b] }
|
|
53
|
-
terminal_objects = active.flat_map { |session| [session, *session.listeners] }.uniq
|
|
54
|
-
saved = []
|
|
55
|
-
polled = false
|
|
56
|
-
begin
|
|
57
|
-
terminal_objects.each do |object|
|
|
58
|
-
next if object.is_a?(Expect) && !object.raw_terminal?
|
|
59
|
-
|
|
60
|
-
io = object.respond_to?(:to_io) ? object.to_io : object
|
|
61
|
-
next unless io.respond_to?(:tty?) && io.tty?
|
|
62
|
-
next if saved.any? { |entry| entry.first.equal?(io) }
|
|
63
|
-
|
|
64
|
-
# 修改前先保存模式,同一个 IO 对象只保存一次,部分初始化失败也能恢复。
|
|
65
|
-
saved << [io, io.console_mode]
|
|
66
|
-
io.raw!
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
loop do
|
|
70
|
-
active.dup.each do |session|
|
|
71
|
-
return session unless relay_buffer(session, buffers, histories, final: session.eof?)
|
|
72
|
-
next unless session.eof?
|
|
73
|
-
|
|
74
|
-
# EOF 回调为真才继续监听其他源,当前结束源随后移出 active。
|
|
75
|
-
callback = session.__send__(:sequences)[:eof]
|
|
76
|
-
return session unless callback&.call
|
|
77
|
-
|
|
78
|
-
active.delete(session)
|
|
79
|
-
end
|
|
80
|
-
return nil if active.empty?
|
|
81
|
-
|
|
82
|
-
remaining = deadline && [deadline - monotonic, 0].max
|
|
83
|
-
ready = polled && remaining&.zero? ? nil : IO.select(active.map(&:to_io), nil, nil, remaining)
|
|
84
|
-
polled = true
|
|
85
|
-
unless ready
|
|
86
|
-
# 到期后不会再等转义的后半段,将暂存的字面前缀作为普通输入转发。
|
|
87
|
-
buffers.each do |session, buffer|
|
|
88
|
-
session.__send__(:propagate, buffer) unless buffer.empty?
|
|
89
|
-
buffer.clear
|
|
90
|
-
end
|
|
91
|
-
return nil
|
|
92
|
-
end
|
|
93
|
-
ready[0].each do |io|
|
|
94
|
-
session = active.find { |candidate| candidate.to_io.equal?(io) }
|
|
95
|
-
data = session.__send__(:read_available, propagate: false, accumulate: false)
|
|
96
|
-
buffers[session] << data if data
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
ensure
|
|
100
|
-
# 转义后的尾部归下次 expect/interact 使用;无论正常返回还是异常,都归还缓冲并恢复终端。
|
|
101
|
-
buffers.each { |session, buffer| session.buffer = buffer + session.buffer }
|
|
102
|
-
saved.reverse_each { |io, mode| io.console_mode = mode unless io.closed? }
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# 转发一个会话的待处理缓冲并剔除转义;返回 false 表示应结束整个转接。
|
|
107
|
-
# 字面序列暂存潜在前缀,正则序列结合历史匹配;final 为真时不再等待后续字节。
|
|
108
|
-
def self.relay_buffer(session, buffers, histories, final: false)
|
|
109
|
-
buffer = buffers.fetch(session)
|
|
110
|
-
history = histories.fetch(session)
|
|
111
|
-
sequences = session.__send__(:sequences).except(:eof)
|
|
112
|
-
loop do
|
|
113
|
-
matches = sequences.filter_map do |key, handler|
|
|
114
|
-
if key.is_a?(Regexp)
|
|
115
|
-
position = Pattern.new(value: key).locate(history + buffer)
|
|
116
|
-
if position
|
|
117
|
-
offset, length, = position
|
|
118
|
-
raise ArgumentError, "escape regexp must consume at least one byte" if length.zero?
|
|
119
|
-
|
|
120
|
-
# 历史前缀已实时转发,不能撤回;负偏移表示本次只需消费转义尚未转发的部分。
|
|
121
|
-
[offset - history.bytesize, length, handler]
|
|
122
|
-
end
|
|
123
|
-
else
|
|
124
|
-
position = buffer.index(key)
|
|
125
|
-
[position, key.bytesize, handler] if position
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
if (found = matches.min_by(&:first))
|
|
129
|
-
position, length, callback = found
|
|
130
|
-
session.__send__(:propagate, buffer.byteslice(0, position)) if position.positive?
|
|
131
|
-
buffer.replace(buffer.byteslice([position + length, 0].max..))
|
|
132
|
-
# 转义消费后清除历史,防止继续回调再次匹配同一个转义。
|
|
133
|
-
history.clear
|
|
134
|
-
return false unless callback&.call
|
|
135
|
-
|
|
136
|
-
next
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# 暂存可能构成字面转义的最长后缀,保证 STOP 分两次读取时 ST 不会提前发给子进程。
|
|
140
|
-
held = 0
|
|
141
|
-
unless final
|
|
142
|
-
sequences.each_key do |key|
|
|
143
|
-
next if key.is_a?(Regexp)
|
|
144
|
-
|
|
145
|
-
[key.bytesize - 1, buffer.bytesize].min.downto(1) do |length|
|
|
146
|
-
if buffer.end_with?(key.byteslice(0, length))
|
|
147
|
-
held = [held, length].max
|
|
148
|
-
break
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
count = buffer.bytesize - held
|
|
154
|
-
session.__send__(:propagate, buffer.byteslice(0, count)) if count.positive?
|
|
155
|
-
if sequences.keys.any?(Regexp)
|
|
156
|
-
history << buffer.byteslice(0, count)
|
|
157
|
-
limit = session.buffer_limit
|
|
158
|
-
history.replace(history.byteslice(-limit, limit)) if limit && history.bytesize > limit
|
|
159
|
-
end
|
|
160
|
-
buffer.replace(held.zero? ? "".b : buffer.byteslice(-held, held))
|
|
161
|
-
return true
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
private_class_method :relay_buffer
|
|
165
|
-
|
|
166
|
-
protected
|
|
167
|
-
|
|
168
|
-
# 仅供转接内部保存和恢复注册表,避免公开可变 Hash 绕过 on_sequence 的校验。
|
|
169
|
-
attr_accessor :sequences
|
|
170
|
-
end
|