network-infra-utility 0.2.0 → 0.5.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/CHANGELOG.md +31 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +70 -0
- data/Rakefile +1 -1
- data/bin/dns-query +834 -0
- data/bin/geo-doc +135 -0
- data/bin/geo-get +1 -1
- 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/GeoAPI.md +1 -0
- data/service/geodb/geodb.rb +278 -1
- data/service/ssh/README.md +942 -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 +104 -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 +138 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "time"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
module NetworkInfraUtility
|
|
8
|
+
module SSH
|
|
9
|
+
module Terminal
|
|
10
|
+
# 会话日志记录与自动轮转。
|
|
11
|
+
# 对应需求 FR-TERM-006。
|
|
12
|
+
#
|
|
13
|
+
# LLD §10.1 可观测性:
|
|
14
|
+
# 路径模式 logs/<sess_id>/<ts>.log
|
|
15
|
+
# 格式 纯文本带时间戳;可选 HTML
|
|
16
|
+
# 轮转 单文件 100MB / 保留 10
|
|
17
|
+
#
|
|
18
|
+
# DoD(LLD §11.2):自动轮转;HTML 格式带颜色还原
|
|
19
|
+
#
|
|
20
|
+
# 集成点:Emulator#feed 中 @logger&.write(data)
|
|
21
|
+
class Logger
|
|
22
|
+
DEFAULT_MAX_SIZE = 100 * 1024 * 1024 # 100MB
|
|
23
|
+
DEFAULT_ROTATE = 10 # 保留 10 个文件
|
|
24
|
+
TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S"
|
|
25
|
+
|
|
26
|
+
attr_reader :path, :max_size, :rotate, :format, :current_size
|
|
27
|
+
|
|
28
|
+
# @param path [String] 日志文件路径(不含扩展名,自动加 .log 或 .html)
|
|
29
|
+
# @param max_size [Integer] 单文件最大字节数
|
|
30
|
+
# @param rotate [Integer] 保留轮转文件数
|
|
31
|
+
# @param format [Symbol] :text 或 :html
|
|
32
|
+
def initialize(path, max_size: DEFAULT_MAX_SIZE, rotate: DEFAULT_ROTATE, format: :text)
|
|
33
|
+
@base_path = path
|
|
34
|
+
@max_size = max_size
|
|
35
|
+
@rotate = rotate
|
|
36
|
+
@format = format
|
|
37
|
+
@current_size = 0
|
|
38
|
+
@write_mutex = Mutex.new
|
|
39
|
+
@io = nil
|
|
40
|
+
@html_header_written = false
|
|
41
|
+
@seq = 0
|
|
42
|
+
open_new_file
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 写入原始终端数据。
|
|
46
|
+
# 由 Emulator#feed 调用,每次收到 SSH 通道数据即写入。
|
|
47
|
+
# @param data [String] 原始字节
|
|
48
|
+
def write(data)
|
|
49
|
+
@write_mutex.synchronize do
|
|
50
|
+
return unless @io
|
|
51
|
+
|
|
52
|
+
if @format == :html
|
|
53
|
+
write_html(data)
|
|
54
|
+
else
|
|
55
|
+
write_text(data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@current_size += data.bytesize
|
|
59
|
+
rotate_if_needed
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
alias << write
|
|
63
|
+
|
|
64
|
+
# 关闭当前日志文件
|
|
65
|
+
def close
|
|
66
|
+
@write_mutex.synchronize do
|
|
67
|
+
if @format == :html && @io
|
|
68
|
+
@io.write("</pre>\n</body>\n</html>\n")
|
|
69
|
+
end
|
|
70
|
+
@io&.close
|
|
71
|
+
@io = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 切换格式(需重新打开文件)
|
|
76
|
+
def format=(fmt)
|
|
77
|
+
close
|
|
78
|
+
@format = fmt
|
|
79
|
+
@html_header_written = false
|
|
80
|
+
open_new_file
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# 打开新的日志文件
|
|
86
|
+
def open_new_file
|
|
87
|
+
dir = File.dirname(@base_path)
|
|
88
|
+
FileUtils.mkdir_p(dir)
|
|
89
|
+
|
|
90
|
+
ext = @format == :html ? ".html" : ".log"
|
|
91
|
+
timestamp = Time.now.strftime(TIMESTAMP_FORMAT)
|
|
92
|
+
@seq += 1
|
|
93
|
+
@path = "#{@base_path}_#{timestamp}_#{@seq}#{ext}"
|
|
94
|
+
|
|
95
|
+
@io = File.open(@path, "wb")
|
|
96
|
+
@io.sync = true
|
|
97
|
+
@current_size = 0
|
|
98
|
+
|
|
99
|
+
if @format == :html
|
|
100
|
+
write_html_header
|
|
101
|
+
else
|
|
102
|
+
# 纯文本头部:时间戳
|
|
103
|
+
@io.write("# Session log started at #{Time.now.iso8601}\n")
|
|
104
|
+
@current_size += 30
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# 纯文本写入:带时间戳前缀
|
|
109
|
+
def write_text(data)
|
|
110
|
+
# 每行加时间戳前缀
|
|
111
|
+
lines = data.split("\n", -1)
|
|
112
|
+
lines.each_with_index do |line, i|
|
|
113
|
+
next if line.empty? && i == lines.size - 1
|
|
114
|
+
|
|
115
|
+
ts = Time.now.strftime("[%H:%M:%S] ")
|
|
116
|
+
@io.write("#{ts}#{line}\n")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# HTML 写入:保留 ANSI 颜色信息,转义 HTML 实体
|
|
121
|
+
def write_html_header
|
|
122
|
+
@io.write(<<~HTML)
|
|
123
|
+
<!DOCTYPE html>
|
|
124
|
+
<html>
|
|
125
|
+
<head>
|
|
126
|
+
<meta charset="utf-8">
|
|
127
|
+
<title>SSH Session Log</title>
|
|
128
|
+
<style>
|
|
129
|
+
body { background: #1e1e1e; color: #d4d4d4; font-family: monospace; }
|
|
130
|
+
pre { white-space: pre-wrap; word-wrap: break-word; }
|
|
131
|
+
</style>
|
|
132
|
+
</head>
|
|
133
|
+
<body>
|
|
134
|
+
<pre>
|
|
135
|
+
HTML
|
|
136
|
+
@html_header_written = true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# HTML 写入:ANSI 转义转为 span 标签
|
|
140
|
+
def write_html(data)
|
|
141
|
+
# 转义 HTML 实体
|
|
142
|
+
escaped = data.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
143
|
+
|
|
144
|
+
# ANSI SGR 转义序列 → <span style="color:...">
|
|
145
|
+
# 这里的实现是骨架,完整 ANSI→HTML 转换在具体实现阶段完善
|
|
146
|
+
# 覆盖常见 SGR:重置 \e[0m、前景色 \e[3Xm、加粗 \e[1m
|
|
147
|
+
escaped.gsub!(/\e\[0m/, "</span>")
|
|
148
|
+
escaped.gsub!(/\e\[1m/, '<span style="font-weight:bold">')
|
|
149
|
+
escaped.gsub!(/\e\[3(\d)m/) do
|
|
150
|
+
idx = Regexp.last_match(1).to_i
|
|
151
|
+
color = ansi_color_to_css(idx)
|
|
152
|
+
%(<span style="color:#{color}">)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
@io.write(escaped)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# ANSI 8 色映射到 CSS 颜色
|
|
159
|
+
def ansi_color_to_css(idx)
|
|
160
|
+
%w[#000000 #cc0000 #00aa00 #aa5500 #0033cc #aa00aa #00aaaa #aaaaaa
|
|
161
|
+
#555555 #ff5555 #55ff55 #ffff55 #5555ff #ff55ff #55ffff #ffffff][idx] || "#ffffff"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# 检查是否需要轮转
|
|
165
|
+
def rotate_if_needed
|
|
166
|
+
return if @current_size < @max_size
|
|
167
|
+
|
|
168
|
+
@io.close
|
|
169
|
+
if @format == :html
|
|
170
|
+
# 需要给当前文件写尾部
|
|
171
|
+
File.open(@path, "a") { |f| f.write("</pre>\n</body>\n</html>\n") }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# 清理超出 rotate 数量的旧文件
|
|
175
|
+
cleanup_old_files
|
|
176
|
+
open_new_file
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# 清理超出保留数量的旧日志文件
|
|
180
|
+
def cleanup_old_files
|
|
181
|
+
dir = File.dirname(@base_path)
|
|
182
|
+
base = File.basename(@base_path)
|
|
183
|
+
ext = @format == :html ? ".html" : ".log"
|
|
184
|
+
|
|
185
|
+
pattern = File.join(dir, "#{base}_*#{ext}")
|
|
186
|
+
files = Dir.glob(pattern).sort_by { |f| File.mtime(f) }
|
|
187
|
+
excess = files.size - @rotate
|
|
188
|
+
return if excess <= 0
|
|
189
|
+
|
|
190
|
+
files.first(excess).each { |f| File.delete(f) }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NetworkInfraUtility
|
|
4
|
+
module SSH
|
|
5
|
+
module Terminal
|
|
6
|
+
# 屏幕状态:行数组 + 光标 + 滚动区 + alt buffer + 字符属性。
|
|
7
|
+
# 由 AnsiParser 经 Emulator 回调驱动。
|
|
8
|
+
class Screen
|
|
9
|
+
attr_reader :cols, :rows, :cursor_x, :cursor_y
|
|
10
|
+
attr_reader :scroll_top, :scroll_bottom
|
|
11
|
+
attr_reader :alt_mode
|
|
12
|
+
|
|
13
|
+
def initialize(cols, rows)
|
|
14
|
+
@cols = cols
|
|
15
|
+
@rows = rows
|
|
16
|
+
@cursor_x = 0
|
|
17
|
+
@cursor_y = 0
|
|
18
|
+
@scroll_top = 0
|
|
19
|
+
@scroll_bottom = rows - 1
|
|
20
|
+
@alt_mode = false
|
|
21
|
+
@main_lines = Array.new(rows) { Line.new(@cols) }
|
|
22
|
+
@alt_lines = Array.new(rows) { Line.new(@cols) }
|
|
23
|
+
@current_style = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def lines
|
|
27
|
+
@alt_mode ? @alt_lines : @main_lines
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def put_char(ch, x, y, style = {})
|
|
31
|
+
line = lines[y]
|
|
32
|
+
return unless line
|
|
33
|
+
|
|
34
|
+
line.put_char(ch, x, style.empty? ? @current_style : style)
|
|
35
|
+
@cursor_x = x + 1
|
|
36
|
+
@cursor_y = y
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cursor_to(x, y)
|
|
40
|
+
@cursor_x = [[x, 0].max, @cols - 1].min
|
|
41
|
+
@cursor_y = [[y, 0].max, @rows - 1].min
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def newline
|
|
45
|
+
if @cursor_y >= @scroll_bottom
|
|
46
|
+
scroll_up(1)
|
|
47
|
+
else
|
|
48
|
+
@cursor_y += 1
|
|
49
|
+
end
|
|
50
|
+
@cursor_x = 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def backspace
|
|
54
|
+
@cursor_x = [@cursor_x - 1, 0].max
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def tab
|
|
58
|
+
@cursor_x = ((@cursor_x / 8) + 1) * 8
|
|
59
|
+
@cursor_x = [@cursor_x, @cols - 1].min
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clear(mode = :full)
|
|
63
|
+
case mode
|
|
64
|
+
when :full
|
|
65
|
+
@main_lines.each { |l| l.clear(@cols) }
|
|
66
|
+
@alt_lines.each { |l| l.clear(@cols) }
|
|
67
|
+
@cursor_x = 0
|
|
68
|
+
@cursor_y = 0
|
|
69
|
+
when :above
|
|
70
|
+
clear_above_cursor
|
|
71
|
+
when :below
|
|
72
|
+
clear_below_cursor
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def clear_line(mode = :full)
|
|
77
|
+
line = lines[@cursor_y]
|
|
78
|
+
return unless line
|
|
79
|
+
|
|
80
|
+
case mode
|
|
81
|
+
when :full then line.clear(@cols)
|
|
82
|
+
when :right then line.clear_from(@cursor_x, @cols)
|
|
83
|
+
when :left then line.clear_range(0, @cursor_x)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def scroll_up(n = 1)
|
|
88
|
+
n.times do
|
|
89
|
+
# 将滚动区首行移入回滚(由 Buffer 处理),后续行上移
|
|
90
|
+
@scroll_top.upto(@scroll_bottom - 1) do |i|
|
|
91
|
+
lines[i] = lines[i + 1]
|
|
92
|
+
end
|
|
93
|
+
lines[@scroll_bottom] = Line.new(@cols)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def scroll_down(n = 1)
|
|
98
|
+
n.times do
|
|
99
|
+
@scroll_bottom.downto(@scroll_top + 1) do |i|
|
|
100
|
+
lines[i] = lines[i - 1]
|
|
101
|
+
end
|
|
102
|
+
lines[@scroll_top] = Line.new(@cols)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def set_scroll_region(top, bottom)
|
|
107
|
+
@scroll_top = [[top, 0].max, @rows - 1].min
|
|
108
|
+
@scroll_bottom = [[bottom, 0].max, @rows - 1].min
|
|
109
|
+
@cursor_x = 0
|
|
110
|
+
@cursor_y = @scroll_top
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def enter_alt
|
|
114
|
+
@alt_mode = true
|
|
115
|
+
@cursor_x = 0
|
|
116
|
+
@cursor_y = 0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def exit_alt
|
|
120
|
+
@alt_mode = false
|
|
121
|
+
@cursor_x = 0
|
|
122
|
+
@cursor_y = 0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def set_style(style)
|
|
126
|
+
@current_style = @current_style.merge(style)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def reset_style
|
|
130
|
+
@current_style = {}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def resize(cols, rows)
|
|
134
|
+
old_cols = @cols
|
|
135
|
+
@cols = cols
|
|
136
|
+
[@main_lines, @alt_lines].each do |ary|
|
|
137
|
+
if rows > ary.size
|
|
138
|
+
(rows - ary.size).times { ary << Line.new(cols) }
|
|
139
|
+
elsif rows < ary.size
|
|
140
|
+
ary.slice!(rows..)
|
|
141
|
+
end
|
|
142
|
+
ary.each { |l| l.resize(cols) } if cols != old_cols
|
|
143
|
+
end
|
|
144
|
+
@rows = rows
|
|
145
|
+
@scroll_bottom = rows - 1
|
|
146
|
+
@cursor_x = [@cursor_x, cols - 1].min
|
|
147
|
+
@cursor_y = [@cursor_y, rows - 1].min
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def to_text
|
|
151
|
+
lines.map(&:to_s).join("\n")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def clear_above_cursor
|
|
157
|
+
0.upto(@cursor_y - 1) { |i| lines[i]&.clear(@cols) }
|
|
158
|
+
lines[@cursor_y]&.clear_range(0, @cursor_x)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def clear_below_cursor
|
|
162
|
+
lines[@cursor_y]&.clear_from(@cursor_x, @cols)
|
|
163
|
+
(@cursor_y + 1).upto(@rows - 1) { |i| lines[i]&.clear(@cols) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 一行字符
|
|
167
|
+
class Line
|
|
168
|
+
attr_reader :cells
|
|
169
|
+
|
|
170
|
+
def initialize(cols)
|
|
171
|
+
@cells = Array.new(cols) { Cell.new }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def put_char(ch, x, style = {})
|
|
175
|
+
@cells[x] = Cell.new(ch, style) if x < @cells.size
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def clear(cols)
|
|
179
|
+
@cells = Array.new(cols) { Cell.new }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def clear_from(x, cols)
|
|
183
|
+
x.upto([cols - 1, @cells.size - 1].min) { |i| @cells[i] = Cell.new }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def clear_range(from, to)
|
|
187
|
+
from.upto([to, @cells.size - 1].min) { |i| @cells[i] = Cell.new }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def resize(cols)
|
|
191
|
+
if cols > @cells.size
|
|
192
|
+
@cells += Array.new(cols - @cells.size) { Cell.new }
|
|
193
|
+
elsif cols < @cells.size
|
|
194
|
+
@cells = @cells[0...cols]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def to_s
|
|
199
|
+
@cells.map(&:char).join.rstrip
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# 单字符单元
|
|
204
|
+
Cell = Struct.new(:char, :style) do
|
|
205
|
+
def initialize(char = " ", style = {})
|
|
206
|
+
super
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module NetworkInfraUtility
|
|
6
|
+
module SSH
|
|
7
|
+
module Terminal
|
|
8
|
+
# 配色方案加载/查询。
|
|
9
|
+
# 对应需求 FR-TERM-002:内置 ≥ 10 套配色,支持导入 iTerm2/VSCode。
|
|
10
|
+
class Theme
|
|
11
|
+
BUILTIN_THEMES = {
|
|
12
|
+
"default" => {
|
|
13
|
+
bg: "#1e1e1e", fg: "#d4d4d4", cursor: "#d4d4d4",
|
|
14
|
+
palette: %w[#000000 #cd3131 #0dbc79 #e5e510 #2472c8 #bc3fbc #11a8cd #e5e5e5
|
|
15
|
+
#666666 #f14c4c #23d18b #f5f543 #3b8eea #d670d6 #29b8db #e5e5e5]
|
|
16
|
+
},
|
|
17
|
+
"solarized-dark" => {
|
|
18
|
+
bg: "#002b36", fg: "#839496", cursor: "#93a1a1",
|
|
19
|
+
palette: %w[#073642 #dc322f #859900 #b58900 #268bd2 #d33682 #2aa198 #eee8d5
|
|
20
|
+
#002b36 #cb4b16 #586e75 #657b83 #839496 #6c71c4 #93a1a1 #fdf6e3]
|
|
21
|
+
},
|
|
22
|
+
"solarized-light" => {
|
|
23
|
+
bg: "#fdf6e3", fg: "#657b83", cursor: "#586e75",
|
|
24
|
+
palette: %w[#073642 #dc322f #859900 #b58900 #268bd2 #d33682 #2aa198 #eee8d5
|
|
25
|
+
#002b36 #cb4b16 #586e75 #657b83 #839496 #6c71c4 #93a1a1 #fdf6e3]
|
|
26
|
+
},
|
|
27
|
+
"dracula" => {
|
|
28
|
+
bg: "#282a36", fg: "#f8f8f2", cursor: "#f8f8f2",
|
|
29
|
+
palette: %w[#000000 #ff5555 #50fa7b #f1fa8c #bd93f9 #ff79c6 #8be9fd #bfbfbf
|
|
30
|
+
#4d4d4d #ff6e67 #5af78e #f4f99d #caa9fa #ff92d0 #9aedfe #e6e6e6]
|
|
31
|
+
},
|
|
32
|
+
"monokai" => {
|
|
33
|
+
bg: "#272822", fg: "#f8f8f2", cursor: "#f8f8f0",
|
|
34
|
+
palette: %w[#272822 #f92672 #a6e22e #fd971f #66d9ef #ae81ff #a1efe4 #f8f8f2
|
|
35
|
+
#75715e #f92672 #a6e22e #fd971f #66d9ef #ae81ff #a1efe4 #f9f8f5]
|
|
36
|
+
},
|
|
37
|
+
"nord" => {
|
|
38
|
+
bg: "#2e3440", fg: "#d8dee9", cursor: "#d8dee9",
|
|
39
|
+
palette: %w[#3b4252 #bf616a #a3be8c #ebcb8b #81a1c1 #b48ead #88c0d0 #e5e9f0
|
|
40
|
+
#4c566a #bf616a #a3be8c #ebcb8b #81a1c1 #b48ead #8fbcbb #eceff4]
|
|
41
|
+
},
|
|
42
|
+
"gruvbox-dark" => {
|
|
43
|
+
bg: "#282828", fg: "#ebdbb2", cursor: "#ebdbb2",
|
|
44
|
+
palette: %w[#282828 #cc241d #98971a #d79921 #458588 #b16286 #689d6a #a89984
|
|
45
|
+
#928374 #fb4934 #b8bb26 #fabd2f #83a598 #d3869b #8ec07c #ebdbb2]
|
|
46
|
+
},
|
|
47
|
+
"one-dark" => {
|
|
48
|
+
bg: "#282c34", fg: "#abb2bf", cursor: "#abb2bf",
|
|
49
|
+
palette: %w[#282c34 #e06c75 #98c379 #e5c07b #61afef #c678dd #56b6c2 #abb2bf
|
|
50
|
+
#5c6370 #e06c75 #98c379 #e5c07b #61afef #c678dd #56b6c2 #ffffff]
|
|
51
|
+
},
|
|
52
|
+
"tokyo-night" => {
|
|
53
|
+
bg: "#1a1b26", fg: "#a9b1d6", cursor: "#c0caf5",
|
|
54
|
+
palette: %w[#15161e #f7768e #9ece6a #e0af68 #7aa2f7 #bb9af7 #7dcfff #a9b1d6
|
|
55
|
+
#414868 #f7768e #9ece6a #e0af68 #7aa2f7 #bb9af7 #7dcfff #c0caf5]
|
|
56
|
+
},
|
|
57
|
+
"catppuccin-mocha" => {
|
|
58
|
+
bg: "#1e1e2e", fg: "#cdd6f4", cursor: "#f5e0dc",
|
|
59
|
+
palette: %w[#45475a #f38ba8 #a6e3a1 #f9e2af #89b4fa #f5c2e7 #94e2d5 #bac2de
|
|
60
|
+
#585b70 #f38ba8 #a6e3a1 #f9e2af #89b4fa #f5c2e7 #94e2d5 #a6adc8]
|
|
61
|
+
},
|
|
62
|
+
"github-dark" => {
|
|
63
|
+
bg: "#0d1117", fg: "#c9d1d9", cursor: "#6cae50",
|
|
64
|
+
palette: %w[#484f58 #ff7b72 #7ee787 #f2cc60 #79c0ff #d2a8ff #56d4dd #c9d1d9
|
|
65
|
+
#6e7681 #ffa198 #56d364 #e3b341 #79c0ff #bc8cff #56d4dd #f0f6fc]
|
|
66
|
+
}
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
attr_reader :name, :data
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
def load(name)
|
|
73
|
+
data = BUILTIN_THEMES[name] || load_custom(name) || BUILTIN_THEMES["default"]
|
|
74
|
+
new(name, data)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def default
|
|
78
|
+
load("default")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def builtin_names
|
|
82
|
+
BUILTIN_THEMES.keys
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 从 iTerm2 .itermcolors 文件导入
|
|
86
|
+
def import_iterm(path)
|
|
87
|
+
# V2.0 功能,预留接口
|
|
88
|
+
raise NotImplementedError, "iTerm2 import is V2.0"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 从 VSCode .json 主题文件导入
|
|
92
|
+
def import_vscode(path)
|
|
93
|
+
# V2.0 功能,预留接口
|
|
94
|
+
raise NotImplementedError, "VSCode import is V2.0"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def load_custom(name)
|
|
100
|
+
# 从 ~/.network-infra-utility/themes/<name>.yml 加载
|
|
101
|
+
themes_dir = File.expand_path("~/.network-infra-utility/themes")
|
|
102
|
+
path = File.join(themes_dir, "#{name}.yml")
|
|
103
|
+
return nil unless File.exist?(path)
|
|
104
|
+
|
|
105
|
+
YAML.safe_load(File.read(path), symbolize_names: true)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def initialize(name, data)
|
|
110
|
+
@name = name
|
|
111
|
+
@data = data.transform_keys(&:to_sym)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def bg; @data[:bg] || "#000000"; end
|
|
115
|
+
def fg; @data[:fg] || "#ffffff"; end
|
|
116
|
+
def cursor; @data[:cursor] || fg; end
|
|
117
|
+
def palette; @data[:palette] || []; end
|
|
118
|
+
|
|
119
|
+
def color(index)
|
|
120
|
+
return fg if index.nil? || index < 0 || index >= palette.size
|
|
121
|
+
|
|
122
|
+
palette[index]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ssh/version"
|
|
4
|
+
require_relative "ssh/client"
|
|
5
|
+
require_relative "ssh/ipc/transport"
|
|
6
|
+
require_relative "ssh/ipc/router"
|
|
7
|
+
require_relative "ssh/ipc/coalesce"
|
|
8
|
+
require_relative "ssh/ipc/errors"
|
|
9
|
+
require_relative "ssh/session/manager"
|
|
10
|
+
require_relative "ssh/session/session"
|
|
11
|
+
require_relative "ssh/session/tree"
|
|
12
|
+
require_relative "ssh/session/history"
|
|
13
|
+
require_relative "ssh/terminal/emulator"
|
|
14
|
+
require_relative "ssh/terminal/ansi_parser"
|
|
15
|
+
require_relative "ssh/terminal/screen"
|
|
16
|
+
require_relative "ssh/terminal/buffer"
|
|
17
|
+
require_relative "ssh/terminal/theme"
|
|
18
|
+
require_relative "ssh/terminal/logger"
|
|
19
|
+
require_relative "ssh/security/vault"
|
|
20
|
+
require_relative "ssh/security/host_key"
|
|
21
|
+
require_relative "ssh/automation/macro_engine"
|
|
22
|
+
require_relative "ssh/config/settings"
|
|
23
|
+
require_relative "ssh/config/schema"
|
|
24
|
+
require_relative "ssh/config/store"
|
|
25
|
+
|
|
26
|
+
# NetworkInfraUtility::SSH — SSH 连接客户端
|
|
27
|
+
#
|
|
28
|
+
# 架构(LLD §1):
|
|
29
|
+
# Erlang/OTP ssh 做协议核心,Ruby 做调度与终端,
|
|
30
|
+
# 两者经 JSON-RPC 2.0 over Unix Socket/TCP 通信。
|
|
31
|
+
#
|
|
32
|
+
# 组合根:SSH::Client——构造时编排全部子系统,
|
|
33
|
+
# 管理 Erlang 引擎生命周期与 IPC 连接。
|
|
34
|
+
#
|
|
35
|
+
# 用法:
|
|
36
|
+
# client = NetworkInfraUtility::SSH::Client.new
|
|
37
|
+
# client.start_engine
|
|
38
|
+
# session = client.connect(host: "10.0.0.1", user: "admin")
|
|
39
|
+
# terminal = session.open_terminal
|
|
40
|
+
# terminal.puts("show version")
|
|
41
|
+
module NetworkInfraUtility
|
|
42
|
+
module SSH
|
|
43
|
+
end
|
|
44
|
+
end
|