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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +28 -19
- data/docs/API.md +88 -0
- data/docs/MIGRATION.md +15 -0
- data/lib/expect/cleanup.rb +33 -0
- data/lib/expect/configuration.rb +87 -66
- data/lib/expect/interaction.rb +226 -209
- data/lib/expect/logging.rb +157 -156
- data/lib/expect/matcher.rb +30 -29
- data/lib/expect/pattern.rb +8 -2
- data/lib/expect/pattern_list.rb +35 -5
- data/lib/expect/redactor.rb +11 -4
- data/lib/expect/relay.rb +97 -73
- data/lib/expect/relay_writer.rb +2 -1
- data/lib/expect/result.rb +32 -8
- data/lib/expect/session.rb +508 -0
- data/lib/expect/session_resources.rb +10 -2
- data/lib/expect/terminal.rb +78 -78
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +333 -520
- data/sig/expect.rbs +355 -0
- metadata +13 -106
- data/.rubocop.yml +0 -66
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -38
- data/benchmark/matching.rb +0 -98
- data/benchmark/redactor.rb +0 -64
- data/benchmark/relay.rb +0 -48
- data/benchmark/scaling.rb +0 -70
- data/benchmark/send_slow.rb +0 -41
- data/benchmark/support.rb +0 -114
- data/docs/COMPATIBILITY.md +0 -98
- data/docs/INTERNAL_CONTRACTS.md +0 -154
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -456
- data/examples/dialogue.rb +0 -30
- data/examples/kibitz/README.md +0 -81
- data/examples/kibitz/kibitz.rb +0 -142
- data/examples/kibitz/test_kibitz.rb +0 -37
- data/examples/ssh_auto.rb +0 -94
- data/examples/ssh_interact.rb +0 -159
- data/examples/ssh_login.rb +0 -64
- data/expect-pty.gemspec +0 -33
- data/script/ci +0 -121
- data/script/release.rb +0 -319
- data/test/buffer_accounting_test.rb +0 -83
- data/test/cleanup_test.rb +0 -251
- data/test/compare_upstream.rb +0 -157
- data/test/configuration_test.rb +0 -133
- data/test/deadline_test.rb +0 -237
- data/test/diagnostics_test.rb +0 -353
- data/test/edge_case_test.rb +0 -262
- data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
- data/test/fixtures/ssh_scripts/02_output.sh +0 -5
- data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
- data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
- data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
- data/test/initialization_failure_test.rb +0 -92
- data/test/integration/README.md +0 -109
- data/test/integration/ssh_scripts.rb +0 -94
- data/test/interact_test.rb +0 -253
- data/test/interconnect_test.rb +0 -425
- data/test/io_test.rb +0 -321
- data/test/kibitz_test.rb +0 -45
- data/test/lifecycle_contract_test.rb +0 -71
- data/test/literal_scan_test.rb +0 -97
- data/test/matching_test.rb +0 -211
- data/test/multi_session_test.rb +0 -66
- data/test/ownership_sequence_test.rb +0 -208
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_interruption_test.rb +0 -296
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/relay_reentrancy_test.rb +0 -159
- data/test/release_test.rb +0 -375
- data/test/ruby_api_test.rb +0 -515
- data/test/scan_reuse_test.rb +0 -109
- data/test/script_logging_test.rb +0 -124
- data/test/support/interact_probe.rb +0 -125
- data/test/support/kibitz_probe.rb +0 -177
- data/test/support/script_probe.rb +0 -158
- data/test/terminal_cleanup_test.rb +0 -349
- data/test/test_helper.rb +0 -58
- data/test/timeout_test.rb +0 -289
- data/test/write_contract_test.rb +0 -105
data/lib/expect/redactor.rb
CHANGED
|
@@ -7,7 +7,7 @@ class Expect
|
|
|
7
7
|
class Redactor
|
|
8
8
|
# 完整诊断文本只匹配完整秘密;流边界的疑似秘密前缀由 finish 的默认策略保护。
|
|
9
9
|
def self.redact(data, patterns, replacement: "[FILTERED]")
|
|
10
|
-
filter = new(patterns, replacement:
|
|
10
|
+
filter = new(patterns, replacement:)
|
|
11
11
|
filter.append(data) + filter.finish(partial: false)
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -62,11 +62,18 @@ class Expect
|
|
|
62
62
|
|
|
63
63
|
# 流关闭时无法再等待后续字节,默认隐藏与秘密开头一致的未完成尾部。
|
|
64
64
|
def mark_partial_secrets
|
|
65
|
+
tail = @pending.byteslice(-1, 1)
|
|
66
|
+
return unless tail
|
|
67
|
+
|
|
65
68
|
@patterns.each do |pattern|
|
|
66
|
-
[pattern.bytesize - 1, @pending.bytesize].min
|
|
67
|
-
|
|
69
|
+
length = [pattern.bytesize - 1, @pending.bytesize].min
|
|
70
|
+
# 前缀必须以当前尾字节结束;只收紧最高候选长度,密集候选仍沿用简单倒序扫描。
|
|
71
|
+
next unless length.positive? && (offset = pattern.rindex(tail, length - 1))
|
|
72
|
+
|
|
73
|
+
(offset + 1).downto(1) do |candidate|
|
|
74
|
+
next unless @pending.end_with?(pattern.byteslice(0, candidate))
|
|
68
75
|
|
|
69
|
-
@hidden[-
|
|
76
|
+
@hidden[-candidate, candidate] = "\1" * candidate
|
|
70
77
|
break
|
|
71
78
|
end
|
|
72
79
|
end
|
data/lib/expect/relay.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
class Expect
|
|
4
4
|
# 一次转接的共同读写循环;发送游标留在源会话中,调用结束后仍可继续。
|
|
5
5
|
# 本对象只暂借未处理输入;读缓冲、发送进度与转义回调分开保存,避免重入时重复交付。
|
|
6
|
+
# @api private
|
|
6
7
|
class Relay
|
|
7
8
|
OWNERSHIP_MUTEX = Mutex.new
|
|
8
9
|
private_constant :OWNERSHIP_MUTEX
|
|
@@ -20,123 +21,143 @@ class Expect
|
|
|
20
21
|
|
|
21
22
|
# 每轮先处理转义和已排队输出,再共同选择读写;停止返回来源会话,总期限到达返回 nil。
|
|
22
23
|
# 真实 IO 的写入由非阻塞游标推进,用户日志和回调仍同步执行,须由调用方保证及时返回。
|
|
23
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 共享读写循环统一维护来源和目标期限。
|
|
24
24
|
def run
|
|
25
25
|
prepare_sources
|
|
26
26
|
outputs.each(&:restart_timeout)
|
|
27
27
|
polled = false
|
|
28
28
|
loop do
|
|
29
|
-
idle =
|
|
30
|
-
|
|
31
|
-
@active.dup.each do |session|
|
|
32
|
-
next if session.pending_output?
|
|
33
|
-
|
|
34
|
-
if (callback = session.__send__(:relay_callback))
|
|
35
|
-
session.__send__(:relay_callback=, nil)
|
|
36
|
-
return session unless callback.first&.call
|
|
37
|
-
end
|
|
29
|
+
stopped, idle, queued = dispatch_sources
|
|
30
|
+
return stopped if stopped
|
|
38
31
|
|
|
39
|
-
|
|
40
|
-
session.__send__(:queue_output, data)
|
|
41
|
-
end
|
|
42
|
-
return session unless result
|
|
32
|
+
return nil if @active.empty?
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
return finish_timeout(idle) if polled && expired?
|
|
35
|
+
|
|
36
|
+
progress = advance_outputs || queued
|
|
37
|
+
|
|
38
|
+
polled = true
|
|
39
|
+
return finish_timeout(idle) if select_and_read(progress) == :timeout
|
|
40
|
+
end
|
|
41
|
+
ensure
|
|
42
|
+
restore_sources
|
|
43
|
+
end
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
next unless session.eof?
|
|
45
|
+
private
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
# 先交付转义前缀再派发回调;EOF 只在待发送数据处理完后生效。
|
|
48
|
+
def dispatch_sources
|
|
49
|
+
idle = []
|
|
50
|
+
queued = false
|
|
51
|
+
@active.dup.each do |session|
|
|
52
|
+
next if session.pending_output?
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
if (callback = session.relay_callback)
|
|
55
|
+
session.relay_callback = nil
|
|
56
|
+
return [session, idle, queued] unless callback.first&.call
|
|
56
57
|
end
|
|
57
|
-
return nil if @active.empty?
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
result = Interaction.relay_buffer(session, @buffers, final: session.eof?) do |data|
|
|
60
|
+
session.queue_output(data)
|
|
61
|
+
end
|
|
62
|
+
return [session, idle, queued] unless result
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
if result == :pending
|
|
65
|
+
queued = true
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
idle << session
|
|
70
|
+
next unless session.eof?
|
|
71
|
+
|
|
72
|
+
callback = session.sequences[:eof]
|
|
73
|
+
return [session, idle, queued] unless callback&.call
|
|
74
|
+
|
|
75
|
+
@active.delete(session)
|
|
76
|
+
end
|
|
77
|
+
[nil, idle, queued]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 共同等待来源与目标,每轮读每个就绪来源一次;EINTR 返回原期限循环。
|
|
81
|
+
def select_and_read(progress)
|
|
82
|
+
pending = outputs
|
|
83
|
+
readers = read_sources(pending)
|
|
84
|
+
deadlines = [@deadline, *pending.map(&:deadline)].compact
|
|
85
|
+
remaining = deadlines.empty? ? nil : [deadlines.min - Expect.monotonic, 0].max
|
|
86
|
+
# 有进展时轮询输入再继续发送,避免大块输出饿死其他来源。
|
|
87
|
+
remaining = 0 if progress
|
|
88
|
+
begin
|
|
89
|
+
ready = IO.select(readers.map(&:to_io), pending.filter_map(&:io).uniq(&:object_id), nil, remaining)
|
|
90
|
+
if ready
|
|
91
|
+
read_ready(readers, ready.first)
|
|
92
|
+
elsif !progress
|
|
93
|
+
return :timeout if expired?
|
|
94
|
+
|
|
95
|
+
pending.each(&:check_timeout!)
|
|
96
|
+
end
|
|
97
|
+
rescue Errno::EINTR
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 同源转接期间读取到借用缓冲;目标会话的背压读取沿用自身状态。
|
|
104
|
+
def read_ready(readers, readable)
|
|
105
|
+
return if readable.empty?
|
|
106
|
+
|
|
107
|
+
if readable.size > 1
|
|
108
|
+
ready = {}.compare_by_identity
|
|
109
|
+
readable.each { |io| ready[io] = true }
|
|
110
|
+
end
|
|
111
|
+
readers.each do |session|
|
|
112
|
+
next unless ready ? ready.key?(session.to_io) : readable.first.equal?(session.to_io)
|
|
62
113
|
|
|
63
|
-
pending = outputs
|
|
64
|
-
readers = read_sources(pending)
|
|
65
|
-
deadlines = [@deadline, *pending.map(&:deadline)].compact
|
|
66
|
-
remaining = deadlines.empty? ? nil : [deadlines.min - Expect.monotonic, 0].max
|
|
67
|
-
# 有进展时轮询输入再继续发送,避免大块输出饿死其他来源。
|
|
68
|
-
remaining = 0 if progress
|
|
69
114
|
begin
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
next unless ready[0].include?(session.to_io)
|
|
75
|
-
|
|
76
|
-
begin
|
|
77
|
-
if @buffers.key?(session)
|
|
78
|
-
session.__send__(:read_available, propagate: false, buffer: @buffers.fetch(session), trim: false)
|
|
79
|
-
else
|
|
80
|
-
session.__send__(:read_available, propagate: false)
|
|
81
|
-
end
|
|
82
|
-
rescue Errno::EINTR
|
|
83
|
-
next
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
elsif !progress
|
|
87
|
-
return finish_timeout(idle) if expired?
|
|
88
|
-
|
|
89
|
-
pending.each(&:check_timeout!)
|
|
115
|
+
if @buffers.key?(session)
|
|
116
|
+
session.read_available(propagate: false, buffer: @buffers.fetch(session), trim: false)
|
|
117
|
+
else
|
|
118
|
+
session.read_available(propagate: false)
|
|
90
119
|
end
|
|
91
120
|
rescue Errno::EINTR
|
|
92
121
|
next
|
|
93
122
|
end
|
|
94
123
|
end
|
|
95
|
-
ensure
|
|
96
|
-
restore_sources
|
|
97
124
|
end
|
|
98
125
|
|
|
99
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
100
|
-
|
|
101
|
-
private
|
|
102
|
-
|
|
103
126
|
# 锁仅保护标记检查/登记,不跨 IO 或用户回调;任一来源被占用时整组拒绝,不动缓冲和期限。
|
|
104
127
|
def prepare_sources
|
|
105
128
|
OWNERSHIP_MUTEX.synchronize do
|
|
106
|
-
if @sessions.any?
|
|
107
|
-
raise ReentrancyError, "source session already has an active relay"
|
|
108
|
-
end
|
|
129
|
+
raise ReentrancyError, "source session already has an active relay" if @sessions.any?(&:relay_owner)
|
|
109
130
|
|
|
110
|
-
@sessions.each { |session| session.
|
|
131
|
+
@sessions.each { |session| session.relay_owner = @token }
|
|
111
132
|
end
|
|
112
133
|
@sessions.each do |session|
|
|
113
|
-
@previous[session] = session.
|
|
134
|
+
@previous[session] = session.interaction_buffer
|
|
114
135
|
@buffers[session] = session.clear_buffer
|
|
115
|
-
session.
|
|
136
|
+
session.interaction_buffer = @buffers.fetch(session)
|
|
116
137
|
end
|
|
117
138
|
end
|
|
118
139
|
|
|
119
140
|
# 未处理输入还给会话,已排队字节仍归游标;构造中断也只恢复已转移的缓冲及自己的标记。
|
|
120
141
|
def restore_sources
|
|
121
|
-
@previous.each { |session, buffer| session.
|
|
122
|
-
@buffers.each { |session, buffer| session.
|
|
142
|
+
@previous.each { |session, buffer| session.interaction_buffer = buffer }
|
|
143
|
+
@buffers.each { |session, buffer| session.restore_relay_buffer(buffer) }
|
|
123
144
|
ensure
|
|
124
145
|
OWNERSHIP_MUTEX.synchronize do
|
|
125
146
|
@sessions.each do |session|
|
|
126
|
-
session.
|
|
147
|
+
session.relay_owner = nil if session.relay_owner.equal?(@token)
|
|
127
148
|
end
|
|
128
149
|
end
|
|
129
150
|
end
|
|
130
151
|
|
|
131
152
|
# 只收集尚未完成的目标,供共同 select 以及最早写入期限计算使用。
|
|
132
|
-
def outputs = @sessions.flat_map
|
|
153
|
+
def outputs = @sessions.flat_map(&:relay_outputs).reject(&:done?)
|
|
133
154
|
|
|
134
155
|
# 每个目标每轮最多推进一个片段;移除已完成游标,并告知主循环是否值得立即继续轮询。
|
|
135
156
|
def advance_outputs(check_timeout: true)
|
|
136
157
|
progress = false
|
|
137
158
|
@sessions.each do |session|
|
|
138
|
-
pending = session.
|
|
139
|
-
pending.each { |output| progress = output.advance(check_timeout:
|
|
159
|
+
pending = session.relay_outputs
|
|
160
|
+
pending.each { |output| progress = output.advance(check_timeout:) || progress }
|
|
140
161
|
pending.reject!(&:done?)
|
|
141
162
|
end
|
|
142
163
|
progress
|
|
@@ -148,9 +169,11 @@ class Expect
|
|
|
148
169
|
# 背压源暂停吸收新输入,限制排队增长;作为写入目标的会话仍需读取以解除双向等待。
|
|
149
170
|
def read_sources(pending)
|
|
150
171
|
# 输出目标可能也在等待我们读取;这些来源即使有待发送数据也必须继续排空。
|
|
151
|
-
targets = pending.
|
|
172
|
+
targets = pending.filter_map do |output|
|
|
173
|
+
Session.for(output.target) if output.target.is_a?(Expect)
|
|
174
|
+
end
|
|
152
175
|
sources = @active.reject { |session| session.pending_output? && !targets.include?(session) }
|
|
153
|
-
(sources + targets).uniq
|
|
176
|
+
(sources + targets).uniq { |session| session.to_io.object_id }.reject(&:eof?)
|
|
154
177
|
end
|
|
155
178
|
|
|
156
179
|
# 总期限结束时将可交付尾部转为发送游标,至多尝试一轮交付;余量由源会话保存。
|
|
@@ -164,7 +187,8 @@ class Expect
|
|
|
164
187
|
buffer = @buffers.fetch(session)
|
|
165
188
|
next if buffer.empty?
|
|
166
189
|
|
|
167
|
-
session.
|
|
190
|
+
session.queue_output(buffer.dup)
|
|
191
|
+
Interaction.remember_output(session, buffer)
|
|
168
192
|
buffer.clear
|
|
169
193
|
end
|
|
170
194
|
# 到期后只尝试一次非阻塞写入,剩余游标留给下一次 interconnect。
|
data/lib/expect/relay_writer.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
class Expect
|
|
4
4
|
# 向单个转接目标写入数据,维护发送进度与期限;恢复时不重放已成功写出的前缀。
|
|
5
5
|
# 只借用目标,不 dup 描述符,也不负责读取;所有就绪等待统一交给 Relay 调度。
|
|
6
|
+
# @api private
|
|
6
7
|
class RelayWriter
|
|
7
8
|
attr_reader :target, :deadline
|
|
8
9
|
|
|
@@ -41,7 +42,7 @@ class Expect
|
|
|
41
42
|
if @offset < @data.bytesize
|
|
42
43
|
bytes = @data.byteslice(@offset, READ_SIZE)
|
|
43
44
|
count = io ? io.write_nonblock(bytes, exception: false) : target.write(bytes)
|
|
44
|
-
if count == :wait_writable
|
|
45
|
+
if io && count == :wait_writable
|
|
45
46
|
check_timeout! if check_timeout
|
|
46
47
|
return false
|
|
47
48
|
end
|
data/lib/expect/result.rb
CHANGED
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Expect
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
# 一次等待的不可变快照;session 与 error 只引用来源和原始异常,不取得其所有权。
|
|
5
|
+
# 文本和捕获值单独复制冻结,后续读取或调用者修改输入不会改变结果。
|
|
6
|
+
# @!method self.new(**fields)
|
|
7
|
+
# 按字段关键字或成员顺序的位置参数构造不可变结果。
|
|
8
|
+
# @!method self.[](**fields)
|
|
9
|
+
# 使用与 new 相同的参数构造不可变结果。
|
|
10
|
+
# @!method self.members
|
|
11
|
+
# 返回按声明顺序排列的字段名。
|
|
12
|
+
# @!method self.inspect
|
|
13
|
+
# 返回结果类的说明。
|
|
14
|
+
# @!method members
|
|
15
|
+
# 返回按声明顺序排列的字段名。
|
|
16
|
+
# @!method with(**fields)
|
|
17
|
+
# 复制结果并替换指定字段,新文本仍独立冻结。
|
|
18
|
+
# @!method to_h
|
|
19
|
+
# 返回字段 Hash;有块时按 Ruby Data 协议转换键值对。
|
|
20
|
+
# @!method deconstruct
|
|
21
|
+
# 按成员声明顺序返回字段数组。
|
|
22
|
+
# @!method deconstruct_keys(keys)
|
|
23
|
+
# 按指定键解构结果,nil 表示全部字段。
|
|
24
|
+
Result = Data.define(:number, :error, :match, :before, :after, :session, :captures) do
|
|
25
|
+
# 构造独立不可变快照,复制字符串和捕获数组。
|
|
26
|
+
# @param captures [Array<String, nil>] 按声明顺序保存捕获组,未参与的组保留 nil。
|
|
27
|
+
def initialize(number: nil, error: nil, match: nil, before: nil, after: nil, session: nil, captures: [])
|
|
28
|
+
super(number:, error:, session:, match: match&.dup&.freeze,
|
|
29
|
+
before: before&.dup&.freeze, after: after&.dup&.freeze,
|
|
30
|
+
captures: captures.map { |value| value&.dup&.freeze }.freeze)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 是否命中文本;EOF 和超时没有模式序号。
|
|
10
34
|
def matched? = !number.nil?
|
|
11
35
|
|
|
12
|
-
#
|
|
36
|
+
# 是否因等待期限到达而返回。
|
|
13
37
|
def timeout? = error == :timeout
|
|
14
38
|
|
|
15
|
-
#
|
|
39
|
+
# 是否读取源已经结束,子进程状态由会话单独查询。
|
|
16
40
|
def eof? = error == :eof
|
|
17
41
|
end
|
|
18
42
|
end
|