network-infra-utility 0.3.0 → 0.6.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/.gitignore +21 -0
- data/GUIDE.md +212 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +70 -0
- data/Rakefile +1 -1
- data/bin/dns-query +836 -0
- data/bin/geo-doc +135 -0
- data/bin/geo-get +1 -1
- data/bin/geo-update +429 -0
- data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
- data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
- data/document/Geo/345/221/275/344/273/244/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +441 -0
- data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
- data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
- data/document/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/344/275/277/347/224/250/346/226/271/346/263/225.md +764 -0
- data/network-infra-utility.gemspec +4 -2
- data/network.rb +3 -1
- data/service/geodb/geodb.rb +278 -1
- data/service/ssh/README.md +955 -0
- data/service/ssh/bin/ssh-client +198 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/345/212/237/350/203/275/351/234/200/346/261/202/346/226/207/346/241/243.md +292 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/257/246/347/273/206/350/256/276/350/256/241/346/226/207/346/241/243.md +1521 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/275/257/344/273/266/350/256/276/350/256/241/346/226/207/346/241/243.md +2493 -0
- data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
- data/service/ssh/ext/ssh_core/config/sys.config +0 -0
- data/service/ssh/ext/ssh_core/config/vm.args +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/rebar.config +24 -0
- data/service/ssh/ext/ssh_core/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
- data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
- data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
- data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
- data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +117 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
- data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
- data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
- data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
- data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
- data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
- data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
- data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
- data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
- data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
- data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
- data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
- data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
- data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
- data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
- data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
- data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
- data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
- data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
- data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
- data/support/basic/as_num.rb +221 -0
- data/support/basic/mac_address.rb +281 -0
- data/version.rb +1 -1
- metadata +142 -1
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NetworkInfraUtility
|
|
4
|
+
module SSH
|
|
5
|
+
module Terminal
|
|
6
|
+
# ANSI/xterm 转义序列解析器。
|
|
7
|
+
# 纯解析器,无状态副作用,只调 Emulator 回调(LLD §7.6)。
|
|
8
|
+
#
|
|
9
|
+
# 构造签名(LLD 修正版):AnsiParser.new(emulator)
|
|
10
|
+
# 传入 Emulator 自身,AnsiParser 回调 Emulator 方法,
|
|
11
|
+
# Emulator 再委托给 Screen。
|
|
12
|
+
#
|
|
13
|
+
# 覆盖目标:≥ 95% ANSI(FR-TERM-001 ④),参考 vte/tty 转义表。
|
|
14
|
+
# DoD(LLD §11.2):ANSI 覆盖率 ≥ 95%(用 vttest 子集验证)
|
|
15
|
+
#
|
|
16
|
+
# 支持的转义类别:
|
|
17
|
+
# - CSI (Control Sequence Introducer) \e[...X
|
|
18
|
+
# - SGR (Select Graphic Rendition) \e[...m
|
|
19
|
+
# - OSC (Operating System Command) \e]...\x07
|
|
20
|
+
# - DCS (Device Control String) \eP...\x9c
|
|
21
|
+
# - 单字符控制序列 \eX
|
|
22
|
+
class AnsiParser
|
|
23
|
+
# 解析状态机
|
|
24
|
+
# :ground — 普通文本,逐字符输出
|
|
25
|
+
# :escape — 收到 ESC (0x1b),等待中间字节
|
|
26
|
+
# :csi_entry — 收到 ESC [,收集参数
|
|
27
|
+
# :csi_param — 收集 CSI 参数(数字和分号)
|
|
28
|
+
# :csi_intermediate — 收集 CSI 中间字节 (空格到/)
|
|
29
|
+
# :osc_string — OSC 字符串,直到 BEL (0x07) 或 ST
|
|
30
|
+
# :dcs_string — DCS 字符串,直到 ST
|
|
31
|
+
# :charset — 字符集指定(ESC ( B 等)
|
|
32
|
+
|
|
33
|
+
ESC = "\e".freeze
|
|
34
|
+
CSI = "[".freeze
|
|
35
|
+
BEL = "\x07".freeze
|
|
36
|
+
ST = "\x9c".freeze # String Terminator (7-bit: ESC \)
|
|
37
|
+
DEL = 0x7f
|
|
38
|
+
|
|
39
|
+
# SGR 前景色映射(标准 8 色)
|
|
40
|
+
SGR_FG = {
|
|
41
|
+
0 => :reset, 30 => :black, 31 => :red, 32 => :green,
|
|
42
|
+
33 => :yellow, 34 => :blue, 35 => :magenta, 36 => :cyan, 37 => :white
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# SGR 背景色映射
|
|
46
|
+
SGR_BG = {
|
|
47
|
+
40 => :black, 41 => :red, 42 => :green, 43 => :yellow,
|
|
48
|
+
44 => :blue, 45 => :magenta, 46 => :cyan, 47 => :white
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# SGR 扩展前景色(高亮 8 色)
|
|
52
|
+
SGR_FG_BRIGHT = {
|
|
53
|
+
90 => :black, 91 => :red, 92 => :green, 93 => :yellow,
|
|
54
|
+
94 => :blue, 95 => :magenta, 96 => :cyan, 97 => :white
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
attr_reader :state
|
|
58
|
+
|
|
59
|
+
# @param emulator [Terminal::Emulator] 回调目标
|
|
60
|
+
def initialize(emulator)
|
|
61
|
+
@emu = emulator
|
|
62
|
+
@state = :ground
|
|
63
|
+
@params = []
|
|
64
|
+
@current_param = +""
|
|
65
|
+
@intermediates = +""
|
|
66
|
+
@string_buffer = +""
|
|
67
|
+
@charset = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 喂入原始字节流,驱动状态机
|
|
71
|
+
# @param data [String] 原始字节
|
|
72
|
+
def feed(data)
|
|
73
|
+
data.each_byte do |byte|
|
|
74
|
+
process_byte(byte)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 重置解析器状态
|
|
79
|
+
def reset
|
|
80
|
+
@state = :ground
|
|
81
|
+
@params = []
|
|
82
|
+
@current_param = +""
|
|
83
|
+
@intermediates = +""
|
|
84
|
+
@string_buffer = +""
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def process_byte(byte)
|
|
90
|
+
char = byte.chr(Encoding::UTF_8)
|
|
91
|
+
case @state
|
|
92
|
+
when :ground
|
|
93
|
+
process_ground(byte, char)
|
|
94
|
+
when :escape
|
|
95
|
+
process_escape(byte, char)
|
|
96
|
+
when :csi_entry, :csi_param
|
|
97
|
+
process_csi(byte, char)
|
|
98
|
+
when :csi_intermediate
|
|
99
|
+
process_csi_intermediate(byte, char)
|
|
100
|
+
when :osc_string
|
|
101
|
+
process_osc(byte, char)
|
|
102
|
+
when :dcs_string
|
|
103
|
+
process_dcs(byte, char)
|
|
104
|
+
when :charset
|
|
105
|
+
process_charset(byte, char)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ---- :ground ----
|
|
110
|
+
def process_ground(byte, char)
|
|
111
|
+
case byte
|
|
112
|
+
when 0x1b # ESC
|
|
113
|
+
@state = :escape
|
|
114
|
+
when 0x0d # CR
|
|
115
|
+
@emu.cursor_to(0, @emu.cursor_y)
|
|
116
|
+
when 0x0a, 0x0b, 0x0c # LF, VT, FF
|
|
117
|
+
@emu.newline
|
|
118
|
+
when 0x08 # BS
|
|
119
|
+
@emu.backspace
|
|
120
|
+
when 0x09 # HT
|
|
121
|
+
@emu.tab
|
|
122
|
+
when 0x07 # BEL
|
|
123
|
+
@emu.bell
|
|
124
|
+
when 0x00 # NUL — 忽略
|
|
125
|
+
# noop
|
|
126
|
+
when DEL # DEL — 忽略
|
|
127
|
+
# noop
|
|
128
|
+
else
|
|
129
|
+
if byte >= 0x20 && byte < 0x7f
|
|
130
|
+
# 可打印 ASCII
|
|
131
|
+
@emu.put_char(char)
|
|
132
|
+
elsif byte >= 0x80
|
|
133
|
+
# UTF-8 / 高位字节 — 直接当作字符
|
|
134
|
+
@emu.put_char(char)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# ---- :escape ----
|
|
140
|
+
def process_escape(byte, char)
|
|
141
|
+
case char
|
|
142
|
+
when CSI # [
|
|
143
|
+
@params = []
|
|
144
|
+
@current_param = +""
|
|
145
|
+
@intermediates = +""
|
|
146
|
+
@state = :csi_entry
|
|
147
|
+
when "]" # OSC
|
|
148
|
+
@string_buffer = +""
|
|
149
|
+
@state = :osc_string
|
|
150
|
+
when "P" # DCS
|
|
151
|
+
@string_buffer = +""
|
|
152
|
+
@state = :dcs_string
|
|
153
|
+
when "(" # G0 字符集
|
|
154
|
+
@charset = :g0
|
|
155
|
+
@state = :charset
|
|
156
|
+
when ")" # G1 字符集
|
|
157
|
+
@charset = :g1
|
|
158
|
+
@state = :charset
|
|
159
|
+
when "M" # Reverse Index (RI)
|
|
160
|
+
@emu.scroll_down(1)
|
|
161
|
+
@state = :ground
|
|
162
|
+
when "D" # Index (IND)
|
|
163
|
+
@emu.newline
|
|
164
|
+
@state = :ground
|
|
165
|
+
when "E" # Next Line (NEL)
|
|
166
|
+
@emu.cursor_to(0, @emu.cursor_y + 1)
|
|
167
|
+
@state = :ground
|
|
168
|
+
when "7" # DECSC — 保存光标
|
|
169
|
+
@state = :ground
|
|
170
|
+
# V2.0: save_cursor
|
|
171
|
+
when "8" # DECRC — 恢复光标
|
|
172
|
+
@state = :ground
|
|
173
|
+
# V2.0: restore_cursor
|
|
174
|
+
when "c" # RIS — 全部重置
|
|
175
|
+
@emu.clear_screen(:full)
|
|
176
|
+
@state = :ground
|
|
177
|
+
when "=" # 应用键盘模式
|
|
178
|
+
@state = :ground
|
|
179
|
+
when ">" # 数字键盘模式
|
|
180
|
+
@state = :ground
|
|
181
|
+
else
|
|
182
|
+
# 未知转义,回到 ground
|
|
183
|
+
@state = :ground
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# ---- :csi_entry / :csi_param ----
|
|
188
|
+
def process_csi(byte, char)
|
|
189
|
+
if byte >= 0x30 && byte <= 0x39 # 0-9
|
|
190
|
+
@current_param << char
|
|
191
|
+
@state = :csi_param
|
|
192
|
+
elsif byte == 0x3b # ;
|
|
193
|
+
@params << (@current_param.empty? ? 0 : @current_param.to_i)
|
|
194
|
+
@current_param = +""
|
|
195
|
+
@state = :csi_param
|
|
196
|
+
elsif byte == 0x3f # ? — 私有参数前缀
|
|
197
|
+
@intermediates << char
|
|
198
|
+
@state = :csi_param
|
|
199
|
+
elsif byte >= 0x3c && byte <= 0x3e # < = > — 私有参数标记
|
|
200
|
+
@intermediates << char
|
|
201
|
+
@state = :csi_param
|
|
202
|
+
elsif byte >= 0x20 && byte <= 0x2f # 空格到/ — 中间字节
|
|
203
|
+
@intermediates << char
|
|
204
|
+
@state = :csi_intermediate
|
|
205
|
+
elsif byte >= 0x40 && byte <= 0x7e # 终止字节
|
|
206
|
+
@params << (@current_param.empty? ? 0 : @current_param.to_i) unless @current_param.empty?
|
|
207
|
+
dispatch_csi(char)
|
|
208
|
+
@state = :ground
|
|
209
|
+
else
|
|
210
|
+
@state = :ground
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# ---- :csi_intermediate ----
|
|
215
|
+
def process_csi_intermediate(byte, char)
|
|
216
|
+
if byte >= 0x20 && byte <= 0x2f
|
|
217
|
+
@intermediates << char
|
|
218
|
+
elsif byte >= 0x40 && byte <= 0x7e
|
|
219
|
+
dispatch_csi(char)
|
|
220
|
+
@state = :ground
|
|
221
|
+
else
|
|
222
|
+
@state = :ground
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# ---- :osc_string ----
|
|
227
|
+
def process_osc(byte, char)
|
|
228
|
+
if byte == 0x07 # BEL — 终止
|
|
229
|
+
handle_osc(@string_buffer)
|
|
230
|
+
@state = :ground
|
|
231
|
+
elsif byte == 0x1b
|
|
232
|
+
# 可能是 ST (ESC \)
|
|
233
|
+
@string_buffer << char
|
|
234
|
+
# 简化:下一个字节如果是 \ 就结束
|
|
235
|
+
elsif byte == 0x5c && @string_buffer.end_with?("\e")
|
|
236
|
+
@string_buffer = @string_buffer[0..-2]
|
|
237
|
+
handle_osc(@string_buffer)
|
|
238
|
+
@state = :ground
|
|
239
|
+
else
|
|
240
|
+
@string_buffer << char
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# ---- :dcs_string ----
|
|
245
|
+
def process_dcs(byte, char)
|
|
246
|
+
if byte == 0x9c # ST
|
|
247
|
+
handle_dcs(@string_buffer)
|
|
248
|
+
@state = :ground
|
|
249
|
+
elsif byte == 0x1b
|
|
250
|
+
@string_buffer << char
|
|
251
|
+
elsif byte == 0x5c && @string_buffer.end_with?("\e")
|
|
252
|
+
@string_buffer = @string_buffer[0..-2]
|
|
253
|
+
handle_dcs(@string_buffer)
|
|
254
|
+
@state = :ground
|
|
255
|
+
else
|
|
256
|
+
@string_buffer << char
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# ---- :charset ----
|
|
261
|
+
def process_charset(byte, char)
|
|
262
|
+
# 字符集指定(如 B = US-ASCII),V1.0 忽略
|
|
263
|
+
@state = :ground
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# ---- CSI 分发 ----
|
|
267
|
+
def dispatch_csi(final_char)
|
|
268
|
+
p = @params
|
|
269
|
+
case final_char
|
|
270
|
+
when "A" # CUU — 光标上移
|
|
271
|
+
@emu.cursor_to(@emu.cursor_x, @emu.cursor_y - (p[0] || 1))
|
|
272
|
+
when "B" # CUD — 光标下移
|
|
273
|
+
@emu.cursor_to(@emu.cursor_x, @emu.cursor_y + (p[0] || 1))
|
|
274
|
+
when "C" # CUF — 光标右移
|
|
275
|
+
@emu.cursor_to(@emu.cursor_x + (p[0] || 1), @emu.cursor_y)
|
|
276
|
+
when "D" # CUB — 光标左移
|
|
277
|
+
@emu.cursor_to(@emu.cursor_x - (p[0] || 1), @emu.cursor_y)
|
|
278
|
+
when "H", "f" # CUP / HVP — 光标定位
|
|
279
|
+
row = (p[0] || 1) - 1
|
|
280
|
+
col = (p[1] || 1) - 1
|
|
281
|
+
@emu.cursor_to(col, row)
|
|
282
|
+
when "J" # ED — 擦除显示
|
|
283
|
+
mode = p[0] || 0
|
|
284
|
+
@emu.clear_screen(mode.zero? ? :below : (mode == 1 ? :above : :full))
|
|
285
|
+
when "K" # EL — 擦除行
|
|
286
|
+
mode = p[0] || 0
|
|
287
|
+
@emu.clear_line(mode.zero? ? :right : (mode == 1 ? :left : :full))
|
|
288
|
+
when "m" # SGR — 图形属性设置
|
|
289
|
+
dispatch_sgr(p)
|
|
290
|
+
when "r" # DECSTBM — 设置滚动区
|
|
291
|
+
top = (p[0] || 1) - 1
|
|
292
|
+
bottom = (p[1] || 1) - 1
|
|
293
|
+
@emu.set_scroll_region(top, bottom)
|
|
294
|
+
when "h" # SM — 设置模式
|
|
295
|
+
handle_set_mode(p, true)
|
|
296
|
+
when "l" # RM — 重置模式
|
|
297
|
+
handle_set_mode(p, false)
|
|
298
|
+
when "n" # DSR — 设备状态报告
|
|
299
|
+
handle_dsr(p)
|
|
300
|
+
when "S" # SU — 向上滚动
|
|
301
|
+
@emu.scroll_up(p[0] || 1)
|
|
302
|
+
when "T" # SD — 向下滚动
|
|
303
|
+
@emu.scroll_down(p[0] || 1)
|
|
304
|
+
when "L" # IL — 插入行
|
|
305
|
+
handle_insert_lines(p[0] || 1)
|
|
306
|
+
when "M" # DL — 删除行
|
|
307
|
+
handle_delete_lines(p[0] || 1)
|
|
308
|
+
when "P" # DCH — 删除字符
|
|
309
|
+
handle_delete_chars(p[0] || 1)
|
|
310
|
+
when "@" # ICH — 插入字符
|
|
311
|
+
handle_insert_chars(p[0] || 1)
|
|
312
|
+
when "X" # ECH — 擦除字符
|
|
313
|
+
handle_erase_chars(p[0] || 1)
|
|
314
|
+
when "d" # VPA — 垂直位置
|
|
315
|
+
@emu.cursor_to(@emu.cursor_x, (p[0] || 1) - 1)
|
|
316
|
+
when "G" # CHA — 水平位置
|
|
317
|
+
@emu.cursor_to((p[0] || 1) - 1, @emu.cursor_y)
|
|
318
|
+
else
|
|
319
|
+
# 未覆盖的 CSI 序列,骨架阶段忽略
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# ---- SGR 分发 ----
|
|
324
|
+
def dispatch_sgr(params)
|
|
325
|
+
params = [0] if params.empty?
|
|
326
|
+
|
|
327
|
+
params.each_with_index do |code, _i|
|
|
328
|
+
case code
|
|
329
|
+
when 0
|
|
330
|
+
@emu.reset_style
|
|
331
|
+
when 1
|
|
332
|
+
@emu.set_style(bold: true)
|
|
333
|
+
when 2
|
|
334
|
+
@emu.set_style(dim: true)
|
|
335
|
+
when 3
|
|
336
|
+
@emu.set_style(italic: true)
|
|
337
|
+
when 4
|
|
338
|
+
@emu.set_style(underline: true)
|
|
339
|
+
when 5, 6
|
|
340
|
+
@emu.set_style(blink: true)
|
|
341
|
+
when 7
|
|
342
|
+
@emu.set_style(reverse: true)
|
|
343
|
+
when 22
|
|
344
|
+
@emu.set_style(bold: false, dim: false)
|
|
345
|
+
when 23
|
|
346
|
+
@emu.set_style(italic: false)
|
|
347
|
+
when 24
|
|
348
|
+
@emu.set_style(underline: false)
|
|
349
|
+
when 27
|
|
350
|
+
@emu.set_style(reverse: false)
|
|
351
|
+
when 38
|
|
352
|
+
# 扩展前景色(38;5;n 或 38;2;r;g;b),骨架阶段占位
|
|
353
|
+
@emu.set_style(fg_ext: true)
|
|
354
|
+
when 48
|
|
355
|
+
# 扩展背景色,骨架阶段占位
|
|
356
|
+
@emu.set_style(bg_ext: true)
|
|
357
|
+
when 39
|
|
358
|
+
@emu.set_style(fg: nil)
|
|
359
|
+
when 49
|
|
360
|
+
@emu.set_style(bg: nil)
|
|
361
|
+
when *SGR_FG.keys
|
|
362
|
+
@emu.set_style(fg: SGR_FG[code])
|
|
363
|
+
when *SGR_BG.keys
|
|
364
|
+
@emu.set_style(bg: SGR_BG[code])
|
|
365
|
+
when *SGR_FG_BRIGHT.keys
|
|
366
|
+
@emu.set_style(fg: SGR_FG_BRIGHT[code], bright: true)
|
|
367
|
+
else
|
|
368
|
+
# 未覆盖的 SGR 码,骨架阶段忽略
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# ---- 模式设置 (DECSET/DECRST) ----
|
|
374
|
+
def handle_set_mode(params, enable)
|
|
375
|
+
params.each do |code|
|
|
376
|
+
case code
|
|
377
|
+
when 1049, 1047 # Alt screen
|
|
378
|
+
enable ? @emu.enter_alt_screen : @emu.exit_alt_screen
|
|
379
|
+
when 1048 # 保存/恢复光标
|
|
380
|
+
# V2.0: save/restore cursor
|
|
381
|
+
else
|
|
382
|
+
# 未覆盖的模式,骨架阶段忽略
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# ---- DSR 设备状态报告 ----
|
|
388
|
+
def handle_dsr(params)
|
|
389
|
+
case params[0]
|
|
390
|
+
when 5 # 状态报告
|
|
391
|
+
# V2.0: send "ESC[0n" (OK) back to terminal
|
|
392
|
+
when 6 # 光标位置报告
|
|
393
|
+
# V2.0: send "ESC[<row>;<col>R" back
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# ---- OSC 处理 ----
|
|
398
|
+
def handle_osc(string)
|
|
399
|
+
# OSC 序列如 "0;title" (窗口标题) 或 "4;0;rgb:00/00/00" (调色板)
|
|
400
|
+
# V1.0 骨架阶段忽略 OSC
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# ---- DCS 处理 ----
|
|
404
|
+
def handle_dcs(string)
|
|
405
|
+
# DCS 序列用于 DECRQSS 等查询
|
|
406
|
+
# V1.0 骨架阶段忽略 DCS
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# ---- 行/字符操作占位 ----
|
|
410
|
+
# 以下操作在 Screen 上的对应方法待具体实现阶段补充
|
|
411
|
+
|
|
412
|
+
def handle_insert_lines(n)
|
|
413
|
+
# IL — 在光标位置插入 n 行
|
|
414
|
+
# 骨架阶段:简化为滚动处理
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def handle_delete_lines(n)
|
|
418
|
+
# DL — 在光标位置删除 n 行
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def handle_delete_chars(n)
|
|
422
|
+
# DCH — 删除 n 个字符
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def handle_insert_chars(n)
|
|
426
|
+
# ICH — 插入 n 个空字符
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def handle_erase_chars(n)
|
|
430
|
+
# ECH — 擦除 n 个字符(用空格替换,不移动)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NetworkInfraUtility
|
|
4
|
+
module SSH
|
|
5
|
+
module Terminal
|
|
6
|
+
# 回滚区 + 当前屏幕快照,提供 search/export。
|
|
7
|
+
# 写入由 Screen 触发(滚动时旧行进入回滚区)。
|
|
8
|
+
# 对应需求 FR-TERM-007:默认 ≥ 10000 行,可配置 1000–100000。
|
|
9
|
+
class Buffer
|
|
10
|
+
attr_reader :scrollback_lines, :max_lines
|
|
11
|
+
attr_accessor :screen
|
|
12
|
+
|
|
13
|
+
def initialize(max_lines: 10_000)
|
|
14
|
+
@max_lines = max_lines
|
|
15
|
+
@scrollback_lines = [] # 已滚出屏幕的历史行
|
|
16
|
+
@screen = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 绑定 Screen,接收滚动事件
|
|
20
|
+
def attach_screen(screen)
|
|
21
|
+
@screen = screen
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Screen 滚动时调用,将被推出的行加入回滚区
|
|
25
|
+
def push_scrollback(line_text)
|
|
26
|
+
@scrollback_lines << line_text
|
|
27
|
+
trim
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 全量行数(回滚 + 屏幕)
|
|
31
|
+
def total_lines
|
|
32
|
+
@scrollback_lines.size + (@screen&.rows || 0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# 搜索全部内容(回滚 + 屏幕)
|
|
36
|
+
# @param pattern [String] 正则表达式
|
|
37
|
+
# @return [Array<Match>]
|
|
38
|
+
def search(pattern)
|
|
39
|
+
regex = Regexp.new(pattern, Regexp::IGNORECASE)
|
|
40
|
+
matches = []
|
|
41
|
+
all_lines.each_with_index do |line, idx|
|
|
42
|
+
line.scan(regex) do
|
|
43
|
+
m = Regexp.last_match
|
|
44
|
+
matches << Match.new(line: idx, start: m.begin(0), end_: m.end(0), text: m[0])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
matches
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 导出纯文本
|
|
51
|
+
def to_text
|
|
52
|
+
all_lines.join("\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 设置最大回滚行数
|
|
56
|
+
def max_lines=(n)
|
|
57
|
+
@max_lines = n
|
|
58
|
+
trim
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def all_lines
|
|
64
|
+
@scrollback_lines + (@screen ? @screen.lines.map(&:to_s) : [])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def trim
|
|
68
|
+
return if @scrollback_lines.size <= @max_lines
|
|
69
|
+
|
|
70
|
+
@scrollback_lines = @scrollback_lines[-@max_lines..]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 搜索匹配结果
|
|
75
|
+
Match = Struct.new(:line, :start, :end_, :text)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "forwardable"
|
|
5
|
+
require_relative "screen"
|
|
6
|
+
require_relative "buffer"
|
|
7
|
+
require_relative "theme"
|
|
8
|
+
require_relative "logger"
|
|
9
|
+
require_relative "ansi_parser"
|
|
10
|
+
|
|
11
|
+
module NetworkInfraUtility
|
|
12
|
+
module SSH
|
|
13
|
+
module Terminal
|
|
14
|
+
# ANSI/xterm 转义序列解析与终端状态维护。
|
|
15
|
+
# 四层分离(LLD §7.6):
|
|
16
|
+
# AnsiParser — 纯解析器,解析 ANSI 转义序列,回调 Emulator 方法
|
|
17
|
+
# Emulator — 终端状态协调器,接收 AnsiParser 回调,委托给 Screen
|
|
18
|
+
# Screen — 屏幕状态(行/光标/滚动区/alt buffer/字符属性)
|
|
19
|
+
# Buffer — 回滚区 + 快照 + search/export
|
|
20
|
+
class Emulator
|
|
21
|
+
extend Forwardable
|
|
22
|
+
|
|
23
|
+
attr_reader :conn_id, :channel_id, :screen, :buffer, :theme, :logger
|
|
24
|
+
|
|
25
|
+
def initialize(client, conn_id, ch_id, cols, rows, theme: nil)
|
|
26
|
+
@client = client
|
|
27
|
+
@conn_id = conn_id
|
|
28
|
+
@channel_id = ch_id
|
|
29
|
+
@cols = cols
|
|
30
|
+
@rows = rows
|
|
31
|
+
@screen = Screen.new(cols, rows)
|
|
32
|
+
@buffer = Buffer.new(max_lines: 10_000)
|
|
33
|
+
@buffer.attach_screen(@screen)
|
|
34
|
+
@theme = Theme.load(theme || "default")
|
|
35
|
+
@parser = AnsiParser.new(self)
|
|
36
|
+
@logger = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 输入从 SSH 收到的原始字节
|
|
40
|
+
def feed(data)
|
|
41
|
+
@parser.feed(data)
|
|
42
|
+
@logger&.write(data)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 发送数据到 SSH 通道
|
|
46
|
+
def send(data)
|
|
47
|
+
@client.ipc.call("channel.send", {
|
|
48
|
+
id: @channel_id,
|
|
49
|
+
data: Base64.strict_encode64(data)
|
|
50
|
+
})
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 发送一行(带 \r)
|
|
54
|
+
def puts(text)
|
|
55
|
+
send("#{text}\r")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 窗口大小变更
|
|
59
|
+
def resize(cols, rows)
|
|
60
|
+
@screen.resize(cols, rows)
|
|
61
|
+
@client.ipc.call("channel.window_change", {
|
|
62
|
+
id: @channel_id, cols: cols, rows: rows
|
|
63
|
+
})
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def_delegators :@screen, :cursor_x, :cursor_y
|
|
67
|
+
|
|
68
|
+
# 设置配色主题(FR-TERM-002)
|
|
69
|
+
def theme=(theme_name)
|
|
70
|
+
@theme = Theme.load(theme_name)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 搜索缓冲区内容(FR-TERM-005, FR-TERM-007)
|
|
74
|
+
def search(pattern)
|
|
75
|
+
@buffer.search(pattern)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 导出缓冲区为文本
|
|
79
|
+
def export(path = nil)
|
|
80
|
+
text = @buffer.to_text
|
|
81
|
+
File.write(path, text) if path
|
|
82
|
+
text
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 启动会话日志(FR-TERM-006)
|
|
86
|
+
def start_logging(path, max_size: 100 * 1024 * 1024, rotate: 10)
|
|
87
|
+
@logger = Logger.new(path, max_size: max_size, rotate: rotate)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stop_logging
|
|
91
|
+
@logger&.close
|
|
92
|
+
@logger = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# ---- AnsiParser 回调 ----
|
|
96
|
+
|
|
97
|
+
def put_char(ch, x = nil, y = nil, style = {})
|
|
98
|
+
@screen.put_char(ch, x || @screen.cursor_x, y || @screen.cursor_y, style)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def cursor_to(x, y)
|
|
102
|
+
@screen.cursor_to(x, y)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def newline
|
|
106
|
+
@screen.newline
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def backspace
|
|
110
|
+
@screen.backspace
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def tab
|
|
114
|
+
@screen.tab
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def clear_screen(mode = :full)
|
|
118
|
+
@screen.clear(mode)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def clear_line(mode = :full)
|
|
122
|
+
@screen.clear_line(mode)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def scroll_up(n = 1)
|
|
126
|
+
@screen.scroll_up(n)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def scroll_down(n = 1)
|
|
130
|
+
@screen.scroll_down(n)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def set_scroll_region(top, bottom)
|
|
134
|
+
@screen.set_scroll_region(top, bottom)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def enter_alt_screen
|
|
138
|
+
@screen.enter_alt
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def exit_alt_screen
|
|
142
|
+
@screen.exit_alt
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def set_style(style)
|
|
146
|
+
@screen.set_style(style)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def reset_style
|
|
150
|
+
@screen.reset_style
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def bell
|
|
154
|
+
# 响铃回调,由 UI 层处理
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|