@lazycatcloud/lzc-cli 1.3.17 → 2.0.0-pre.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.
- package/README.md +47 -7
- package/changelog.md +14 -0
- package/lib/app/apkshell.js +7 -44
- package/lib/app/index.js +178 -64
- package/lib/app/lpk_build.js +446 -61
- package/lib/app/lpk_build_images.js +749 -0
- package/lib/app/lpk_create.js +192 -45
- package/lib/app/lpk_create_generator.js +141 -13
- package/lib/app/lpk_devshell.js +33 -19
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +17 -9
- package/lib/app/manifest_build.js +259 -0
- package/lib/app/project_cp.js +59 -0
- package/lib/app/project_deploy.js +58 -0
- package/lib/app/project_exec.js +82 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +62 -0
- package/lib/app/project_runtime.js +356 -0
- package/lib/app/project_start.js +95 -0
- package/lib/app/project_sync.js +499 -0
- package/lib/appstore/apkshell.js +50 -0
- package/lib/box/index.js +101 -4
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +21 -0
- package/lib/debug_bridge.js +891 -83
- package/lib/docker/index.js +30 -10
- package/lib/i18n/locales/en/translation.json +262 -255
- package/lib/i18n/locales/zh/translation.json +262 -255
- package/lib/lpk/core.js +488 -0
- package/lib/lpk/index.js +210 -0
- package/lib/migrate/index.js +52 -0
- package/lib/package_info.js +135 -0
- package/lib/shellapi.js +35 -1
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +94 -15
- package/package.json +3 -3
- package/scripts/cli.js +6 -0
- package/scripts/smoke/frontend-dev-entry.mjs +104 -0
- package/scripts/smoke/template-project.mjs +311 -0
- package/template/_lpk/README.md +15 -4
- package/template/_lpk/gui-vnc.manifest.yml.in +18 -0
- package/template/_lpk/hello-vue.manifest.yml.in +38 -0
- package/template/_lpk/manifest.yml.in +4 -11
- package/template/_lpk/package.yml.in +7 -0
- package/template/_lpk/todolist-golang.manifest.yml.in +30 -0
- package/template/_lpk/todolist-java.manifest.yml.in +29 -0
- package/template/_lpk/todolist-python.manifest.yml.in +37 -0
- package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/lzc-build.dev.yml +4 -0
- package/template/blank/lzc-build.yml +24 -41
- package/template/blank/lzc-manifest.yml +7 -9
- package/template/blank/package.yml +7 -0
- package/template/golang/Dockerfile +19 -0
- package/template/golang/Dockerfile.dev +20 -0
- package/template/golang/README.md +44 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/_lzcdevignore +21 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.dev.yml +12 -0
- package/template/golang/lzc-build.yml +16 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/manifest.dev.page.js +24 -0
- package/template/golang/run.sh +10 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +23 -0
- package/template/gui-vnc/_gitignore +2 -0
- package/template/gui-vnc/images/Dockerfile +30 -0
- package/template/gui-vnc/images/kasmvnc.yaml +33 -0
- package/template/gui-vnc/images/startup-script.desktop +9 -0
- package/template/gui-vnc/images/startup-script.sh +6 -0
- package/template/gui-vnc/lzc-build.dev.yml +4 -0
- package/template/gui-vnc/lzc-build.yml +18 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/Dockerfile.dev +18 -0
- package/template/python/README.md +50 -0
- package/template/python/_gitignore +3 -0
- package/template/python/_lzcdevignore +21 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.dev.yml +12 -0
- package/template/python/lzc-build.yml +16 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/manifest.dev.page.js +25 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +14 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/Dockerfile.dev +20 -0
- package/template/springboot/README.md +44 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/_lzcdevignore +21 -0
- package/template/springboot/lzc-build.dev.yml +12 -0
- package/template/springboot/lzc-build.yml +16 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/manifest.dev.page.js +24 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +10 -0
- package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
- package/template/springboot/src/main/resources/application.properties +1 -0
- package/template/springboot/src/main/resources/static/index.html +238 -0
- package/template/vue/README.md +18 -21
- package/template/vue/lzc-build.dev.yml +7 -0
- package/template/vue/lzc-build.yml +30 -43
- package/template/vue/manifest.dev.page.js +50 -0
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +26 -0
- package/template/vue-minidb/_gitignore +25 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.dev.yml +7 -0
- package/template/vue-minidb/lzc-build.yml +46 -0
- package/template/vue-minidb/lzc-icon.png +0 -0
- package/template/vue-minidb/manifest.dev.page.js +50 -0
- package/template/vue-minidb/package.json +21 -0
- package/template/vue-minidb/public/vite.svg +1 -0
- package/template/vue-minidb/src/App.vue +206 -0
- package/template/vue-minidb/src/assets/vue.svg +1 -0
- package/template/vue-minidb/src/main.ts +5 -0
- package/template/vue-minidb/src/style.css +136 -0
- package/template/vue-minidb/src/vite-env.d.ts +1 -0
- package/template/vue-minidb/tsconfig.app.json +24 -0
- package/template/vue-minidb/tsconfig.json +7 -0
- package/template/vue-minidb/tsconfig.node.json +22 -0
- package/template/vue-minidb/vite.config.ts +10 -0
- /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
package/lib/debug_bridge.js
CHANGED
|
@@ -5,10 +5,14 @@ import fetch from 'node-fetch';
|
|
|
5
5
|
|
|
6
6
|
import shellApi from './shellapi.js';
|
|
7
7
|
import { _SYSTEM_ENV_PREFIX } from './config/env.js';
|
|
8
|
-
import {
|
|
8
|
+
import { isTraceMode, resolveDomain, sleep, findSshPublicKey, selectSshPublicKey, isWindows, compareVersions } from './utils.js';
|
|
9
9
|
import { t } from './i18n/index.js';
|
|
10
|
+
import { resolveBuildRemoteFromFile } from './build_remote.js';
|
|
10
11
|
|
|
11
12
|
const bannerfileContent = `˄=ᆽ=ᐟ \\`;
|
|
13
|
+
const DEBUG_BRIDGE_CONTAINER = 'cloudlazycatdevelopertools-app-1';
|
|
14
|
+
const DEBUG_BRIDGE_BINARY = '/lzcapp/pkg/content/debug.bridge';
|
|
15
|
+
const DEBUG_BRIDGE_APP_ID = 'cloud.lazycat.developer.tools';
|
|
12
16
|
|
|
13
17
|
export function sshBinary() {
|
|
14
18
|
if (isWindows) {
|
|
@@ -22,57 +26,305 @@ export function sshCmdArgs(...args) {
|
|
|
22
26
|
return [...defaultOptions, ...args];
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
export function sshCmdArgsRaw(...args) {
|
|
30
|
+
const defaultOptions = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'ControlMaster=no', '-q', '-p', '22222'];
|
|
31
|
+
if (isTraceMode()) {
|
|
32
|
+
defaultOptions.push('-v');
|
|
33
|
+
}
|
|
34
|
+
return [...defaultOptions, ...args];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stripAnsi(text = '') {
|
|
38
|
+
return String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractInstallErrorDetail(rawOutput = '') {
|
|
42
|
+
const text = stripAnsi(rawOutput);
|
|
43
|
+
|
|
44
|
+
const detailMatch = text.match(/detail:"((?:\\"|[^"])*)"/);
|
|
45
|
+
if (detailMatch && detailMatch[1]) {
|
|
46
|
+
return detailMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const descMatch = text.match(/rpc error:\s*code\s*=\s*\S+\s*desc\s*=\s*([^\n\r]+)/i);
|
|
50
|
+
if (descMatch && descMatch[1]) {
|
|
51
|
+
return descMatch[1].trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines = text
|
|
55
|
+
.split(/\r?\n/)
|
|
56
|
+
.map((line) => line.trim())
|
|
57
|
+
.filter((line) => {
|
|
58
|
+
if (!line) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (line.startsWith('Usage:') || line.startsWith('Flags:') || line.startsWith('cmd:')) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
});
|
|
66
|
+
if (lines.length > 0) {
|
|
67
|
+
return lines[0];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function streamInstallOutput(child, { printStdout = true, printStderr = false } = {}) {
|
|
74
|
+
let stdout = '';
|
|
75
|
+
let stderr = '';
|
|
76
|
+
|
|
77
|
+
child.stdout?.on('data', (chunk) => {
|
|
78
|
+
const text = String(chunk ?? '');
|
|
79
|
+
stdout += text;
|
|
80
|
+
if (printStdout) {
|
|
81
|
+
process.stdout.write(text);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
child.stderr?.on('data', (chunk) => {
|
|
85
|
+
const text = String(chunk ?? '');
|
|
86
|
+
stderr += text;
|
|
87
|
+
if (printStderr) {
|
|
88
|
+
process.stderr.write(text);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
getStdout: () => stdout,
|
|
94
|
+
getStderr: () => stderr,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeErrorMessage(error) {
|
|
99
|
+
if (!error) {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
if (typeof error === 'string') {
|
|
103
|
+
return error.trim();
|
|
104
|
+
}
|
|
105
|
+
if (typeof error.message === 'string') {
|
|
106
|
+
return error.message.trim();
|
|
107
|
+
}
|
|
108
|
+
return String(error).trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function shellEscapeArg(value) {
|
|
112
|
+
const text = String(value ?? '');
|
|
113
|
+
if (text === '') {
|
|
114
|
+
return "''";
|
|
115
|
+
}
|
|
116
|
+
if (/^[A-Za-z0-9_/.:=,@+-]+$/.test(text)) {
|
|
117
|
+
return text;
|
|
118
|
+
}
|
|
119
|
+
return "'" + text.replace(/'/g, "'\\''") + "'";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function buildSSHRemoteCommand(args = []) {
|
|
123
|
+
return args.map((arg) => shellEscapeArg(arg)).join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
25
126
|
export class DebugBridge {
|
|
26
|
-
constructor() {
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
127
|
+
constructor(baseDir = process.cwd(), buildRemote = undefined) {
|
|
128
|
+
this.buildRemote = buildRemote === undefined ? resolveBuildRemoteFromFile(baseDir) : buildRemote;
|
|
129
|
+
if (this.buildRemote) {
|
|
130
|
+
this.uid = '';
|
|
131
|
+
this.boxname = this.buildRemote.boxname;
|
|
132
|
+
} else {
|
|
133
|
+
this.uid = shellApi.uid;
|
|
134
|
+
this.boxname = shellApi.boxname;
|
|
135
|
+
}
|
|
29
136
|
this.domain = `dev.${this.boxname}.heiyu.space`;
|
|
30
137
|
this.checkUseResolve = !!process.env[`${_SYSTEM_ENV_PREFIX}_CHECK_DNS_RESOLVE`];
|
|
31
|
-
this.sshPrivateKeyPath = '';
|
|
32
138
|
}
|
|
33
139
|
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
140
|
+
isBuildRemoteMode() {
|
|
141
|
+
return !!this.buildRemote;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
remoteSshArgsRaw({ tty = false } = {}) {
|
|
145
|
+
if (!this.buildRemote) {
|
|
146
|
+
throw new Error('build remote is not configured');
|
|
147
|
+
}
|
|
148
|
+
const args = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'ControlMaster=no', '-q', '-p', String(this.buildRemote.sshPort)];
|
|
149
|
+
if (isTraceMode()) {
|
|
150
|
+
args.push('-v');
|
|
151
|
+
}
|
|
152
|
+
if (tty) {
|
|
153
|
+
args.push('-t');
|
|
40
154
|
}
|
|
155
|
+
args.push(this.buildRemote.sshTarget);
|
|
156
|
+
return args;
|
|
41
157
|
}
|
|
42
158
|
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
159
|
+
remoteBridgeArgs(commandArgs = [], { tty = false } = {}) {
|
|
160
|
+
const args = [...this.remoteSshArgsRaw({ tty })];
|
|
161
|
+
args.push('lzc-docker', 'exec', tty ? '-it' : '-i', DEBUG_BRIDGE_CONTAINER, DEBUG_BRIDGE_BINARY, ...commandArgs);
|
|
162
|
+
return args;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
remoteBridgeExec(commandArgs = [], { tty = false } = {}) {
|
|
166
|
+
return spawn.sync(sshBinary(), this.remoteBridgeArgs(commandArgs, { tty }), {
|
|
167
|
+
shell: false,
|
|
168
|
+
encoding: 'utf-8',
|
|
169
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
remoteHostExec(argv = []) {
|
|
174
|
+
return spawn.sync(sshBinary(), [...this.remoteSshArgsRaw(), ...argv], {
|
|
175
|
+
shell: false,
|
|
176
|
+
encoding: 'utf-8',
|
|
177
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async remoteCommon(commandArgs = []) {
|
|
182
|
+
const ssh = this.remoteBridgeExec(commandArgs);
|
|
183
|
+
logger.debug(`run remote command: ${commandArgs.join(' ')}`);
|
|
184
|
+
if (ssh.status === 0) {
|
|
185
|
+
return ssh.stdout;
|
|
47
186
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
187
|
+
throw new Error(`remote command failed: ${commandArgs.join(' ')}\n${ssh.stdout ?? ''}\n${ssh.stderr ?? ''}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
collectUIDCandidatesFromContainerEnv() {
|
|
191
|
+
const candidates = new Set();
|
|
192
|
+
const ps = this.remoteHostExec(['lzc-docker', 'ps', '--format', '{{.Names}}']);
|
|
193
|
+
if (ps.status !== 0) {
|
|
194
|
+
return [];
|
|
53
195
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
196
|
+
const containers = String(ps.stdout ?? '')
|
|
197
|
+
.split(/\r?\n/)
|
|
198
|
+
.map((item) => item.trim())
|
|
199
|
+
.filter((item) => item);
|
|
200
|
+
for (const container of containers.slice(0, 48)) {
|
|
201
|
+
const envResult = this.remoteHostExec(['lzc-docker', 'exec', '-i', container, 'env']);
|
|
202
|
+
if (envResult.status !== 0) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
String(envResult.stdout ?? '')
|
|
206
|
+
.split(/\r?\n/)
|
|
207
|
+
.map((line) => line.trim())
|
|
208
|
+
.forEach((line) => {
|
|
209
|
+
if (!line.startsWith('LAZYCAT_USER_ID=') && !line.startsWith('LAZYCAT_APP_DEPLOY_UID=')) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const value = String(line.split('=').slice(1).join('=') ?? '').trim();
|
|
213
|
+
if (value) {
|
|
214
|
+
candidates.add(value);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
60
217
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
218
|
+
return [...candidates];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async probeLegacyUID() {
|
|
222
|
+
const candidates = [];
|
|
223
|
+
const appendCandidate = (value) => {
|
|
224
|
+
const uid = String(value ?? '').trim();
|
|
225
|
+
if (!uid) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!candidates.includes(uid)) {
|
|
229
|
+
candidates.push(uid);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
this.collectUIDCandidatesFromContainerEnv().forEach((item) => appendCandidate(item));
|
|
234
|
+
appendCandidate(process.env.BOX_UID);
|
|
235
|
+
['c', 'admin', 'root'].forEach((item) => appendCandidate(item));
|
|
236
|
+
'abcdefghijklmnopqrstuvwxyz0123456789'.split('').forEach((item) => appendCandidate(item));
|
|
237
|
+
|
|
238
|
+
for (const uid of candidates) {
|
|
239
|
+
const probe = this.remoteBridgeExec(['info', '--uid', uid, DEBUG_BRIDGE_APP_ID]);
|
|
240
|
+
if (probe.status !== 0) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(String(probe.stdout ?? '').trim());
|
|
245
|
+
if (parsed?.appid === DEBUG_BRIDGE_APP_ID) {
|
|
246
|
+
logger.debug(`resolved legacy uid by probe: ${uid}`);
|
|
247
|
+
return uid;
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
logger.debug(`legacy uid probe got non-json output, use uid candidate: ${uid}`);
|
|
251
|
+
return uid;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async resolveCurrentUID() {
|
|
259
|
+
if (!this.isBuildRemoteMode()) {
|
|
260
|
+
return this.uid;
|
|
261
|
+
}
|
|
262
|
+
const cached = String(this.uid ?? '').trim();
|
|
263
|
+
if (cached) {
|
|
264
|
+
return cached;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const uidReply = this.remoteBridgeExec(['uid']);
|
|
268
|
+
if (uidReply.status === 0) {
|
|
269
|
+
const resolved = String(uidReply.stdout ?? '').trim();
|
|
270
|
+
if (!resolved) {
|
|
271
|
+
throw new Error('resolve uid failed: empty output');
|
|
272
|
+
}
|
|
273
|
+
this.uid = resolved;
|
|
274
|
+
return resolved;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const legacyUID = await this.probeLegacyUID();
|
|
278
|
+
if (legacyUID) {
|
|
279
|
+
this.uid = legacyUID;
|
|
280
|
+
return legacyUID;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
throw new Error(`resolve uid failed: ${String(uidReply.stderr ?? uidReply.stdout ?? '').trim() || 'unknown error'}`);
|
|
68
284
|
}
|
|
69
285
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
286
|
+
async remoteCommandWithUID(commandArgs = []) {
|
|
287
|
+
const uid = await this.resolveCurrentUID();
|
|
288
|
+
const args = [...commandArgs];
|
|
289
|
+
args.splice(1, 0, '--uid', uid);
|
|
290
|
+
return args;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async resolveDevId() {
|
|
294
|
+
if (this.isBuildRemoteMode()) {
|
|
295
|
+
return '';
|
|
296
|
+
}
|
|
297
|
+
return shellApi.resolveClientId();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async init() {
|
|
301
|
+
await this.checkDevTools();
|
|
302
|
+
if (!(await this.canPublicKey())) {
|
|
303
|
+
if (this.isBuildRemoteMode()) {
|
|
304
|
+
throw new Error(`ssh login failed for remote ${this.buildRemote.sshTarget}:${this.buildRemote.sshPort}`);
|
|
305
|
+
}
|
|
306
|
+
// 如果不能 ssh public key 登录则提示授权申请,否则后面可能会出现 rsync 询问密码的问题
|
|
307
|
+
await this.sshApplyGrant();
|
|
308
|
+
}
|
|
73
309
|
}
|
|
74
310
|
|
|
75
311
|
async checkDevTools() {
|
|
312
|
+
if (this.isBuildRemoteMode()) {
|
|
313
|
+
try {
|
|
314
|
+
await this.remoteCommon(['version']);
|
|
315
|
+
return;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const detail = normalizeErrorMessage(error);
|
|
318
|
+
if (detail.includes(`No such container: ${DEBUG_BRIDGE_CONTAINER}`)) {
|
|
319
|
+
throw new Error(`Lazycat Developer Tools is not running on target box "${this.boxname}". Start app "cloud.lazycat.developer.tools" and retry.`);
|
|
320
|
+
}
|
|
321
|
+
if (detail.includes('is not running')) {
|
|
322
|
+
throw new Error(`Lazycat Developer Tools container is not running on target box "${this.boxname}". Start app "cloud.lazycat.developer.tools" and retry.`);
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`Failed to check Lazycat Developer Tools in remote mode.\n${detail || 'unknown error'}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
76
328
|
let domain = this.domain;
|
|
77
329
|
if (this.checkUseResolve) {
|
|
78
330
|
try {
|
|
@@ -134,22 +386,65 @@ export class DebugBridge {
|
|
|
134
386
|
|
|
135
387
|
async install(lpkPath, pkgId) {
|
|
136
388
|
const stream = fs.createReadStream(lpkPath);
|
|
389
|
+
if (this.isBuildRemoteMode()) {
|
|
390
|
+
const commandArgs = await this.remoteCommandWithUID(['install']);
|
|
391
|
+
if (pkgId) {
|
|
392
|
+
commandArgs.push('--pkgId', pkgId);
|
|
393
|
+
}
|
|
394
|
+
const ssh = spawn(sshBinary(), this.remoteBridgeArgs(commandArgs), {
|
|
395
|
+
shell: false,
|
|
396
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
397
|
+
});
|
|
398
|
+
const output = streamInstallOutput(ssh, { printStdout: false, printStderr: false });
|
|
399
|
+
stream.pipe(ssh.stdin);
|
|
400
|
+
return new Promise((resolve, reject) => {
|
|
401
|
+
ssh.on('error', (error) => {
|
|
402
|
+
reject(error);
|
|
403
|
+
});
|
|
404
|
+
ssh.on('close', (code) => {
|
|
405
|
+
if (code == 0) {
|
|
406
|
+
resolve();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const detail = extractInstallErrorDetail(`${output.getStdout()}\n${output.getStderr()}`);
|
|
410
|
+
reject(detail || t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败'));
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
137
415
|
const resolvedIp = await resolveDomain(this.domain);
|
|
138
|
-
const ssh = spawn(sshBinary(), [...
|
|
416
|
+
const ssh = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), `install --uid ${this.uid}`, pkgId ? `--pkgId ${pkgId}` : ''], {
|
|
139
417
|
shell: true,
|
|
140
|
-
stdio: ['pipe', '
|
|
418
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
141
419
|
});
|
|
420
|
+
const output = streamInstallOutput(ssh, { printStdout: false, printStderr: false });
|
|
142
421
|
stream.pipe(ssh.stdin);
|
|
143
422
|
return new Promise((resolve, reject) => {
|
|
423
|
+
ssh.on('error', (error) => {
|
|
424
|
+
reject(error);
|
|
425
|
+
});
|
|
144
426
|
ssh.on('close', (code) => {
|
|
145
|
-
code == 0
|
|
427
|
+
if (code == 0) {
|
|
428
|
+
resolve();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const detail = extractInstallErrorDetail(`${output.getStdout()}\n${output.getStderr()}`);
|
|
432
|
+
reject(detail || t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败'));
|
|
146
433
|
});
|
|
147
434
|
});
|
|
148
435
|
}
|
|
149
436
|
|
|
150
437
|
async canPublicKey() {
|
|
438
|
+
if (this.isBuildRemoteMode()) {
|
|
439
|
+
const ssh = spawn.sync(sshBinary(), [...this.remoteSshArgsRaw(), 'true'], {
|
|
440
|
+
shell: false,
|
|
441
|
+
encoding: 'utf-8',
|
|
442
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
443
|
+
});
|
|
444
|
+
return ssh.status === 0;
|
|
445
|
+
}
|
|
151
446
|
try {
|
|
152
|
-
await this.common(sshBinary(), [...
|
|
447
|
+
await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`)]);
|
|
153
448
|
return true;
|
|
154
449
|
} catch (err) {
|
|
155
450
|
logger.debug('canPublicKey error: ', err);
|
|
@@ -162,6 +457,9 @@ export class DebugBridge {
|
|
|
162
457
|
}
|
|
163
458
|
|
|
164
459
|
async sshApplyGrant() {
|
|
460
|
+
if (this.isBuildRemoteMode()) {
|
|
461
|
+
throw new Error('build remote mode requires ssh key authorization in host ssh service');
|
|
462
|
+
}
|
|
165
463
|
const keys = await findSshPublicKey();
|
|
166
464
|
logger.info(t('lzc_cli.lib.debug_bridge.ssh_apply_grant_not_exist_tips', '检测到您当前的环境还没有添加 ssh 公钥到 ‘懒猫开发者工具’ 中,请选择您需要添加的公钥类型'));
|
|
167
465
|
const sshInfo = await selectSshPublicKey(keys);
|
|
@@ -188,21 +486,75 @@ export class DebugBridge {
|
|
|
188
486
|
}
|
|
189
487
|
|
|
190
488
|
async status(appId) {
|
|
191
|
-
|
|
489
|
+
if (this.isBuildRemoteMode()) {
|
|
490
|
+
return this.remoteCommon(await this.remoteCommandWithUID(['status', appId]));
|
|
491
|
+
}
|
|
492
|
+
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `status --uid ${this.uid}`, appId]);
|
|
192
493
|
}
|
|
193
494
|
|
|
495
|
+
async info(appId) {
|
|
496
|
+
let stdout;
|
|
497
|
+
if (this.isBuildRemoteMode()) {
|
|
498
|
+
stdout = await this.remoteCommon(await this.remoteCommandWithUID(['info', appId]));
|
|
499
|
+
} else {
|
|
500
|
+
stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `info --uid ${this.uid}`, appId]);
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
return JSON.parse(stdout);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
throw new Error(`parse app info failed: ${error.message}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async syncDevID(appId, devId = '', userApp = false) {
|
|
510
|
+
const normalizedDevId = String(devId ?? '').trim();
|
|
511
|
+
if (this.isBuildRemoteMode()) {
|
|
512
|
+
const commandArgs = await this.remoteCommandWithUID(['sync-dev-id']);
|
|
513
|
+
if (normalizedDevId) {
|
|
514
|
+
commandArgs.push('--dev-id', normalizedDevId);
|
|
515
|
+
}
|
|
516
|
+
if (userApp) {
|
|
517
|
+
commandArgs.push('--userapp');
|
|
518
|
+
}
|
|
519
|
+
commandArgs.push(appId);
|
|
520
|
+
return this.remoteCommon(commandArgs);
|
|
521
|
+
}
|
|
522
|
+
const rawArgs = ['sync-dev-id', `--uid ${this.uid}`];
|
|
523
|
+
if (normalizedDevId) {
|
|
524
|
+
rawArgs.push(`--dev-id ${normalizedDevId}`);
|
|
525
|
+
}
|
|
526
|
+
if (userApp) {
|
|
527
|
+
rawArgs.push('--userapp');
|
|
528
|
+
}
|
|
529
|
+
rawArgs.push(appId);
|
|
530
|
+
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), rawArgs.join(' ')]);
|
|
531
|
+
}
|
|
194
532
|
async isDevshell(appId) {
|
|
195
533
|
await this.backendVersion020();
|
|
196
|
-
|
|
534
|
+
if (this.isBuildRemoteMode()) {
|
|
535
|
+
const stdout = await this.remoteCommon(await this.remoteCommandWithUID(['isDevshellV2', appId]));
|
|
536
|
+
return stdout.trim() == 'true';
|
|
537
|
+
}
|
|
538
|
+
const stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `isDevshellV2 --uid ${this.uid}`, appId]);
|
|
197
539
|
return stdout == 'true';
|
|
198
540
|
}
|
|
199
541
|
|
|
200
542
|
async resume(appId) {
|
|
201
|
-
|
|
543
|
+
if (this.isBuildRemoteMode()) {
|
|
544
|
+
return this.remoteCommon(await this.remoteCommandWithUID(['resume', appId]));
|
|
545
|
+
}
|
|
546
|
+
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `resume --uid ${this.uid}`, appId]);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async pause(appId) {
|
|
550
|
+
if (this.isBuildRemoteMode()) {
|
|
551
|
+
return this.remoteCommon(await this.remoteCommandWithUID(['pause', appId]));
|
|
552
|
+
}
|
|
553
|
+
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `pause --uid ${this.uid}`, appId]);
|
|
202
554
|
}
|
|
203
555
|
|
|
204
556
|
async version() {
|
|
205
|
-
const output = await this.common(sshBinary(), [...
|
|
557
|
+
const output = this.isBuildRemoteMode() ? await this.remoteCommon(['version']) : await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `version`]);
|
|
206
558
|
logger.debug(`backend version:\n${output}`);
|
|
207
559
|
try {
|
|
208
560
|
const data = JSON.parse(output);
|
|
@@ -213,7 +565,15 @@ export class DebugBridge {
|
|
|
213
565
|
}
|
|
214
566
|
|
|
215
567
|
async uninstall(appId, deleteAppData = false) {
|
|
216
|
-
|
|
568
|
+
if (this.isBuildRemoteMode()) {
|
|
569
|
+
const commandArgs = await this.remoteCommandWithUID(['uninstall']);
|
|
570
|
+
if (deleteAppData) {
|
|
571
|
+
commandArgs.push('--delete-data');
|
|
572
|
+
}
|
|
573
|
+
commandArgs.push(appId);
|
|
574
|
+
return this.remoteCommon(commandArgs);
|
|
575
|
+
}
|
|
576
|
+
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `uninstall --uid ${this.uid}`, deleteAppData ? '--delete-data' : '', appId]);
|
|
217
577
|
}
|
|
218
578
|
|
|
219
579
|
async devshell(appId, isUserApp, onconnect = null) {
|
|
@@ -229,25 +589,28 @@ export class DebugBridge {
|
|
|
229
589
|
await sleep(100);
|
|
230
590
|
}
|
|
231
591
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
isUserApp ? '--userapp' : '',
|
|
242
|
-
appId,
|
|
243
|
-
'/bin/sh',
|
|
244
|
-
'/lzcapp/pkg/content/devshell/exec.sh',
|
|
245
|
-
],
|
|
246
|
-
{
|
|
247
|
-
shell: true,
|
|
592
|
+
let stream;
|
|
593
|
+
if (this.isBuildRemoteMode()) {
|
|
594
|
+
const commandArgs = await this.remoteCommandWithUID(['devshell']);
|
|
595
|
+
if (isUserApp) {
|
|
596
|
+
commandArgs.push('--userapp');
|
|
597
|
+
}
|
|
598
|
+
commandArgs.push(appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh');
|
|
599
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(commandArgs, { tty: true }), {
|
|
600
|
+
shell: false,
|
|
248
601
|
stdio: 'inherit',
|
|
249
|
-
}
|
|
250
|
-
|
|
602
|
+
});
|
|
603
|
+
} else {
|
|
604
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
605
|
+
stream = spawn(
|
|
606
|
+
sshBinary(),
|
|
607
|
+
[...sshCmdArgs(`box@${resolvedIp}`), '-t', 'devshell', `--uid ${this.uid}`, isUserApp ? '--userapp' : '', appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh'],
|
|
608
|
+
{
|
|
609
|
+
shell: true,
|
|
610
|
+
stdio: 'inherit',
|
|
611
|
+
},
|
|
612
|
+
);
|
|
613
|
+
}
|
|
251
614
|
return new Promise((resolve, reject) => {
|
|
252
615
|
stream.on('close', (code) => {
|
|
253
616
|
code == 0 ? resolve() : reject();
|
|
@@ -261,51 +624,496 @@ export class DebugBridge {
|
|
|
261
624
|
}
|
|
262
625
|
|
|
263
626
|
async buildImage(label, contextTar) {
|
|
264
|
-
const backendVersion = await this.version();
|
|
265
|
-
|
|
266
627
|
const tag = `debug.bridge/${label}`;
|
|
267
|
-
const resolvedIp = await resolveDomain(this.domain);
|
|
268
628
|
const stream = fs.createReadStream(contextTar);
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
629
|
+
let buildStream;
|
|
630
|
+
const buildArgs = ['build', '--tag', tag];
|
|
631
|
+
if (this.isBuildRemoteMode()) {
|
|
632
|
+
buildStream = spawn(sshBinary(), this.remoteBridgeArgs(buildArgs), {
|
|
633
|
+
shell: false,
|
|
634
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
635
|
+
});
|
|
636
|
+
} else {
|
|
637
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
638
|
+
buildStream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), ['build', '--tag', tag].join(' ')], {
|
|
639
|
+
shell: true,
|
|
640
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
641
|
+
});
|
|
642
|
+
}
|
|
274
643
|
stream.pipe(buildStream.stdin);
|
|
275
644
|
return new Promise((resolve, reject) => {
|
|
276
645
|
buildStream.on('close', (code) => {
|
|
277
|
-
code == 0
|
|
278
|
-
? resolve(compareVersions('0.1.12', backendVersion) >= 0 ? `127.0.0.1:5000/${tag}` : `dev.${this.boxname}.heiyu.space/${tag}`)
|
|
279
|
-
: reject(t('lzc_cli.lib.debug_bridge.build_image_fail', `在微服中构建 image 失败`));
|
|
646
|
+
code == 0 ? resolve(tag) : reject(t('lzc_cli.lib.debug_bridge.build_image_fail', `在微服中构建 image 失败`));
|
|
280
647
|
});
|
|
281
648
|
}).finally(() => {
|
|
282
649
|
fs.rmSync(contextTar);
|
|
283
650
|
});
|
|
284
651
|
}
|
|
285
652
|
|
|
653
|
+
async buildImageForPack(label, contextTar) {
|
|
654
|
+
const tag = `debug.bridge/${label}`;
|
|
655
|
+
const stream = fs.createReadStream(contextTar);
|
|
656
|
+
let buildStream;
|
|
657
|
+
const isBuildPackResultLine = (line) => {
|
|
658
|
+
const trimmed = String(line ?? '').trim();
|
|
659
|
+
if (!trimmed || !trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
const parsed = JSON.parse(trimmed);
|
|
664
|
+
return (
|
|
665
|
+
parsed &&
|
|
666
|
+
typeof parsed === 'object' &&
|
|
667
|
+
typeof parsed.tag === 'string' &&
|
|
668
|
+
typeof parsed.archiveKey === 'string' &&
|
|
669
|
+
typeof parsed.imageID === 'string' &&
|
|
670
|
+
Array.isArray(parsed.diffIDs)
|
|
671
|
+
);
|
|
672
|
+
} catch {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
if (this.isBuildRemoteMode()) {
|
|
677
|
+
buildStream = spawn(sshBinary(), this.remoteBridgeArgs(['build-pack', '--tag', tag]), {
|
|
678
|
+
shell: false,
|
|
679
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
680
|
+
});
|
|
681
|
+
} else {
|
|
682
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
683
|
+
buildStream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'build-pack', '--tag', tag], {
|
|
684
|
+
shell: false,
|
|
685
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
stream.pipe(buildStream.stdin);
|
|
689
|
+
|
|
690
|
+
return new Promise((resolve, reject) => {
|
|
691
|
+
let output = '';
|
|
692
|
+
let stdoutLineBuffer = '';
|
|
693
|
+
buildStream.stdout.on('data', (chunk) => {
|
|
694
|
+
const text = chunk.toString();
|
|
695
|
+
output += text;
|
|
696
|
+
stdoutLineBuffer += text;
|
|
697
|
+
while (true) {
|
|
698
|
+
const lineEndIndex = stdoutLineBuffer.indexOf('\n');
|
|
699
|
+
if (lineEndIndex < 0) {
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
const lineWithNewline = stdoutLineBuffer.slice(0, lineEndIndex + 1);
|
|
703
|
+
stdoutLineBuffer = stdoutLineBuffer.slice(lineEndIndex + 1);
|
|
704
|
+
const line = lineWithNewline.replace(/\r?\n$/, '');
|
|
705
|
+
if (!isBuildPackResultLine(line)) {
|
|
706
|
+
process.stdout.write(lineWithNewline);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
buildStream.on('error', (error) => {
|
|
711
|
+
reject(error);
|
|
712
|
+
});
|
|
713
|
+
buildStream.on('close', (code) => {
|
|
714
|
+
if (stdoutLineBuffer) {
|
|
715
|
+
if (!isBuildPackResultLine(stdoutLineBuffer)) {
|
|
716
|
+
process.stdout.write(stdoutLineBuffer);
|
|
717
|
+
}
|
|
718
|
+
stdoutLineBuffer = '';
|
|
719
|
+
}
|
|
720
|
+
if (code !== 0) {
|
|
721
|
+
reject(new Error('build-pack failed'));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const line = output
|
|
726
|
+
.split(/\r?\n/)
|
|
727
|
+
.map((item) => item.trim())
|
|
728
|
+
.filter((item) => item !== '')
|
|
729
|
+
.at(-1);
|
|
730
|
+
if (!line) {
|
|
731
|
+
throw new Error('build-pack output is empty');
|
|
732
|
+
}
|
|
733
|
+
const payload = JSON.parse(line);
|
|
734
|
+
resolve(payload);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
reject(new Error(`parse build-pack output failed: ${error.message}`));
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}).finally(() => {
|
|
740
|
+
fs.rmSync(contextTar, { force: true });
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
286
744
|
async lzcDocker(argv) {
|
|
287
745
|
await this.backendVersion020();
|
|
746
|
+
let stream;
|
|
747
|
+
if (this.isBuildRemoteMode()) {
|
|
748
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv], { tty: true }), {
|
|
749
|
+
shell: false,
|
|
750
|
+
stdio: 'inherit',
|
|
751
|
+
});
|
|
752
|
+
} else {
|
|
753
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
754
|
+
const remoteCommand = buildSSHRemoteCommand(['lzc-docker', ...argv]);
|
|
755
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), '-t', remoteCommand], {
|
|
756
|
+
shell: false,
|
|
757
|
+
stdio: 'inherit',
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
return new Promise((resolve, reject) => {
|
|
761
|
+
stream.on('close', (code) => {
|
|
762
|
+
code == 0 ? resolve() : reject();
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
}
|
|
288
766
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
767
|
+
async lzcDockerPipe(argv, stdinStream = null) {
|
|
768
|
+
await this.backendVersion020();
|
|
769
|
+
let stream;
|
|
770
|
+
if (this.isBuildRemoteMode()) {
|
|
771
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv]), {
|
|
772
|
+
shell: false,
|
|
773
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
774
|
+
});
|
|
775
|
+
} else {
|
|
776
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
777
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', ...argv], {
|
|
778
|
+
shell: false,
|
|
779
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return await new Promise((resolve, reject) => {
|
|
784
|
+
let done = false;
|
|
785
|
+
const fail = (error) => {
|
|
786
|
+
if (done) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
done = true;
|
|
790
|
+
try {
|
|
791
|
+
stream.kill('SIGKILL');
|
|
792
|
+
} catch {}
|
|
793
|
+
reject(error);
|
|
794
|
+
};
|
|
795
|
+
stream.on('error', (error) => fail(error));
|
|
796
|
+
stream.on('close', (code) => {
|
|
797
|
+
if (done) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
done = true;
|
|
801
|
+
code == 0 ? resolve() : reject(t('lzc_cli.lib.debug_bridge.lzc_docker_capture_fail', 'lzc-docker command failed'));
|
|
802
|
+
});
|
|
803
|
+
if (stdinStream) {
|
|
804
|
+
stdinStream.on('error', (error) => fail(error));
|
|
805
|
+
stdinStream.pipe(stream.stdin);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
stream.stdin.end();
|
|
293
809
|
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async lzcDockerPull(image) {
|
|
813
|
+
await this.backendVersion020();
|
|
814
|
+
let stream;
|
|
815
|
+
if (this.isBuildRemoteMode()) {
|
|
816
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', 'pull', image]), {
|
|
817
|
+
shell: false,
|
|
818
|
+
stdio: 'inherit',
|
|
819
|
+
});
|
|
820
|
+
} else {
|
|
821
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
822
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', 'pull', image], {
|
|
823
|
+
shell: false,
|
|
824
|
+
stdio: 'inherit',
|
|
825
|
+
});
|
|
826
|
+
}
|
|
294
827
|
return new Promise((resolve, reject) => {
|
|
295
828
|
stream.on('close', (code) => {
|
|
296
|
-
code == 0 ? resolve() : reject();
|
|
829
|
+
code == 0 ? resolve() : reject(t('lzc_cli.lib.debug_bridge.lzc_docker_pull_fail', `lzc-docker pull 失败: ${image}`));
|
|
297
830
|
});
|
|
298
831
|
});
|
|
299
832
|
}
|
|
300
833
|
|
|
301
|
-
async
|
|
834
|
+
async lzcDockerSave(images, outputPath) {
|
|
302
835
|
await this.backendVersion020();
|
|
836
|
+
if (!images || images.length == 0) {
|
|
837
|
+
throw t('lzc_cli.lib.debug_bridge.lzc_docker_save_images_empty_fail', 'images 不能为空');
|
|
838
|
+
}
|
|
839
|
+
const output = fs.createWriteStream(outputPath);
|
|
840
|
+
let stream;
|
|
841
|
+
if (this.isBuildRemoteMode()) {
|
|
842
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', 'image', 'save', ...images]), {
|
|
843
|
+
shell: false,
|
|
844
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
845
|
+
});
|
|
846
|
+
} else {
|
|
847
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
848
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', 'image', 'save', ...images], {
|
|
849
|
+
shell: false,
|
|
850
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
stream.stdout.pipe(output);
|
|
854
|
+
|
|
855
|
+
return new Promise((resolve, reject) => {
|
|
856
|
+
let done = false;
|
|
857
|
+
const fail = (err) => {
|
|
858
|
+
if (done) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
done = true;
|
|
862
|
+
try {
|
|
863
|
+
stream.kill('SIGKILL');
|
|
864
|
+
} catch {}
|
|
865
|
+
try {
|
|
866
|
+
output.destroy();
|
|
867
|
+
} catch {}
|
|
868
|
+
try {
|
|
869
|
+
fs.rmSync(outputPath, { force: true });
|
|
870
|
+
} catch {}
|
|
871
|
+
reject(err);
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
stream.on('error', (e) => {
|
|
875
|
+
fail(e);
|
|
876
|
+
});
|
|
877
|
+
output.on('error', (e) => {
|
|
878
|
+
fail(e);
|
|
879
|
+
});
|
|
880
|
+
stream.on('close', (code) => {
|
|
881
|
+
if (done) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
output.end(() => {
|
|
885
|
+
if (code == 0) {
|
|
886
|
+
done = true;
|
|
887
|
+
resolve();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
fail(t('lzc_cli.lib.debug_bridge.lzc_docker_save_fail', 'lzc-docker image save 失败'));
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async packImages(imagesSpec, outputPath) {
|
|
897
|
+
await this.backendVersion020();
|
|
898
|
+
if (!Array.isArray(imagesSpec) || imagesSpec.length === 0) {
|
|
899
|
+
throw new Error('imagesSpec cannot be empty');
|
|
900
|
+
}
|
|
901
|
+
const spec = Buffer.from(
|
|
902
|
+
JSON.stringify({
|
|
903
|
+
images: imagesSpec,
|
|
904
|
+
}),
|
|
905
|
+
).toString('base64');
|
|
906
|
+
|
|
907
|
+
const output = fs.createWriteStream(outputPath);
|
|
908
|
+
let stream;
|
|
909
|
+
if (this.isBuildRemoteMode()) {
|
|
910
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['pack-images', '--spec', spec]), {
|
|
911
|
+
shell: false,
|
|
912
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
913
|
+
});
|
|
914
|
+
} else {
|
|
915
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
916
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'pack-images', '--spec', spec], {
|
|
917
|
+
shell: false,
|
|
918
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
stream.stdout.pipe(output);
|
|
922
|
+
|
|
923
|
+
return new Promise((resolve, reject) => {
|
|
924
|
+
let done = false;
|
|
925
|
+
const fail = (err) => {
|
|
926
|
+
if (done) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
done = true;
|
|
930
|
+
try {
|
|
931
|
+
stream.kill('SIGKILL');
|
|
932
|
+
} catch {}
|
|
933
|
+
try {
|
|
934
|
+
output.destroy();
|
|
935
|
+
} catch {}
|
|
936
|
+
try {
|
|
937
|
+
fs.rmSync(outputPath, { force: true });
|
|
938
|
+
} catch {}
|
|
939
|
+
reject(err);
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
stream.on('error', (e) => {
|
|
943
|
+
fail(e);
|
|
944
|
+
});
|
|
945
|
+
output.on('error', (e) => {
|
|
946
|
+
fail(e);
|
|
947
|
+
});
|
|
948
|
+
stream.on('close', (code) => {
|
|
949
|
+
if (done) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
output.end(() => {
|
|
953
|
+
if (code == 0) {
|
|
954
|
+
done = true;
|
|
955
|
+
resolve();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
fail(new Error('pack-images failed'));
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async lzcDockerCapture(argv, options = {}) {
|
|
965
|
+
await this.backendVersion020();
|
|
966
|
+
const stdinText = options && Object.prototype.hasOwnProperty.call(options, 'stdinText') ? String(options.stdinText ?? '') : '';
|
|
967
|
+
let stream;
|
|
968
|
+
if (this.isBuildRemoteMode()) {
|
|
969
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv]), {
|
|
970
|
+
shell: false,
|
|
971
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
972
|
+
});
|
|
973
|
+
} else {
|
|
974
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
975
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', ...argv], {
|
|
976
|
+
shell: false,
|
|
977
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return await new Promise((resolve, reject) => {
|
|
982
|
+
let output = '';
|
|
983
|
+
if (stdinText) {
|
|
984
|
+
stream.stdin.write(stdinText);
|
|
985
|
+
}
|
|
986
|
+
stream.stdin.end();
|
|
987
|
+
stream.stdout.on('data', (chunk) => {
|
|
988
|
+
output += chunk.toString();
|
|
989
|
+
});
|
|
990
|
+
stream.on('error', (err) => reject(err));
|
|
991
|
+
stream.on('close', (code) => {
|
|
992
|
+
if (code == 0) {
|
|
993
|
+
resolve(output.trim());
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
reject(t('lzc_cli.lib.debug_bridge.lzc_docker_capture_fail', 'lzc-docker command failed'));
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async lzcDockerComposeCapture(argv) {
|
|
1002
|
+
await this.backendVersion020();
|
|
1003
|
+
let stream;
|
|
1004
|
+
if (this.isBuildRemoteMode()) {
|
|
1005
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker-compose', ...argv]), {
|
|
1006
|
+
shell: false,
|
|
1007
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1008
|
+
});
|
|
1009
|
+
} else {
|
|
1010
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
1011
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker-compose', ...argv], {
|
|
1012
|
+
shell: false,
|
|
1013
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return await new Promise((resolve, reject) => {
|
|
1018
|
+
let stdout = '';
|
|
1019
|
+
let stderr = '';
|
|
1020
|
+
stream.stdout.on('data', (chunk) => {
|
|
1021
|
+
stdout += chunk.toString();
|
|
1022
|
+
});
|
|
1023
|
+
stream.stderr.on('data', (chunk) => {
|
|
1024
|
+
stderr += chunk.toString();
|
|
1025
|
+
});
|
|
1026
|
+
stream.on('error', (err) => reject(err));
|
|
1027
|
+
stream.on('close', (code) => {
|
|
1028
|
+
if (code == 0) {
|
|
1029
|
+
resolve(stdout.trim());
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
reject(new Error(`lzc-docker-compose command failed\n${stdout}\n${stderr}`));
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async hostReadFile(pathname) {
|
|
1038
|
+
const target = String(pathname ?? '').trim();
|
|
1039
|
+
if (!target) {
|
|
1040
|
+
throw new Error('hostReadFile path is empty');
|
|
1041
|
+
}
|
|
1042
|
+
if (this.isBuildRemoteMode()) {
|
|
1043
|
+
const result = this.remoteHostExec(['cat', target]);
|
|
1044
|
+
if (result.status === 0) {
|
|
1045
|
+
return String(result.stdout ?? '');
|
|
1046
|
+
}
|
|
1047
|
+
throw new Error(`read remote file failed: ${target}`);
|
|
1048
|
+
}
|
|
303
1049
|
|
|
304
1050
|
const resolvedIp = await resolveDomain(this.domain);
|
|
305
|
-
const stream = spawn(sshBinary(), [...
|
|
306
|
-
shell:
|
|
307
|
-
stdio: '
|
|
1051
|
+
const stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'cat', target], {
|
|
1052
|
+
shell: false,
|
|
1053
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1054
|
+
});
|
|
1055
|
+
return await new Promise((resolve, reject) => {
|
|
1056
|
+
let stdout = '';
|
|
1057
|
+
let stderr = '';
|
|
1058
|
+
stream.stdout.on('data', (chunk) => {
|
|
1059
|
+
stdout += chunk.toString();
|
|
1060
|
+
});
|
|
1061
|
+
stream.stderr.on('data', (chunk) => {
|
|
1062
|
+
stderr += chunk.toString();
|
|
1063
|
+
});
|
|
1064
|
+
stream.on('error', (err) => reject(err));
|
|
1065
|
+
stream.on('close', (code) => {
|
|
1066
|
+
if (code === 0) {
|
|
1067
|
+
resolve(stdout);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
reject(new Error(`read host file failed\n${stderr}`));
|
|
1071
|
+
});
|
|
308
1072
|
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async lzcDockerImageRepoDigests(image) {
|
|
1076
|
+
const inspect = await this.lzcDockerImageInspect(image);
|
|
1077
|
+
const repoDigests = inspect?.RepoDigests;
|
|
1078
|
+
if (!Array.isArray(repoDigests)) {
|
|
1079
|
+
return [];
|
|
1080
|
+
}
|
|
1081
|
+
return repoDigests.filter((item) => typeof item === 'string' && item.trim() !== '');
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async lzcDockerImageInspect(image) {
|
|
1085
|
+
const output = await this.lzcDockerCapture(['image', 'inspect', image]);
|
|
1086
|
+
if (!output) {
|
|
1087
|
+
throw new Error(`lzc-docker image inspect output is empty: ${image}`);
|
|
1088
|
+
}
|
|
1089
|
+
let parsed;
|
|
1090
|
+
try {
|
|
1091
|
+
parsed = JSON.parse(output);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
throw new Error(`failed to parse image inspect output for ${image}: ${error.message}`);
|
|
1094
|
+
}
|
|
1095
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed[0] || typeof parsed[0] !== 'object') {
|
|
1096
|
+
throw new Error(`invalid image inspect output for ${image}`);
|
|
1097
|
+
}
|
|
1098
|
+
return parsed[0];
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async lzcDockerCompose(argv) {
|
|
1102
|
+
await this.backendVersion020();
|
|
1103
|
+
let stream;
|
|
1104
|
+
if (this.isBuildRemoteMode()) {
|
|
1105
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker-compose', ...argv], { tty: true }), {
|
|
1106
|
+
shell: false,
|
|
1107
|
+
stdio: 'inherit',
|
|
1108
|
+
});
|
|
1109
|
+
} else {
|
|
1110
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
1111
|
+
const remoteCommand = buildSSHRemoteCommand(['lzc-docker-compose', ...argv]);
|
|
1112
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), '-t', remoteCommand], {
|
|
1113
|
+
shell: false,
|
|
1114
|
+
stdio: 'inherit',
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
309
1117
|
return new Promise((resolve, reject) => {
|
|
310
1118
|
stream.on('close', (code) => {
|
|
311
1119
|
code == 0 ? resolve() : reject();
|