@sstar/embedlink_agent 0.1.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.
- package/README.md +107 -0
- package/dist/.platform +1 -0
- package/dist/board/docs.js +59 -0
- package/dist/board/notes.js +11 -0
- package/dist/board_uart/history.js +81 -0
- package/dist/board_uart/index.js +66 -0
- package/dist/board_uart/manager.js +313 -0
- package/dist/board_uart/resource.js +578 -0
- package/dist/board_uart/sessions.js +559 -0
- package/dist/config/index.js +341 -0
- package/dist/core/activity.js +7 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/log_stream.js +26 -0
- package/dist/files/__tests__/files_manager.test.js +209 -0
- package/dist/files/artifact_manager.js +68 -0
- package/dist/files/file_operation_logger.js +271 -0
- package/dist/files/files_manager.js +511 -0
- package/dist/files/index.js +87 -0
- package/dist/files/types.js +5 -0
- package/dist/firmware/burn_recover.js +733 -0
- package/dist/firmware/prepare_images.js +184 -0
- package/dist/firmware/user_guide.js +43 -0
- package/dist/index.js +449 -0
- package/dist/logger.js +245 -0
- package/dist/macro/index.js +241 -0
- package/dist/macro/runner.js +168 -0
- package/dist/nfs/index.js +105 -0
- package/dist/plugins/loader.js +30 -0
- package/dist/proto/agent.proto +473 -0
- package/dist/resources/docs/board-interaction.md +115 -0
- package/dist/resources/docs/firmware-upgrade.md +404 -0
- package/dist/resources/docs/nfs-mount-guide.md +78 -0
- package/dist/resources/docs/tftp-transfer-guide.md +81 -0
- package/dist/secrets/index.js +9 -0
- package/dist/server/grpc.js +1069 -0
- package/dist/server/web.js +2284 -0
- package/dist/ssh/adapter.js +126 -0
- package/dist/ssh/candidates.js +85 -0
- package/dist/ssh/index.js +3 -0
- package/dist/ssh/paircheck.js +35 -0
- package/dist/ssh/tunnel.js +111 -0
- package/dist/tftp/client.js +345 -0
- package/dist/tftp/index.js +284 -0
- package/dist/tftp/server.js +731 -0
- package/dist/uboot/index.js +45 -0
- package/dist/ui/assets/index-BlnLVmbt.js +374 -0
- package/dist/ui/assets/index-xMbarYXA.css +32 -0
- package/dist/ui/index.html +21 -0
- package/dist/utils/network.js +150 -0
- package/dist/utils/platform.js +83 -0
- package/dist/utils/port-check.js +153 -0
- package/dist/utils/user-prompt.js +139 -0
- package/package.json +64 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { execRemoteRaw } from '../server/web.js';
|
|
2
|
+
export class SSHAdapter {
|
|
3
|
+
constructor(client) {
|
|
4
|
+
this.systemType = null;
|
|
5
|
+
this.homeDir = null;
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
// 系统检测 - 一次检测,全程使用
|
|
9
|
+
async init() {
|
|
10
|
+
await this.detectSystem();
|
|
11
|
+
await this.detectHomeDir();
|
|
12
|
+
}
|
|
13
|
+
async detectSystem() {
|
|
14
|
+
try {
|
|
15
|
+
// 一个命令就能检测系统类型:Unix系统有uname命令,Windows没有
|
|
16
|
+
const result = await this.exec(`uname >/dev/null 2>&1 && echo "unix" || echo "windows"`);
|
|
17
|
+
this.systemType = result.trim() === 'unix' ? 'unix' : 'windows';
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// 如果检测失败,默认为unix(更常见)
|
|
21
|
+
this.systemType = 'unix';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async detectHomeDir() {
|
|
25
|
+
try {
|
|
26
|
+
if (this.systemType === 'unix') {
|
|
27
|
+
this.homeDir = (await this.exec('echo "${HOME:-/tmp}"')).trim();
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this.homeDir = (await this.exec('echo %USERPROFILE%')).trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// 检测失败时的fallback
|
|
35
|
+
this.homeDir = this.systemType === 'unix' ? '/tmp' : 'C:\\temp';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// 核心执行方法 - 统一错误处理
|
|
39
|
+
async exec(command) {
|
|
40
|
+
return execRemoteRaw(this.client, command);
|
|
41
|
+
}
|
|
42
|
+
// 文件操作 - 自动使用正确的命令和路径
|
|
43
|
+
async fileExists(path) {
|
|
44
|
+
const cmd = this.systemType === 'unix'
|
|
45
|
+
? `test -f "${path}" && echo "EXISTS"`
|
|
46
|
+
: `if exist "${path.replace(/\//g, '\\')}" echo EXISTS`;
|
|
47
|
+
const result = await this.exec(cmd);
|
|
48
|
+
return result.trim() === 'EXISTS';
|
|
49
|
+
}
|
|
50
|
+
async readFile(path) {
|
|
51
|
+
const cmd = this.systemType === 'unix' ? `cat "${path}"` : `type "${path.replace(/\//g, '\\')}"`;
|
|
52
|
+
return await this.exec(cmd);
|
|
53
|
+
}
|
|
54
|
+
async writeFile(path, content) {
|
|
55
|
+
// 统一使用base64方式,避免转义问题
|
|
56
|
+
const base64 = Buffer.from(content).toString('base64');
|
|
57
|
+
const tempFile = `${path}.tmp.${Date.now()}`;
|
|
58
|
+
if (this.systemType === 'unix') {
|
|
59
|
+
await this.exec(`echo '${base64}' | base64 -d > "${tempFile}"`);
|
|
60
|
+
await this.exec(`mv "${tempFile}" "${path}"`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const winPath = path.replace(/\//g, '\\');
|
|
64
|
+
const winTemp = tempFile.replace(/\//g, '\\');
|
|
65
|
+
await this.exec(`echo ${base64} > "${winTemp}.b64"`);
|
|
66
|
+
try {
|
|
67
|
+
await this.exec(`certutil -decode "${winTemp}.b64" "${winPath}"`);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// certutil失败时使用powershell
|
|
71
|
+
await this.exec(`powershell -Command "[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${base64}'))" > "${winPath}"`);
|
|
72
|
+
}
|
|
73
|
+
await this.exec(`del /f /q "${winTemp}.b64" 2>nul`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async mkdir(path) {
|
|
77
|
+
const cmd = this.systemType === 'unix'
|
|
78
|
+
? `mkdir -p "${path}"`
|
|
79
|
+
: `if not exist "${path.replace(/\//g, '\\')}" mkdir "${path.replace(/\//g, '\\')}"`;
|
|
80
|
+
await this.exec(cmd);
|
|
81
|
+
}
|
|
82
|
+
// 网络检测 - 统一接口
|
|
83
|
+
async isPortListening(port) {
|
|
84
|
+
const cmd = this.systemType === 'unix'
|
|
85
|
+
? `netstat -an 2>/dev/null | grep :${port} | grep LISTEN || ss -ltn 2>/dev/null | grep :${port}`
|
|
86
|
+
: `netstat -an | findstr :${port}`;
|
|
87
|
+
const result = await this.exec(cmd);
|
|
88
|
+
return result.includes(`:${port}`);
|
|
89
|
+
}
|
|
90
|
+
// 路径处理 - 自动使用正确分隔符
|
|
91
|
+
joinPath(...parts) {
|
|
92
|
+
if (this.systemType === 'windows') {
|
|
93
|
+
return parts.join('\\');
|
|
94
|
+
}
|
|
95
|
+
return parts.join('/');
|
|
96
|
+
}
|
|
97
|
+
getConfigDir() {
|
|
98
|
+
if (!this.homeDir)
|
|
99
|
+
throw new Error('Adapter not initialized');
|
|
100
|
+
return this.joinPath(this.homeDir, '.config', 'embed_link');
|
|
101
|
+
}
|
|
102
|
+
getAgentJsonPath() {
|
|
103
|
+
return this.joinPath(this.getConfigDir(), 'agent.json');
|
|
104
|
+
}
|
|
105
|
+
// 高级操作 - 组合基础操作
|
|
106
|
+
async readAgentJson() {
|
|
107
|
+
const agentJsonPath = this.getAgentJsonPath();
|
|
108
|
+
if (!(await this.fileExists(agentJsonPath))) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const content = await this.readFile(agentJsonPath);
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(content);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async writeAgentJson(data) {
|
|
120
|
+
const configDir = this.getConfigDir();
|
|
121
|
+
await this.mkdir(configDir);
|
|
122
|
+
const agentJsonPath = this.getAgentJsonPath();
|
|
123
|
+
const content = JSON.stringify(data, null, 2);
|
|
124
|
+
await this.writeFile(agentJsonPath, content);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { exec } from 'node:child_process';
|
|
5
|
+
import dns from 'node:dns/promises';
|
|
6
|
+
export async function gatherSshCandidates() {
|
|
7
|
+
const hosts = [];
|
|
8
|
+
// ~/.ssh/config
|
|
9
|
+
try {
|
|
10
|
+
const file = await fs.readFile(path.join(os.homedir(), '.ssh', 'config'), 'utf8');
|
|
11
|
+
const lines = file.split(/\r?\n/);
|
|
12
|
+
let current = null;
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
const mHost = line.match(/^\s*Host\s+(.+)/i);
|
|
15
|
+
if (mHost) {
|
|
16
|
+
current = { host: mHost[1].trim(), hostName: undefined };
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const mName = line.match(/^\s*HostName\s+(.+)/i);
|
|
20
|
+
if (mName && current) {
|
|
21
|
+
current.hostName = mName[1].trim();
|
|
22
|
+
hosts.push({
|
|
23
|
+
label: current.host,
|
|
24
|
+
host: current.hostName || current.host,
|
|
25
|
+
configSource: 'ssh-config',
|
|
26
|
+
});
|
|
27
|
+
current = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
// netstat/ss to detect active ssh peers
|
|
33
|
+
try {
|
|
34
|
+
const cmd = process.platform === 'win32'
|
|
35
|
+
? 'netstat -na'
|
|
36
|
+
: process.platform === 'darwin'
|
|
37
|
+
? 'netstat -an'
|
|
38
|
+
: 'ss -tn';
|
|
39
|
+
const out = await execAsync(cmd, 2500);
|
|
40
|
+
const re = /([0-9]{1,3}(?:\.[0-9]{1,3}){3}):22/gi;
|
|
41
|
+
const ips = new Set();
|
|
42
|
+
let m;
|
|
43
|
+
while ((m = re.exec(out)))
|
|
44
|
+
ips.add(m[1]);
|
|
45
|
+
for (const ip of ips) {
|
|
46
|
+
const label = await reverseOrIp(ip, 800);
|
|
47
|
+
hosts.push({
|
|
48
|
+
label,
|
|
49
|
+
host: ip,
|
|
50
|
+
configSource: 'manual',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
return hosts;
|
|
56
|
+
}
|
|
57
|
+
async function reverseOrIp(ip, timeoutMs) {
|
|
58
|
+
try {
|
|
59
|
+
const p = dns.reverse(ip);
|
|
60
|
+
const r = (await Promise.race([
|
|
61
|
+
p,
|
|
62
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), timeoutMs)),
|
|
63
|
+
]));
|
|
64
|
+
if (Array.isArray(r) && r.length)
|
|
65
|
+
return r[0];
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
return ip;
|
|
69
|
+
}
|
|
70
|
+
function execAsync(cmd, timeoutMs = 3000) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const p = exec(cmd, { windowsHide: true }, (err, stdout, stderr) => {
|
|
73
|
+
if (err)
|
|
74
|
+
return reject(err);
|
|
75
|
+
resolve(String(stdout || stderr || ''));
|
|
76
|
+
});
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
try {
|
|
79
|
+
p.kill();
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
reject(new Error('timeout'));
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { credentials, loadPackageDefinition } from '@grpc/grpc-js';
|
|
5
|
+
import { loadSync } from '@grpc/proto-loader';
|
|
6
|
+
export function createAgentPairingClient(addr) {
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const candidates = [
|
|
9
|
+
path.join(here, '..', '..', 'proto', 'agent.proto'),
|
|
10
|
+
path.join(here, '..', '..', '..', 'proto', 'agent.proto'),
|
|
11
|
+
];
|
|
12
|
+
const protoPath = candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
13
|
+
const def = loadSync(protoPath, {
|
|
14
|
+
longs: String,
|
|
15
|
+
enums: String,
|
|
16
|
+
defaults: true,
|
|
17
|
+
oneofs: true,
|
|
18
|
+
keepCase: true,
|
|
19
|
+
});
|
|
20
|
+
const pkg = loadPackageDefinition(def);
|
|
21
|
+
const Cls = pkg.embedlink.agent.v1.PairingService;
|
|
22
|
+
const client = new Cls(addr, credentials.createInsecure());
|
|
23
|
+
return {
|
|
24
|
+
pairCheck(req) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const payload = {
|
|
27
|
+
pairCode: req.pairCode,
|
|
28
|
+
clientId: req.clientId || '',
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
client.PairCheck(payload, (e, r) => (e ? reject(e) : resolve(r)));
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { error, ErrorCodes } from '../core/errors.js';
|
|
4
|
+
export class SshTunnel {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.client = new Client();
|
|
7
|
+
this.connected = false;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 验证SSH隧道的连通性
|
|
11
|
+
* 通过SSH连接执行一个简单的本地端口连通性测试
|
|
12
|
+
*/
|
|
13
|
+
async verifyConnectivity(timeoutMs = 3000) {
|
|
14
|
+
if (!this.connected || !this.opts)
|
|
15
|
+
return false;
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
resolve(false); // 超时视为不可用
|
|
19
|
+
}, timeoutMs);
|
|
20
|
+
// 尝试通过SSH连接到本地目标端口
|
|
21
|
+
const conn = net.connect(this.opts.dstPort, this.opts.dstHost);
|
|
22
|
+
conn.on('connect', () => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
conn.destroy();
|
|
25
|
+
resolve(true);
|
|
26
|
+
});
|
|
27
|
+
conn.on('error', () => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
resolve(false);
|
|
30
|
+
});
|
|
31
|
+
conn.setTimeout(timeoutMs);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
start(opts) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
this.opts = opts;
|
|
37
|
+
this.client
|
|
38
|
+
.on('ready', () => {
|
|
39
|
+
this.client.forwardIn(opts.bindAddr, opts.bindPort, (err) => {
|
|
40
|
+
if (err)
|
|
41
|
+
return reject(err);
|
|
42
|
+
this.client.on('tcp connection', (info, accept, rejectTcp) => {
|
|
43
|
+
const stream = accept();
|
|
44
|
+
const socket = net.connect(opts.dstPort, opts.dstHost);
|
|
45
|
+
stream.on('error', () => {
|
|
46
|
+
try {
|
|
47
|
+
socket.destroy();
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
});
|
|
51
|
+
socket.on('error', () => {
|
|
52
|
+
try {
|
|
53
|
+
stream.end();
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
});
|
|
57
|
+
stream.pipe(socket).pipe(stream);
|
|
58
|
+
});
|
|
59
|
+
this.connected = true;
|
|
60
|
+
resolve({ bindPort: opts.bindPort });
|
|
61
|
+
});
|
|
62
|
+
})
|
|
63
|
+
.on('error', (e) => reject(error(ErrorCodes.EL_SSH_CONNECT_FAILED, String(e))))
|
|
64
|
+
.connect({
|
|
65
|
+
host: opts.host,
|
|
66
|
+
port: opts.port,
|
|
67
|
+
username: opts.user,
|
|
68
|
+
password: opts.password,
|
|
69
|
+
privateKey: opts.privateKey,
|
|
70
|
+
tryKeyboard: false,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
stop() {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
// Set connected to false immediately
|
|
77
|
+
this.connected = false;
|
|
78
|
+
try {
|
|
79
|
+
// Add proper cleanup handlers
|
|
80
|
+
this.client.on('end', () => {
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
this.client.on('error', () => {
|
|
84
|
+
resolve(); // Even error should resolve the promise
|
|
85
|
+
});
|
|
86
|
+
// Set a timeout to ensure we don't hang forever
|
|
87
|
+
const timeout = setTimeout(() => {
|
|
88
|
+
resolve(); // Force resolve after timeout
|
|
89
|
+
}, 3000);
|
|
90
|
+
this.client.on('end', () => {
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
});
|
|
93
|
+
this.client.on('error', () => {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
});
|
|
96
|
+
// End the connection
|
|
97
|
+
this.client.end();
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
// If something goes wrong, still resolve
|
|
101
|
+
resolve();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
isConnected() {
|
|
106
|
+
return this.connected;
|
|
107
|
+
}
|
|
108
|
+
getClient() {
|
|
109
|
+
return this.client;
|
|
110
|
+
}
|
|
111
|
+
}
|