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