expect-pty 0.5.3 → 0.7.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 +55 -1
- data/README.md +122 -130
- data/docs/API.md +113 -0
- data/docs/MIGRATION.md +68 -0
- data/lib/expect/cleanup.rb +34 -0
- data/lib/expect/interaction.rb +228 -210
- data/lib/expect/logging.rb +117 -162
- data/lib/expect/matcher.rb +42 -38
- data/lib/expect/pattern.rb +10 -3
- data/lib/expect/pattern_list.rb +39 -9
- data/lib/expect/redactor.rb +12 -5
- data/lib/expect/relay.rb +106 -81
- data/lib/expect/relay_writer.rb +7 -5
- data/lib/expect/result.rb +33 -9
- data/lib/expect/session.rb +554 -0
- data/lib/expect/session_resources.rb +19 -18
- data/lib/expect/version.rb +2 -2
- data/lib/expect.rb +68 -578
- data/sig/expect.rbs +279 -0
- metadata +14 -123
- 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/lib/expect/configuration.rb +0 -122
- data/lib/expect/terminal.rb +0 -102
- 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/interaction.rb
CHANGED
|
@@ -3,100 +3,133 @@
|
|
|
3
3
|
require_relative "relay_writer"
|
|
4
4
|
require_relative "relay"
|
|
5
5
|
|
|
6
|
-
# 为会话补充人工接管和多路 IO 转接;核心会话定义位于
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
# 为会话补充人工接管和多路 IO 转接;核心会话定义位于 session.rb。
|
|
7
|
+
module Expect
|
|
8
|
+
# 为公开 Session 混入转义注册与人工接管接口;协作方法各自标记为内部协议。
|
|
9
|
+
# 宿主提供真实读写 IO、缓冲及 outputs;本模块保存转义、显示历史和待写游标。
|
|
10
|
+
# Relay 暂借输入缓冲并调度游标,Matcher 可在转义回调中临时接管,退出时归还未消费尾部。
|
|
11
|
+
module Interaction
|
|
12
|
+
# 正则没有“潜在部分匹配”接口,只保留有限历史;已转发的历史字节不能撤回。
|
|
13
|
+
REGEXP_ESCAPE_HISTORY_LIMIT = 65_536
|
|
14
|
+
# 没有显式回调的转义仍用同一 callable 协议,返回 false 让 Relay 停止。
|
|
15
|
+
STOP = -> { false }.freeze
|
|
16
|
+
private_constant :REGEXP_ESCAPE_HISTORY_LIMIT, :STOP
|
|
17
|
+
|
|
18
|
+
# 在 raw 本地终端显示远端文本,补齐 LF 所需的 CR,同时保留已有 CRLF。
|
|
19
|
+
# @api private
|
|
20
|
+
class InteractOutput
|
|
21
|
+
# 包装器借用目标,不复制或关闭其描述符;换行状态属于这个目标的连续显示流。
|
|
22
|
+
def initialize(target)
|
|
23
|
+
@target = target
|
|
24
|
+
@previous_carriage_return = false
|
|
25
|
+
end
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
attr_reader :target
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
# 只改变显示字节,不改写匹配输入;记住上一块的 CR,避免分块的 CRLF 被扩成 CRCRLF。
|
|
30
|
+
def render(data)
|
|
31
|
+
bytes = data.to_s.b
|
|
32
|
+
rendered = bytes.gsub(/(?<!\r)\n/n, "\r\n")
|
|
33
|
+
rendered = rendered.byteslice(1..) if @previous_carriage_return && bytes.start_with?("\n")
|
|
34
|
+
@previous_carriage_return = bytes.end_with?("\r")
|
|
35
|
+
rendered
|
|
36
|
+
end
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
# 提供普通可写对象接口;Relay 会先 render,再按转换后字节数维护独立发送游标。
|
|
39
|
+
def write(data)
|
|
40
|
+
@target.write(render(data))
|
|
41
|
+
end
|
|
34
42
|
end
|
|
35
|
-
end
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
private_constant :InteractOutput
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
# 注册字面、正则转义或 :eof 事件;回调用闭包保存上下文,nil/false 停止,其余值继续。
|
|
47
|
+
def on_sequence(sequence, &block)
|
|
48
|
+
key = case sequence
|
|
49
|
+
when :eof, Regexp then sequence
|
|
50
|
+
when String then sequence.b.freeze
|
|
51
|
+
else raise ArgumentError, "sequence must be a String, Regexp or :eof"
|
|
52
|
+
end
|
|
53
|
+
raise ArgumentError, "escape sequence must not be empty" if key == ""
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
@sequences[key] = block || STOP
|
|
56
|
+
self
|
|
57
|
+
end
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
source.log_stdout = false
|
|
66
|
-
source.log_listeners = true
|
|
67
|
-
source.on_sequence(escape) if escape
|
|
68
|
-
Expect.interconnect(self, source, timeout: timeout)
|
|
69
|
-
ensure
|
|
70
|
-
begin
|
|
71
|
-
if saved_self
|
|
72
|
-
self.listeners, self.log_stdout, self.log_listeners = saved_self
|
|
73
|
-
source.listeners, source.log_stdout, source.log_listeners, source.sequences = saved_source
|
|
74
|
-
end
|
|
59
|
+
# 临时将输入、会话和输出相连,实现人工接管;结束时恢复双方输出目标、转义和终端模式。
|
|
60
|
+
def interact(input: $stdin, escape: nil, output: nil, timeout: nil, raw: true)
|
|
61
|
+
source = interact_source(input)
|
|
62
|
+
output = input.equal?($stdin) ? $stdout : input if output.nil?
|
|
63
|
+
saved_self = outputs
|
|
64
|
+
saved_source = [source.outputs, source.sequences.dup]
|
|
65
|
+
terminal_state = prepare_interact_terminal(source) if raw
|
|
66
|
+
display = interact_display(source, output, terminal_state)
|
|
67
|
+
# 临时建立“用户输入 -> 子进程 -> 显示输出”的双向连接,原监听关系在 ensure 中恢复。
|
|
68
|
+
self.outputs = [display]
|
|
69
|
+
source.outputs = [self]
|
|
70
|
+
source.on_sequence(escape) unless escape.nil?
|
|
71
|
+
Relay.new([self, source], timeout).run
|
|
75
72
|
ensure
|
|
76
|
-
|
|
73
|
+
begin
|
|
74
|
+
if saved_self
|
|
75
|
+
self.outputs = saved_self
|
|
76
|
+
source.outputs, source.sequences = saved_source
|
|
77
|
+
end
|
|
78
|
+
ensure
|
|
79
|
+
restore_interact_terminal(terminal_state)
|
|
80
|
+
end
|
|
77
81
|
end
|
|
78
|
-
end
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
# 超时或异常后仍有未交付的数据;重新 interconnect 同一源会话可继续发送。
|
|
84
|
+
def pending_output? = @pending_writes.any? { |output| !output.done? }
|
|
85
|
+
|
|
86
|
+
# 将待处理输入排入唯一的转接发送队列;返回 :ready、:queued 或 :stopped。
|
|
87
|
+
# 字面序列暂存潜在前缀,正则序列结合历史匹配;final 为真时不再等待后续字节。
|
|
88
|
+
# @api private
|
|
89
|
+
def self.queue_input(session, buffer, final: false)
|
|
90
|
+
loop do
|
|
91
|
+
sequences = session.sequences.except(:eof)
|
|
92
|
+
history = session.relay_history
|
|
93
|
+
found, regexp_scanned, utf8_regexp = scan_sequences(buffer, sequences, history, final:)
|
|
94
|
+
if found
|
|
95
|
+
result = handle_escape(session, buffer, history, found)
|
|
96
|
+
return result unless result == :ready
|
|
97
|
+
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# 暂存可能构成字面转义的最长后缀,保证 STOP 分两次读取时 ST 不会提前发给子进程。
|
|
102
|
+
held = final ? 0 : hold_literal_prefix(buffer, sequences)
|
|
103
|
+
count = [buffer.bytesize - held, READ_SIZE].min
|
|
104
|
+
session.queue_output(buffer.byteslice(0, count)) if count.positive?
|
|
105
|
+
if regexp_scanned
|
|
106
|
+
trim_history(history, buffer.byteslice(0, count), limit: session.buffer_limit || REGEXP_ESCAPE_HISTORY_LIMIT,
|
|
107
|
+
utf8: utf8_regexp)
|
|
108
|
+
end
|
|
109
|
+
buffer.slice!(0, count)
|
|
110
|
+
return count.positive? ? :queued : :ready
|
|
111
|
+
end
|
|
84
112
|
end
|
|
85
113
|
|
|
86
|
-
Relay
|
|
87
|
-
|
|
114
|
+
# 转义前缀必须交付完才运行回调;有前缀时保存回调,交给 Relay 交付后再执行。
|
|
115
|
+
def self.handle_escape(session, buffer, history, found)
|
|
116
|
+
position, length, callback = found
|
|
117
|
+
if position.positive?
|
|
118
|
+
session.queue_output(buffer.byteslice(0, position))
|
|
119
|
+
buffer.replace(buffer.byteslice((position + length)..))
|
|
120
|
+
history.clear
|
|
121
|
+
# 已确定的转义不能在等待写入后重新匹配;尾部可能在等待期间继续增长。
|
|
122
|
+
session.relay_callback = callback
|
|
123
|
+
return :queued
|
|
124
|
+
end
|
|
125
|
+
buffer.replace(buffer.byteslice([position + length, 0].max..))
|
|
126
|
+
# 转义消费后清除历史,防止继续回调再次匹配同一个转义。
|
|
127
|
+
history.clear
|
|
128
|
+
callback&.call ? :ready : :stopped
|
|
129
|
+
end
|
|
88
130
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# 转发一个会话的待处理缓冲并剔除转义;返回 false 表示应结束整个转接。
|
|
93
|
-
# 字面序列暂存潜在前缀,正则序列结合历史匹配;final 为真时不再等待后续字节。
|
|
94
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 转义匹配、前缀暂存和回调消费属于同一轮扫描。
|
|
95
|
-
def self.relay_buffer(session, buffers, final: false)
|
|
96
|
-
buffer = buffers.fetch(session)
|
|
97
|
-
loop do
|
|
98
|
-
sequences = session.__send__(:sequences).except(:eof)
|
|
99
|
-
history = session.__send__(:relay_history)
|
|
131
|
+
# 单轮正则共用一个组合文本;回调返回后下一轮重新取得规则和历史。
|
|
132
|
+
def self.scan_sequences(buffer, sequences, history, final:)
|
|
100
133
|
text = nil
|
|
101
134
|
utf8_regexp = false
|
|
102
135
|
matches = sequences.filter_map do |key, handler|
|
|
@@ -104,7 +137,7 @@ class Expect
|
|
|
104
137
|
# 仅在本轮扫描复用组合文本;回调可能更换规则或消费缓冲,下一轮必须重新构造。
|
|
105
138
|
text ||= history + buffer
|
|
106
139
|
utf8_regexp ||= key.fixed_encoding? && key.encoding == Encoding::UTF_8
|
|
107
|
-
position = Pattern.new(value: key).locate(text, final:
|
|
140
|
+
position = Pattern.new(value: key).locate(text, final:)
|
|
108
141
|
if position
|
|
109
142
|
offset, length, = position
|
|
110
143
|
raise ArgumentError, "escape regexp must consume at least one byte" if length.zero?
|
|
@@ -117,157 +150,142 @@ class Expect
|
|
|
117
150
|
[position, key.bytesize, handler] if position
|
|
118
151
|
end
|
|
119
152
|
end
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if position.positive?
|
|
123
|
-
if block_given?
|
|
124
|
-
yield buffer.byteslice(0, position)
|
|
125
|
-
buffer.replace(buffer.byteslice((position + length)..))
|
|
126
|
-
history.clear
|
|
127
|
-
# 已确定的转义不能在等待写入后重新匹配;尾部可能在等待期间继续增长。
|
|
128
|
-
session.__send__(:relay_callback=, [callback])
|
|
129
|
-
return :pending
|
|
130
|
-
end
|
|
131
|
-
session.__send__(:propagate, buffer.byteslice(0, position))
|
|
132
|
-
end
|
|
133
|
-
buffer.replace(buffer.byteslice([position + length, 0].max..))
|
|
134
|
-
# 转义消费后清除历史,防止继续回调再次匹配同一个转义。
|
|
135
|
-
history.clear
|
|
136
|
-
return false unless callback&.call
|
|
153
|
+
[matches.min_by(&:first), !text.nil?, utf8_regexp]
|
|
154
|
+
end
|
|
137
155
|
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
# 普通转发和超时排出的字面前缀都属于正则历史,跨次转接时必须接续同一字节流。
|
|
157
|
+
# @api private
|
|
158
|
+
def self.remember_output(session, data)
|
|
159
|
+
regexps = session.sequences.keys.grep(Regexp)
|
|
160
|
+
return if regexps.empty?
|
|
161
|
+
|
|
162
|
+
utf8 = regexps.any? { |regexp| regexp.fixed_encoding? && regexp.encoding == Encoding::UTF_8 }
|
|
163
|
+
trim_history(session.relay_history, data, limit: session.buffer_limit || REGEXP_ESCAPE_HISTORY_LIMIT, utf8:)
|
|
164
|
+
end
|
|
140
165
|
|
|
141
|
-
|
|
166
|
+
# 只暂存可能拼成完整字面转义的最长后缀。
|
|
167
|
+
def self.hold_literal_prefix(buffer, sequences)
|
|
142
168
|
held = 0
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
count = buffer.bytesize - held
|
|
156
|
-
count = [count, READ_SIZE].min if block_given?
|
|
157
|
-
if count.positive?
|
|
158
|
-
data = buffer.byteslice(0, count)
|
|
159
|
-
block_given? ? yield(data) : session.__send__(:propagate, data)
|
|
160
|
-
end
|
|
161
|
-
if text
|
|
162
|
-
history << buffer.byteslice(0, count)
|
|
163
|
-
limit = session.buffer_limit || REGEXP_ESCAPE_HISTORY_LIMIT
|
|
164
|
-
if history.bytesize > limit
|
|
165
|
-
history.replace(history.byteslice(-limit, limit))
|
|
166
|
-
if utf8_regexp
|
|
167
|
-
# 窗口可能从 UTF-8 续字节开始;丢弃不完整的首字符后再交给固定编码正则。
|
|
168
|
-
history.slice!(0) while (byte = history.getbyte(0)) && (0x80..0xBF).cover?(byte)
|
|
169
|
-
end
|
|
169
|
+
sequences.each_key do |key|
|
|
170
|
+
next if key.is_a?(Regexp)
|
|
171
|
+
|
|
172
|
+
[key.bytesize - 1, buffer.bytesize].min.downto(1) do |prefix_length|
|
|
173
|
+
next unless buffer.end_with?(key.byteslice(0, prefix_length))
|
|
174
|
+
|
|
175
|
+
held = [held, prefix_length].max
|
|
176
|
+
break
|
|
170
177
|
end
|
|
171
178
|
end
|
|
172
|
-
|
|
173
|
-
return block_given? && count.positive? ? :pending : true
|
|
179
|
+
held
|
|
174
180
|
end
|
|
175
|
-
end
|
|
176
181
|
|
|
177
|
-
|
|
182
|
+
# 已转发历史有界保留,固定 UTF-8 正则不能从续字节开始匹配。
|
|
183
|
+
def self.trim_history(history, data, limit:, utf8:)
|
|
184
|
+
history << data
|
|
185
|
+
return unless history.bytesize > limit
|
|
178
186
|
|
|
179
|
-
|
|
187
|
+
history.replace(history.byteslice(-limit, limit))
|
|
188
|
+
return unless utf8
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return input if input.is_a?(Expect)
|
|
190
|
+
history.slice!(0) while (byte = history.getbyte(0)) && (0x80..0xBF).cover?(byte)
|
|
191
|
+
end
|
|
184
192
|
|
|
185
|
-
|
|
186
|
-
@interact_inputs.delete_if { |io, session| io.closed? || session.closed? }
|
|
187
|
-
@interact_inputs[input] ||= Expect.open(input)
|
|
188
|
-
end
|
|
193
|
+
private_class_method :handle_escape, :scan_sequences, :hold_literal_prefix, :trim_history
|
|
189
194
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
RelayWriter.new(target, data)
|
|
195
|
+
# 为当前数据块冻结目标选择并各建一个发送游标;此后修改 outputs 只影响后续数据。
|
|
196
|
+
# 调用方须先排空旧游标;显示转换也只做一次,短写重试时不能重复转换 CRLF。
|
|
197
|
+
# @api private
|
|
198
|
+
def queue_output(data)
|
|
199
|
+
@pending_writes = @outputs.map do |target|
|
|
200
|
+
if target.is_a?(InteractOutput)
|
|
201
|
+
RelayWriter.new(target.target, target.render(data))
|
|
202
|
+
else
|
|
203
|
+
RelayWriter.new(target, data)
|
|
204
|
+
end
|
|
201
205
|
end
|
|
202
206
|
end
|
|
203
|
-
end
|
|
204
207
|
|
|
205
|
-
|
|
208
|
+
public
|
|
209
|
+
|
|
210
|
+
# 仅供转接内部保存和恢复注册表,避免公开可变 Hash 绕过 on_sequence 的校验。
|
|
211
|
+
# @api private
|
|
212
|
+
attr_accessor :sequences
|
|
213
|
+
# 让同步写入的背压读取遵守当前转接的数据所有权,退出后恢复普通匹配缓冲。
|
|
214
|
+
# @api private
|
|
215
|
+
attr_accessor :interaction_buffer
|
|
216
|
+
# 活跃 Relay 的所有权 token;只限制同源递归转接,不限制转义回调中的 Matcher。
|
|
217
|
+
# @api private
|
|
218
|
+
attr_accessor :relay_owner
|
|
219
|
+
# 只有裁剪、替换和消费才改变代次;同一代次只会追加,供字面扫描复用已排除的前缀。
|
|
220
|
+
# @api private
|
|
221
|
+
attr_reader :buffer_generation
|
|
222
|
+
# 待交付游标随源会话保存,Relay 的超时或异常退出不会丢失各目标已经写出的进度。
|
|
223
|
+
# @api private
|
|
224
|
+
attr_reader :pending_writes
|
|
225
|
+
# 等待前缀交付的动作;缺省停止动作也是真正的 callable,不使用数组包装状态。
|
|
226
|
+
# @api private
|
|
227
|
+
attr_accessor :relay_callback
|
|
228
|
+
|
|
229
|
+
# 历史属于产生它的转义规则;同规则重入继续匹配,换规则不能重放已转发输入。
|
|
230
|
+
# @api private
|
|
231
|
+
def relay_history
|
|
232
|
+
sequences = @sequences.except(:eof)
|
|
233
|
+
@relay_history = "".b unless @relay_history_sequences == sequences
|
|
234
|
+
@relay_history_sequences = sequences
|
|
235
|
+
@relay_history
|
|
236
|
+
end
|
|
206
237
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
238
|
+
# 转接尚未处理的输入不能被匹配窗口上限裁掉;下次 expect 会重新应用该上限。
|
|
239
|
+
# @api private
|
|
240
|
+
def restore_relay_buffer(buffer)
|
|
241
|
+
@buffer = buffer + @buffer
|
|
242
|
+
@buffer_generation += 1
|
|
243
|
+
end
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
state = [io, io.console_mode]
|
|
213
|
-
io.raw!
|
|
214
|
-
state
|
|
215
|
-
rescue Exception # rubocop:disable Lint/RescueException -- 中断时也恢复可能已部分修改的终端。
|
|
216
|
-
restore_interact_terminal(state)
|
|
217
|
-
raise
|
|
218
|
-
end
|
|
245
|
+
private
|
|
219
246
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
247
|
+
# 同一输入 IO 重用一个借用会话,以保留上次接管预读的尾部并避免不断积累包装器。
|
|
248
|
+
def interact_source(input)
|
|
249
|
+
return input if input.is_a?(Session)
|
|
223
250
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
251
|
+
@interact_inputs.delete_if { |io, session| io.closed? || session.closed? }
|
|
252
|
+
@interact_inputs[input] ||= Expect.open(input)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# 只有 interact 知道哪个流是本地键盘;通用 interconnect 不修改终端模式。
|
|
256
|
+
def prepare_interact_terminal(source)
|
|
257
|
+
return unless source.tty?
|
|
227
258
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
259
|
+
state = nil
|
|
260
|
+
Cleanup.on_failure(-> { restore_interact_terminal(state) }) do
|
|
261
|
+
io = source.to_io
|
|
262
|
+
state = [io, io.console_mode]
|
|
263
|
+
io.raw!
|
|
264
|
+
state
|
|
265
|
+
end
|
|
266
|
+
end
|
|
232
267
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
268
|
+
# 还原 prepare 保存的完整终端模式;没有切换过或句柄已关闭时无需恢复。
|
|
269
|
+
def restore_interact_terminal(state)
|
|
270
|
+
return unless state
|
|
236
271
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
output
|
|
241
|
-
end
|
|
272
|
+
io, mode = state
|
|
273
|
+
io.console_mode = mode unless io.closed?
|
|
274
|
+
end
|
|
242
275
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
# 活跃 Relay 的所有权 token;只限制同源递归转接,不限制转义回调中的 Matcher。
|
|
252
|
-
attr_accessor :relay_owner
|
|
253
|
-
# 只有裁剪、替换和消费才改变代次;同一代次只会追加,供字面扫描复用已排除的前缀。
|
|
254
|
-
attr_reader :buffer_generation
|
|
255
|
-
# 待交付游标随源会话保存,Relay 的超时或异常退出不会丢失各目标已经写出的进度。
|
|
256
|
-
attr_reader :relay_outputs
|
|
257
|
-
# 数组包装区分“没有待执行回调”与“已识别无处理器的停止转义”,前缀交付后只派发一次。
|
|
258
|
-
attr_accessor :relay_callback
|
|
259
|
-
|
|
260
|
-
# 历史属于产生它的转义规则;同规则重入继续匹配,换规则不能重放已转发输入。
|
|
261
|
-
def relay_history
|
|
262
|
-
sequences = @sequences.except(:eof)
|
|
263
|
-
@relay_history = "".b unless @relay_history_sequences == sequences
|
|
264
|
-
@relay_history_sequences = sequences
|
|
265
|
-
@relay_history
|
|
266
|
-
end
|
|
276
|
+
# 仅为已切为 raw 的同一个本地终端补齐换行,文件、管道和其他终端保留原字节。
|
|
277
|
+
def interact_display(source, output, terminal_state)
|
|
278
|
+
@interact_output = nil unless terminal_state && @interact_output&.target.equal?(output)
|
|
279
|
+
return output unless terminal_state
|
|
280
|
+
|
|
281
|
+
target = output.respond_to?(:to_io) ? output.to_io : output
|
|
282
|
+
return output unless target.respond_to?(:tty?) && target.tty?
|
|
283
|
+
return output unless source.to_io.stat.rdev == target.stat.rdev
|
|
267
284
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
285
|
+
# 同一输出的 CRLF 可能跨越两次接管;只缓存最近目标,避免长期持有旧终端。
|
|
286
|
+
@interact_output ||= InteractOutput.new(output)
|
|
287
|
+
rescue IOError, SystemCallError
|
|
288
|
+
output
|
|
289
|
+
end
|
|
272
290
|
end
|
|
273
291
|
end
|