@lazycatcloud/lzc-cli 1.3.13 → 2.0.0-pre.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 +30 -5
- package/changelog.md +16 -0
- package/lib/app/index.js +174 -58
- package/lib/app/lpk_build.js +197 -18
- package/lib/app/lpk_build_images.js +728 -0
- package/lib/app/lpk_create.js +96 -23
- package/lib/app/lpk_create_generator.js +150 -12
- package/lib/app/lpk_devshell.js +35 -21
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +15 -7
- package/lib/app/project_cp.js +64 -0
- package/lib/app/project_deploy.js +33 -0
- package/lib/app/project_exec.js +45 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +67 -0
- package/lib/app/project_runtime.js +261 -0
- package/lib/app/project_start.js +100 -0
- package/lib/appstore/index.js +56 -16
- package/lib/appstore/publish.js +16 -13
- package/lib/box/index.js +103 -6
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +22 -0
- package/lib/config/index.js +4 -3
- package/lib/debug_bridge.js +837 -44
- package/lib/docker/index.js +30 -10
- package/lib/i18n/index.js +1 -0
- package/lib/i18n/locales/en/translation.json +263 -250
- package/lib/i18n/locales/zh/translation.json +57 -44
- package/lib/lpk/core.js +487 -0
- package/lib/lpk/index.js +210 -0
- package/lib/shellapi.js +5 -5
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +17 -12
- package/package.json +4 -3
- package/scripts/cli.js +4 -0
- package/template/_lpk/README.md +11 -3
- package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
- package/template/_lpk/manifest.yml.in +4 -2
- package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
- package/template/_lpk/todolist-java.manifest.yml.in +15 -0
- package/template/_lpk/todolist-python.manifest.yml.in +15 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/_gitignore +1 -0
- package/template/blank/lzc-build.yml +25 -40
- package/template/blank/lzc-manifest.yml +14 -7
- package/template/golang/Dockerfile +19 -0
- package/template/golang/README.md +33 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.yml +21 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/run.sh +3 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +19 -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.yml +23 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/README.md +33 -0
- package/template/python/_gitignore +3 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.yml +21 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +3 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/README.md +33 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/lzc-build.yml +21 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +3 -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 +17 -7
- package/template/vue/_gitignore +1 -0
- package/template/vue/lzc-build.yml +31 -42
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +34 -0
- package/template/vue-minidb/_gitignore +26 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.yml +48 -0
- package/template/vue-minidb/lzc-icon.png +0 -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,23 +26,283 @@ 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
|
+
|
|
25
111
|
export class DebugBridge {
|
|
26
|
-
constructor() {
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
112
|
+
constructor(baseDir = process.cwd(), buildRemote = undefined) {
|
|
113
|
+
this.buildRemote = buildRemote === undefined ? resolveBuildRemoteFromFile(baseDir) : buildRemote;
|
|
114
|
+
if (this.buildRemote) {
|
|
115
|
+
this.uid = '';
|
|
116
|
+
this.boxname = this.buildRemote.boxname;
|
|
117
|
+
} else {
|
|
118
|
+
this.uid = shellApi.uid;
|
|
119
|
+
this.boxname = shellApi.boxname;
|
|
120
|
+
}
|
|
29
121
|
this.domain = `dev.${this.boxname}.heiyu.space`;
|
|
30
122
|
this.checkUseResolve = !!process.env[`${_SYSTEM_ENV_PREFIX}_CHECK_DNS_RESOLVE`];
|
|
31
123
|
}
|
|
32
124
|
|
|
125
|
+
isBuildRemoteMode() {
|
|
126
|
+
return !!this.buildRemote;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
remoteSshArgsRaw({ tty = false } = {}) {
|
|
130
|
+
if (!this.buildRemote) {
|
|
131
|
+
throw new Error('build remote is not configured');
|
|
132
|
+
}
|
|
133
|
+
const args = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'ControlMaster=no', '-q', '-p', String(this.buildRemote.sshPort)];
|
|
134
|
+
if (isTraceMode()) {
|
|
135
|
+
args.push('-v');
|
|
136
|
+
}
|
|
137
|
+
if (tty) {
|
|
138
|
+
args.push('-t');
|
|
139
|
+
}
|
|
140
|
+
args.push(this.buildRemote.sshTarget);
|
|
141
|
+
return args;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
remoteBridgeArgs(commandArgs = [], { tty = false } = {}) {
|
|
145
|
+
const args = [...this.remoteSshArgsRaw({ tty })];
|
|
146
|
+
args.push('lzc-docker', 'exec', tty ? '-it' : '-i', DEBUG_BRIDGE_CONTAINER, DEBUG_BRIDGE_BINARY, ...commandArgs);
|
|
147
|
+
return args;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
remoteBridgeExec(commandArgs = [], { tty = false } = {}) {
|
|
151
|
+
return spawn.sync(sshBinary(), this.remoteBridgeArgs(commandArgs, { tty }), {
|
|
152
|
+
shell: false,
|
|
153
|
+
encoding: 'utf-8',
|
|
154
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
remoteHostExec(argv = []) {
|
|
159
|
+
return spawn.sync(sshBinary(), [...this.remoteSshArgsRaw(), ...argv], {
|
|
160
|
+
shell: false,
|
|
161
|
+
encoding: 'utf-8',
|
|
162
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async remoteCommon(commandArgs = []) {
|
|
167
|
+
const ssh = this.remoteBridgeExec(commandArgs);
|
|
168
|
+
logger.debug(`run remote command: ${commandArgs.join(' ')}`);
|
|
169
|
+
if (ssh.status === 0) {
|
|
170
|
+
return ssh.stdout;
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`remote command failed: ${commandArgs.join(' ')}\n${ssh.stdout ?? ''}\n${ssh.stderr ?? ''}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
collectUIDCandidatesFromContainerEnv() {
|
|
176
|
+
const candidates = new Set();
|
|
177
|
+
const ps = this.remoteHostExec(['lzc-docker', 'ps', '--format', '{{.Names}}']);
|
|
178
|
+
if (ps.status !== 0) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const containers = String(ps.stdout ?? '')
|
|
182
|
+
.split(/\r?\n/)
|
|
183
|
+
.map((item) => item.trim())
|
|
184
|
+
.filter((item) => item);
|
|
185
|
+
for (const container of containers.slice(0, 48)) {
|
|
186
|
+
const envResult = this.remoteHostExec(['lzc-docker', 'exec', '-i', container, 'env']);
|
|
187
|
+
if (envResult.status !== 0) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
String(envResult.stdout ?? '')
|
|
191
|
+
.split(/\r?\n/)
|
|
192
|
+
.map((line) => line.trim())
|
|
193
|
+
.forEach((line) => {
|
|
194
|
+
if (!line.startsWith('LAZYCAT_USER_ID=') && !line.startsWith('LAZYCAT_APP_DEPLOY_UID=')) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const value = String(line.split('=').slice(1).join('=') ?? '').trim();
|
|
198
|
+
if (value) {
|
|
199
|
+
candidates.add(value);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return [...candidates];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async probeLegacyUID() {
|
|
207
|
+
const candidates = [];
|
|
208
|
+
const appendCandidate = (value) => {
|
|
209
|
+
const uid = String(value ?? '').trim();
|
|
210
|
+
if (!uid) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!candidates.includes(uid)) {
|
|
214
|
+
candidates.push(uid);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.collectUIDCandidatesFromContainerEnv().forEach((item) => appendCandidate(item));
|
|
219
|
+
appendCandidate(process.env.BOX_UID);
|
|
220
|
+
['c', 'admin', 'root'].forEach((item) => appendCandidate(item));
|
|
221
|
+
'abcdefghijklmnopqrstuvwxyz0123456789'.split('').forEach((item) => appendCandidate(item));
|
|
222
|
+
|
|
223
|
+
for (const uid of candidates) {
|
|
224
|
+
const probe = this.remoteBridgeExec(['info', '--uid', uid, DEBUG_BRIDGE_APP_ID]);
|
|
225
|
+
if (probe.status !== 0) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(String(probe.stdout ?? '').trim());
|
|
230
|
+
if (parsed?.appid === DEBUG_BRIDGE_APP_ID) {
|
|
231
|
+
logger.debug(`resolved legacy uid by probe: ${uid}`);
|
|
232
|
+
return uid;
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
logger.debug(`legacy uid probe got non-json output, use uid candidate: ${uid}`);
|
|
236
|
+
return uid;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async resolveCurrentUID() {
|
|
244
|
+
if (!this.isBuildRemoteMode()) {
|
|
245
|
+
return this.uid;
|
|
246
|
+
}
|
|
247
|
+
const cached = String(this.uid ?? '').trim();
|
|
248
|
+
if (cached) {
|
|
249
|
+
return cached;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const uidReply = this.remoteBridgeExec(['uid']);
|
|
253
|
+
if (uidReply.status === 0) {
|
|
254
|
+
const resolved = String(uidReply.stdout ?? '').trim();
|
|
255
|
+
if (!resolved) {
|
|
256
|
+
throw new Error('resolve uid failed: empty output');
|
|
257
|
+
}
|
|
258
|
+
this.uid = resolved;
|
|
259
|
+
return resolved;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const legacyUID = await this.probeLegacyUID();
|
|
263
|
+
if (legacyUID) {
|
|
264
|
+
this.uid = legacyUID;
|
|
265
|
+
return legacyUID;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw new Error(`resolve uid failed: ${String(uidReply.stderr ?? uidReply.stdout ?? '').trim() || 'unknown error'}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async remoteCommandWithUID(commandArgs = []) {
|
|
272
|
+
const uid = await this.resolveCurrentUID();
|
|
273
|
+
const args = [...commandArgs];
|
|
274
|
+
args.splice(1, 0, '--uid', uid);
|
|
275
|
+
return args;
|
|
276
|
+
}
|
|
277
|
+
|
|
33
278
|
async init() {
|
|
34
279
|
await this.checkDevTools();
|
|
35
280
|
if (!(await this.canPublicKey())) {
|
|
281
|
+
if (this.isBuildRemoteMode()) {
|
|
282
|
+
throw new Error(`ssh login failed for remote ${this.buildRemote.sshTarget}:${this.buildRemote.sshPort}`);
|
|
283
|
+
}
|
|
36
284
|
// 如果不能 ssh public key 登录则提示授权申请,否则后面可能会出现 rsync 询问密码的问题
|
|
37
285
|
await this.sshApplyGrant();
|
|
38
286
|
}
|
|
39
287
|
}
|
|
40
288
|
|
|
41
289
|
async checkDevTools() {
|
|
290
|
+
if (this.isBuildRemoteMode()) {
|
|
291
|
+
try {
|
|
292
|
+
await this.remoteCommon(['version']);
|
|
293
|
+
return;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
const detail = normalizeErrorMessage(error);
|
|
296
|
+
if (detail.includes(`No such container: ${DEBUG_BRIDGE_CONTAINER}`)) {
|
|
297
|
+
throw new Error(`Lazycat Developer Tools is not running on target box "${this.boxname}". Start app "cloud.lazycat.developer.tools" and retry.`);
|
|
298
|
+
}
|
|
299
|
+
if (detail.includes('is not running')) {
|
|
300
|
+
throw new Error(`Lazycat Developer Tools container is not running on target box "${this.boxname}". Start app "cloud.lazycat.developer.tools" and retry.`);
|
|
301
|
+
}
|
|
302
|
+
throw new Error(`Failed to check Lazycat Developer Tools in remote mode.\n${detail || 'unknown error'}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
42
306
|
let domain = this.domain;
|
|
43
307
|
if (this.checkUseResolve) {
|
|
44
308
|
try {
|
|
@@ -59,10 +323,10 @@ export class DebugBridge {
|
|
|
59
323
|
t(
|
|
60
324
|
'lzc_cli.lib.debug_bridge.check_dev_tools_not_exist_tips',
|
|
61
325
|
`检测到你还没有安装 '懒猫开发者工具',请先到商店中搜索安装
|
|
62
|
-
点击直接跳转 https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
|
|
63
|
-
点击打开应用 https://dev.{{boxname}}.heiyu.space 查看应用状态
|
|
326
|
+
点击直接跳转 https://appstore.{{ boxname }}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
|
|
327
|
+
点击打开应用 https://dev.{{ boxname }}.heiyu.space 查看应用状态
|
|
64
328
|
`,
|
|
65
|
-
{ boxname: this.boxname },
|
|
329
|
+
{ boxname: this.boxname, interpolation: { escapeValue: false } },
|
|
66
330
|
),
|
|
67
331
|
);
|
|
68
332
|
reject();
|
|
@@ -82,16 +346,17 @@ export class DebugBridge {
|
|
|
82
346
|
encoding: 'utf-8',
|
|
83
347
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
84
348
|
});
|
|
85
|
-
logger.debug(t('lzc_cli.lib.debug_bridge.common_start_log', `执行命令 {{cmd}} {{args}}`, { cmd, args: args.join(' ') }));
|
|
349
|
+
logger.debug(t('lzc_cli.lib.debug_bridge.common_start_log', `执行命令 {{ cmd }} {{ args }}`, { cmd, args: args.join(' '), interpolation: { escapeValue: false } }));
|
|
86
350
|
return new Promise((resolve, reject) => {
|
|
87
351
|
ssh.status == 0
|
|
88
352
|
? resolve(ssh.stdout)
|
|
89
353
|
: reject(
|
|
90
|
-
t('lzc_cli.lib.debug_bridge.common_exec_fail', `执行命令 {{cmd}} {{args}} 出错\n{{stdout}}\n{{stderr}}`, {
|
|
354
|
+
t('lzc_cli.lib.debug_bridge.common_exec_fail', `执行命令 {{ cmd }} {{ args }} 出错\n{{ stdout }}\n{{ stderr }}`, {
|
|
91
355
|
cmd,
|
|
92
356
|
args: args.join(' '),
|
|
93
357
|
stdout: ssh.stdout ?? '',
|
|
94
358
|
stdout: ssh.stderr ?? '',
|
|
359
|
+
interpolation: { escapeValue: false },
|
|
95
360
|
}),
|
|
96
361
|
);
|
|
97
362
|
});
|
|
@@ -99,20 +364,63 @@ export class DebugBridge {
|
|
|
99
364
|
|
|
100
365
|
async install(lpkPath, pkgId) {
|
|
101
366
|
const stream = fs.createReadStream(lpkPath);
|
|
367
|
+
if (this.isBuildRemoteMode()) {
|
|
368
|
+
const commandArgs = await this.remoteCommandWithUID(['install']);
|
|
369
|
+
if (pkgId) {
|
|
370
|
+
commandArgs.push('--pkgId', pkgId);
|
|
371
|
+
}
|
|
372
|
+
const ssh = spawn(sshBinary(), this.remoteBridgeArgs(commandArgs), {
|
|
373
|
+
shell: false,
|
|
374
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
375
|
+
});
|
|
376
|
+
const output = streamInstallOutput(ssh, { printStdout: false, printStderr: false });
|
|
377
|
+
stream.pipe(ssh.stdin);
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
ssh.on('error', (error) => {
|
|
380
|
+
reject(error);
|
|
381
|
+
});
|
|
382
|
+
ssh.on('close', (code) => {
|
|
383
|
+
if (code == 0) {
|
|
384
|
+
resolve();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const detail = extractInstallErrorDetail(`${output.getStdout()}\n${output.getStderr()}`);
|
|
388
|
+
reject(detail || t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败'));
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
102
393
|
const resolvedIp = await resolveDomain(this.domain);
|
|
103
394
|
const ssh = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), `install --uid ${this.uid}`, pkgId ? `--pkgId ${pkgId}` : ''], {
|
|
104
395
|
shell: true,
|
|
105
|
-
stdio: ['pipe', '
|
|
396
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
106
397
|
});
|
|
398
|
+
const output = streamInstallOutput(ssh, { printStdout: false, printStderr: false });
|
|
107
399
|
stream.pipe(ssh.stdin);
|
|
108
400
|
return new Promise((resolve, reject) => {
|
|
401
|
+
ssh.on('error', (error) => {
|
|
402
|
+
reject(error);
|
|
403
|
+
});
|
|
109
404
|
ssh.on('close', (code) => {
|
|
110
|
-
code == 0
|
|
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 失败'));
|
|
111
411
|
});
|
|
112
412
|
});
|
|
113
413
|
}
|
|
114
414
|
|
|
115
415
|
async canPublicKey() {
|
|
416
|
+
if (this.isBuildRemoteMode()) {
|
|
417
|
+
const ssh = spawn.sync(sshBinary(), [...this.remoteSshArgsRaw(), 'true'], {
|
|
418
|
+
shell: false,
|
|
419
|
+
encoding: 'utf-8',
|
|
420
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
421
|
+
});
|
|
422
|
+
return ssh.status === 0;
|
|
423
|
+
}
|
|
116
424
|
try {
|
|
117
425
|
await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`)]);
|
|
118
426
|
return true;
|
|
@@ -127,6 +435,9 @@ export class DebugBridge {
|
|
|
127
435
|
}
|
|
128
436
|
|
|
129
437
|
async sshApplyGrant() {
|
|
438
|
+
if (this.isBuildRemoteMode()) {
|
|
439
|
+
throw new Error('build remote mode requires ssh key authorization in host ssh service');
|
|
440
|
+
}
|
|
130
441
|
const keys = await findSshPublicKey();
|
|
131
442
|
logger.info(t('lzc_cli.lib.debug_bridge.ssh_apply_grant_not_exist_tips', '检测到您当前的环境还没有添加 ssh 公钥到 ‘懒猫开发者工具’ 中,请选择您需要添加的公钥类型'));
|
|
132
443
|
const sshInfo = await selectSshPublicKey(keys);
|
|
@@ -137,14 +448,15 @@ export class DebugBridge {
|
|
|
137
448
|
logger.warn(
|
|
138
449
|
t(
|
|
139
450
|
'lzc_cli.lib.debug_bridge.ssh_apply_grant_not_credible_tips',
|
|
140
|
-
`您当前机器的公钥未添加到微服({{boxname}})的信任列表中,请使用微服管理员账号在浏览器中访问以下地址,将您选择的公钥自动添加到信任列表中。(所有操作均只在您微服中进行,包括本开发机在内的任何数据不会泄漏到您的微服之外)
|
|
451
|
+
`您当前机器的公钥未添加到微服({{ boxname }})的信任列表中,请使用微服管理员账号在浏览器中访问以下地址,将您选择的公钥自动添加到信任列表中。(所有操作均只在您微服中进行,包括本开发机在内的任何数据不会泄漏到您的微服之外)
|
|
141
452
|
|
|
142
|
-
-> https://{{domain}}/auth?key={{pk}}
|
|
453
|
+
-> https://{{ domain }}/auth?key={{ pk }}
|
|
143
454
|
`,
|
|
144
455
|
{
|
|
145
456
|
boxname: this.boxname,
|
|
146
457
|
domain: this.domain,
|
|
147
|
-
|
|
458
|
+
pk,
|
|
459
|
+
interpolation: { escapeValue: false },
|
|
148
460
|
},
|
|
149
461
|
),
|
|
150
462
|
);
|
|
@@ -152,21 +464,52 @@ export class DebugBridge {
|
|
|
152
464
|
}
|
|
153
465
|
|
|
154
466
|
async status(appId) {
|
|
467
|
+
if (this.isBuildRemoteMode()) {
|
|
468
|
+
return this.remoteCommon(await this.remoteCommandWithUID(['status', appId]));
|
|
469
|
+
}
|
|
155
470
|
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `status --uid ${this.uid}`, appId]);
|
|
156
471
|
}
|
|
157
472
|
|
|
473
|
+
async info(appId) {
|
|
474
|
+
let stdout;
|
|
475
|
+
if (this.isBuildRemoteMode()) {
|
|
476
|
+
stdout = await this.remoteCommon(await this.remoteCommandWithUID(['info', appId]));
|
|
477
|
+
} else {
|
|
478
|
+
stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `info --uid ${this.uid}`, appId]);
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
return JSON.parse(stdout);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
throw new Error(`parse app info failed: ${error.message}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
158
487
|
async isDevshell(appId) {
|
|
159
488
|
await this.backendVersion020();
|
|
489
|
+
if (this.isBuildRemoteMode()) {
|
|
490
|
+
const stdout = await this.remoteCommon(await this.remoteCommandWithUID(['isDevshellV2', appId]));
|
|
491
|
+
return stdout.trim() == 'true';
|
|
492
|
+
}
|
|
160
493
|
const stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `isDevshellV2 --uid ${this.uid}`, appId]);
|
|
161
494
|
return stdout == 'true';
|
|
162
495
|
}
|
|
163
496
|
|
|
164
497
|
async resume(appId) {
|
|
498
|
+
if (this.isBuildRemoteMode()) {
|
|
499
|
+
return this.remoteCommon(await this.remoteCommandWithUID(['resume', appId]));
|
|
500
|
+
}
|
|
165
501
|
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `resume --uid ${this.uid}`, appId]);
|
|
166
502
|
}
|
|
167
503
|
|
|
504
|
+
async pause(appId) {
|
|
505
|
+
if (this.isBuildRemoteMode()) {
|
|
506
|
+
return this.remoteCommon(await this.remoteCommandWithUID(['pause', appId]));
|
|
507
|
+
}
|
|
508
|
+
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `pause --uid ${this.uid}`, appId]);
|
|
509
|
+
}
|
|
510
|
+
|
|
168
511
|
async version() {
|
|
169
|
-
const output = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `version`]);
|
|
512
|
+
const output = this.isBuildRemoteMode() ? await this.remoteCommon(['version']) : await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `version`]);
|
|
170
513
|
logger.debug(`backend version:\n${output}`);
|
|
171
514
|
try {
|
|
172
515
|
const data = JSON.parse(output);
|
|
@@ -177,6 +520,14 @@ export class DebugBridge {
|
|
|
177
520
|
}
|
|
178
521
|
|
|
179
522
|
async uninstall(appId, deleteAppData = false) {
|
|
523
|
+
if (this.isBuildRemoteMode()) {
|
|
524
|
+
const commandArgs = await this.remoteCommandWithUID(['uninstall']);
|
|
525
|
+
if (deleteAppData) {
|
|
526
|
+
commandArgs.push('--delete-data');
|
|
527
|
+
}
|
|
528
|
+
commandArgs.push(appId);
|
|
529
|
+
return this.remoteCommon(commandArgs);
|
|
530
|
+
}
|
|
180
531
|
return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `uninstall --uid ${this.uid}`, deleteAppData ? '--delete-data' : '', appId]);
|
|
181
532
|
}
|
|
182
533
|
|
|
@@ -193,16 +544,28 @@ export class DebugBridge {
|
|
|
193
544
|
await sleep(100);
|
|
194
545
|
}
|
|
195
546
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
547
|
+
let stream;
|
|
548
|
+
if (this.isBuildRemoteMode()) {
|
|
549
|
+
const commandArgs = await this.remoteCommandWithUID(['devshell']);
|
|
550
|
+
if (isUserApp) {
|
|
551
|
+
commandArgs.push('--userapp');
|
|
552
|
+
}
|
|
553
|
+
commandArgs.push(appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh');
|
|
554
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(commandArgs, { tty: true }), {
|
|
555
|
+
shell: false,
|
|
203
556
|
stdio: 'inherit',
|
|
204
|
-
}
|
|
205
|
-
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
560
|
+
stream = spawn(
|
|
561
|
+
sshBinary(),
|
|
562
|
+
[...sshCmdArgs(`box@${resolvedIp}`), '-t', 'devshell', `--uid ${this.uid}`, isUserApp ? '--userapp' : '', appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh'],
|
|
563
|
+
{
|
|
564
|
+
shell: true,
|
|
565
|
+
stdio: 'inherit',
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
}
|
|
206
569
|
return new Promise((resolve, reject) => {
|
|
207
570
|
stream.on('close', (code) => {
|
|
208
571
|
code == 0 ? resolve() : reject();
|
|
@@ -216,51 +579,481 @@ export class DebugBridge {
|
|
|
216
579
|
}
|
|
217
580
|
|
|
218
581
|
async buildImage(label, contextTar) {
|
|
219
|
-
const backendVersion = await this.version();
|
|
220
|
-
|
|
221
582
|
const tag = `debug.bridge/${label}`;
|
|
222
|
-
const resolvedIp = await resolveDomain(this.domain);
|
|
223
583
|
const stream = fs.createReadStream(contextTar);
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
584
|
+
let buildStream;
|
|
585
|
+
const buildArgs = ['build', '--tag', tag];
|
|
586
|
+
if (this.isBuildRemoteMode()) {
|
|
587
|
+
buildStream = spawn(sshBinary(), this.remoteBridgeArgs(buildArgs), {
|
|
588
|
+
shell: false,
|
|
589
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
590
|
+
});
|
|
591
|
+
} else {
|
|
592
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
593
|
+
buildStream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), ['build', '--tag', tag].join(' ')], {
|
|
594
|
+
shell: true,
|
|
595
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
596
|
+
});
|
|
597
|
+
}
|
|
229
598
|
stream.pipe(buildStream.stdin);
|
|
230
599
|
return new Promise((resolve, reject) => {
|
|
231
600
|
buildStream.on('close', (code) => {
|
|
232
|
-
code == 0
|
|
233
|
-
? resolve(compareVersions('0.1.12', backendVersion) >= 0 ? `127.0.0.1:5000/${tag}` : `dev.${this.boxname}.heiyu.space/${tag}`)
|
|
234
|
-
: reject(t('lzc_cli.lib.debug_bridge.build_image_fail', `在微服中构建 image 失败`));
|
|
601
|
+
code == 0 ? resolve(tag) : reject(t('lzc_cli.lib.debug_bridge.build_image_fail', `在微服中构建 image 失败`));
|
|
235
602
|
});
|
|
236
603
|
}).finally(() => {
|
|
237
604
|
fs.rmSync(contextTar);
|
|
238
605
|
});
|
|
239
606
|
}
|
|
240
607
|
|
|
608
|
+
async buildImageForPack(label, contextTar) {
|
|
609
|
+
const tag = `debug.bridge/${label}`;
|
|
610
|
+
const stream = fs.createReadStream(contextTar);
|
|
611
|
+
let buildStream;
|
|
612
|
+
const isBuildPackResultLine = (line) => {
|
|
613
|
+
const trimmed = String(line ?? '').trim();
|
|
614
|
+
if (!trimmed || !trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
const parsed = JSON.parse(trimmed);
|
|
619
|
+
return (
|
|
620
|
+
parsed &&
|
|
621
|
+
typeof parsed === 'object' &&
|
|
622
|
+
typeof parsed.tag === 'string' &&
|
|
623
|
+
typeof parsed.archiveKey === 'string' &&
|
|
624
|
+
typeof parsed.imageID === 'string' &&
|
|
625
|
+
Array.isArray(parsed.diffIDs)
|
|
626
|
+
);
|
|
627
|
+
} catch {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
if (this.isBuildRemoteMode()) {
|
|
632
|
+
buildStream = spawn(sshBinary(), this.remoteBridgeArgs(['build-pack', '--tag', tag]), {
|
|
633
|
+
shell: false,
|
|
634
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
635
|
+
});
|
|
636
|
+
} else {
|
|
637
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
638
|
+
buildStream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'build-pack', '--tag', tag], {
|
|
639
|
+
shell: false,
|
|
640
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
stream.pipe(buildStream.stdin);
|
|
644
|
+
|
|
645
|
+
return new Promise((resolve, reject) => {
|
|
646
|
+
let output = '';
|
|
647
|
+
let stdoutLineBuffer = '';
|
|
648
|
+
buildStream.stdout.on('data', (chunk) => {
|
|
649
|
+
const text = chunk.toString();
|
|
650
|
+
output += text;
|
|
651
|
+
stdoutLineBuffer += text;
|
|
652
|
+
while (true) {
|
|
653
|
+
const lineEndIndex = stdoutLineBuffer.indexOf('\n');
|
|
654
|
+
if (lineEndIndex < 0) {
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
const lineWithNewline = stdoutLineBuffer.slice(0, lineEndIndex + 1);
|
|
658
|
+
stdoutLineBuffer = stdoutLineBuffer.slice(lineEndIndex + 1);
|
|
659
|
+
const line = lineWithNewline.replace(/\r?\n$/, '');
|
|
660
|
+
if (!isBuildPackResultLine(line)) {
|
|
661
|
+
process.stdout.write(lineWithNewline);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
buildStream.on('error', (error) => {
|
|
666
|
+
reject(error);
|
|
667
|
+
});
|
|
668
|
+
buildStream.on('close', (code) => {
|
|
669
|
+
if (stdoutLineBuffer) {
|
|
670
|
+
if (!isBuildPackResultLine(stdoutLineBuffer)) {
|
|
671
|
+
process.stdout.write(stdoutLineBuffer);
|
|
672
|
+
}
|
|
673
|
+
stdoutLineBuffer = '';
|
|
674
|
+
}
|
|
675
|
+
if (code !== 0) {
|
|
676
|
+
reject(new Error('build-pack failed'));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const line = output
|
|
681
|
+
.split(/\r?\n/)
|
|
682
|
+
.map((item) => item.trim())
|
|
683
|
+
.filter((item) => item !== '')
|
|
684
|
+
.at(-1);
|
|
685
|
+
if (!line) {
|
|
686
|
+
throw new Error('build-pack output is empty');
|
|
687
|
+
}
|
|
688
|
+
const payload = JSON.parse(line);
|
|
689
|
+
resolve(payload);
|
|
690
|
+
} catch (error) {
|
|
691
|
+
reject(new Error(`parse build-pack output failed: ${error.message}`));
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}).finally(() => {
|
|
695
|
+
fs.rmSync(contextTar, { force: true });
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
241
699
|
async lzcDocker(argv) {
|
|
242
700
|
await this.backendVersion020();
|
|
701
|
+
const stream = this.isBuildRemoteMode()
|
|
702
|
+
? spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv], { tty: true }), {
|
|
703
|
+
shell: false,
|
|
704
|
+
stdio: 'inherit',
|
|
705
|
+
})
|
|
706
|
+
: spawn(sshBinary(), [...sshCmdArgs(`box@${await resolveDomain(this.domain)}`), '-t', 'lzc-docker', ...argv], {
|
|
707
|
+
shell: true,
|
|
708
|
+
stdio: 'inherit',
|
|
709
|
+
});
|
|
710
|
+
return new Promise((resolve, reject) => {
|
|
711
|
+
stream.on('close', (code) => {
|
|
712
|
+
code == 0 ? resolve() : reject();
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
}
|
|
243
716
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
717
|
+
async lzcDockerPipe(argv, stdinStream = null) {
|
|
718
|
+
await this.backendVersion020();
|
|
719
|
+
let stream;
|
|
720
|
+
if (this.isBuildRemoteMode()) {
|
|
721
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv]), {
|
|
722
|
+
shell: false,
|
|
723
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
724
|
+
});
|
|
725
|
+
} else {
|
|
726
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
727
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', ...argv], {
|
|
728
|
+
shell: false,
|
|
729
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return await new Promise((resolve, reject) => {
|
|
734
|
+
let done = false;
|
|
735
|
+
const fail = (error) => {
|
|
736
|
+
if (done) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
done = true;
|
|
740
|
+
try {
|
|
741
|
+
stream.kill('SIGKILL');
|
|
742
|
+
} catch {}
|
|
743
|
+
reject(error);
|
|
744
|
+
};
|
|
745
|
+
stream.on('error', (error) => fail(error));
|
|
746
|
+
stream.on('close', (code) => {
|
|
747
|
+
if (done) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
done = true;
|
|
751
|
+
code == 0 ? resolve() : reject(t('lzc_cli.lib.debug_bridge.lzc_docker_capture_fail', 'lzc-docker command failed'));
|
|
752
|
+
});
|
|
753
|
+
if (stdinStream) {
|
|
754
|
+
stdinStream.on('error', (error) => fail(error));
|
|
755
|
+
stdinStream.pipe(stream.stdin);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
stream.stdin.end();
|
|
248
759
|
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async lzcDockerPull(image) {
|
|
763
|
+
await this.backendVersion020();
|
|
764
|
+
let stream;
|
|
765
|
+
if (this.isBuildRemoteMode()) {
|
|
766
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', 'pull', image]), {
|
|
767
|
+
shell: false,
|
|
768
|
+
stdio: 'inherit',
|
|
769
|
+
});
|
|
770
|
+
} else {
|
|
771
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
772
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', 'pull', image], {
|
|
773
|
+
shell: false,
|
|
774
|
+
stdio: 'inherit',
|
|
775
|
+
});
|
|
776
|
+
}
|
|
249
777
|
return new Promise((resolve, reject) => {
|
|
250
778
|
stream.on('close', (code) => {
|
|
251
|
-
code == 0 ? resolve() : reject();
|
|
779
|
+
code == 0 ? resolve() : reject(t('lzc_cli.lib.debug_bridge.lzc_docker_pull_fail', `lzc-docker pull 失败: ${image}`));
|
|
252
780
|
});
|
|
253
781
|
});
|
|
254
782
|
}
|
|
255
783
|
|
|
256
|
-
async
|
|
784
|
+
async lzcDockerSave(images, outputPath) {
|
|
257
785
|
await this.backendVersion020();
|
|
786
|
+
if (!images || images.length == 0) {
|
|
787
|
+
throw t('lzc_cli.lib.debug_bridge.lzc_docker_save_images_empty_fail', 'images 不能为空');
|
|
788
|
+
}
|
|
789
|
+
const output = fs.createWriteStream(outputPath);
|
|
790
|
+
let stream;
|
|
791
|
+
if (this.isBuildRemoteMode()) {
|
|
792
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', 'image', 'save', ...images]), {
|
|
793
|
+
shell: false,
|
|
794
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
795
|
+
});
|
|
796
|
+
} else {
|
|
797
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
798
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', 'image', 'save', ...images], {
|
|
799
|
+
shell: false,
|
|
800
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
stream.stdout.pipe(output);
|
|
804
|
+
|
|
805
|
+
return new Promise((resolve, reject) => {
|
|
806
|
+
let done = false;
|
|
807
|
+
const fail = (err) => {
|
|
808
|
+
if (done) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
done = true;
|
|
812
|
+
try {
|
|
813
|
+
stream.kill('SIGKILL');
|
|
814
|
+
} catch {}
|
|
815
|
+
try {
|
|
816
|
+
output.destroy();
|
|
817
|
+
} catch {}
|
|
818
|
+
try {
|
|
819
|
+
fs.rmSync(outputPath, { force: true });
|
|
820
|
+
} catch {}
|
|
821
|
+
reject(err);
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
stream.on('error', (e) => {
|
|
825
|
+
fail(e);
|
|
826
|
+
});
|
|
827
|
+
output.on('error', (e) => {
|
|
828
|
+
fail(e);
|
|
829
|
+
});
|
|
830
|
+
stream.on('close', (code) => {
|
|
831
|
+
if (done) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
output.end(() => {
|
|
835
|
+
if (code == 0) {
|
|
836
|
+
done = true;
|
|
837
|
+
resolve();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
fail(t('lzc_cli.lib.debug_bridge.lzc_docker_save_fail', 'lzc-docker image save 失败'));
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async packImages(imagesSpec, outputPath) {
|
|
847
|
+
await this.backendVersion020();
|
|
848
|
+
if (!Array.isArray(imagesSpec) || imagesSpec.length === 0) {
|
|
849
|
+
throw new Error('imagesSpec cannot be empty');
|
|
850
|
+
}
|
|
851
|
+
const spec = Buffer.from(
|
|
852
|
+
JSON.stringify({
|
|
853
|
+
images: imagesSpec,
|
|
854
|
+
}),
|
|
855
|
+
).toString('base64');
|
|
856
|
+
|
|
857
|
+
const output = fs.createWriteStream(outputPath);
|
|
858
|
+
let stream;
|
|
859
|
+
if (this.isBuildRemoteMode()) {
|
|
860
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['pack-images', '--spec', spec]), {
|
|
861
|
+
shell: false,
|
|
862
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
863
|
+
});
|
|
864
|
+
} else {
|
|
865
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
866
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'pack-images', '--spec', spec], {
|
|
867
|
+
shell: false,
|
|
868
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
stream.stdout.pipe(output);
|
|
872
|
+
|
|
873
|
+
return new Promise((resolve, reject) => {
|
|
874
|
+
let done = false;
|
|
875
|
+
const fail = (err) => {
|
|
876
|
+
if (done) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
done = true;
|
|
880
|
+
try {
|
|
881
|
+
stream.kill('SIGKILL');
|
|
882
|
+
} catch {}
|
|
883
|
+
try {
|
|
884
|
+
output.destroy();
|
|
885
|
+
} catch {}
|
|
886
|
+
try {
|
|
887
|
+
fs.rmSync(outputPath, { force: true });
|
|
888
|
+
} catch {}
|
|
889
|
+
reject(err);
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
stream.on('error', (e) => {
|
|
893
|
+
fail(e);
|
|
894
|
+
});
|
|
895
|
+
output.on('error', (e) => {
|
|
896
|
+
fail(e);
|
|
897
|
+
});
|
|
898
|
+
stream.on('close', (code) => {
|
|
899
|
+
if (done) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
output.end(() => {
|
|
903
|
+
if (code == 0) {
|
|
904
|
+
done = true;
|
|
905
|
+
resolve();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
fail(new Error('pack-images failed'));
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async lzcDockerCapture(argv) {
|
|
915
|
+
await this.backendVersion020();
|
|
916
|
+
let stream;
|
|
917
|
+
if (this.isBuildRemoteMode()) {
|
|
918
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv]), {
|
|
919
|
+
shell: false,
|
|
920
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
921
|
+
});
|
|
922
|
+
} else {
|
|
923
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
924
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', ...argv], {
|
|
925
|
+
shell: false,
|
|
926
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return await new Promise((resolve, reject) => {
|
|
931
|
+
let output = '';
|
|
932
|
+
stream.stdout.on('data', (chunk) => {
|
|
933
|
+
output += chunk.toString();
|
|
934
|
+
});
|
|
935
|
+
stream.on('error', (err) => reject(err));
|
|
936
|
+
stream.on('close', (code) => {
|
|
937
|
+
if (code == 0) {
|
|
938
|
+
resolve(output.trim());
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
reject(t('lzc_cli.lib.debug_bridge.lzc_docker_capture_fail', 'lzc-docker command failed'));
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async lzcDockerComposeCapture(argv) {
|
|
947
|
+
await this.backendVersion020();
|
|
948
|
+
let stream;
|
|
949
|
+
if (this.isBuildRemoteMode()) {
|
|
950
|
+
stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker-compose', ...argv]), {
|
|
951
|
+
shell: false,
|
|
952
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
953
|
+
});
|
|
954
|
+
} else {
|
|
955
|
+
const resolvedIp = await resolveDomain(this.domain);
|
|
956
|
+
stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker-compose', ...argv], {
|
|
957
|
+
shell: false,
|
|
958
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return await new Promise((resolve, reject) => {
|
|
963
|
+
let stdout = '';
|
|
964
|
+
let stderr = '';
|
|
965
|
+
stream.stdout.on('data', (chunk) => {
|
|
966
|
+
stdout += chunk.toString();
|
|
967
|
+
});
|
|
968
|
+
stream.stderr.on('data', (chunk) => {
|
|
969
|
+
stderr += chunk.toString();
|
|
970
|
+
});
|
|
971
|
+
stream.on('error', (err) => reject(err));
|
|
972
|
+
stream.on('close', (code) => {
|
|
973
|
+
if (code == 0) {
|
|
974
|
+
resolve(stdout.trim());
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
reject(new Error(`lzc-docker-compose command failed\n${stdout}\n${stderr}`));
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async hostReadFile(pathname) {
|
|
983
|
+
const target = String(pathname ?? '').trim();
|
|
984
|
+
if (!target) {
|
|
985
|
+
throw new Error('hostReadFile path is empty');
|
|
986
|
+
}
|
|
987
|
+
if (this.isBuildRemoteMode()) {
|
|
988
|
+
const result = this.remoteHostExec(['cat', target]);
|
|
989
|
+
if (result.status === 0) {
|
|
990
|
+
return String(result.stdout ?? '');
|
|
991
|
+
}
|
|
992
|
+
throw new Error(`read remote file failed: ${target}`);
|
|
993
|
+
}
|
|
258
994
|
|
|
259
995
|
const resolvedIp = await resolveDomain(this.domain);
|
|
260
|
-
const stream = spawn(sshBinary(), [...
|
|
261
|
-
shell:
|
|
262
|
-
stdio: '
|
|
996
|
+
const stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'cat', target], {
|
|
997
|
+
shell: false,
|
|
998
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
999
|
+
});
|
|
1000
|
+
return await new Promise((resolve, reject) => {
|
|
1001
|
+
let stdout = '';
|
|
1002
|
+
let stderr = '';
|
|
1003
|
+
stream.stdout.on('data', (chunk) => {
|
|
1004
|
+
stdout += chunk.toString();
|
|
1005
|
+
});
|
|
1006
|
+
stream.stderr.on('data', (chunk) => {
|
|
1007
|
+
stderr += chunk.toString();
|
|
1008
|
+
});
|
|
1009
|
+
stream.on('error', (err) => reject(err));
|
|
1010
|
+
stream.on('close', (code) => {
|
|
1011
|
+
if (code === 0) {
|
|
1012
|
+
resolve(stdout);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
reject(new Error(`read host file failed\n${stderr}`));
|
|
1016
|
+
});
|
|
263
1017
|
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async lzcDockerImageRepoDigests(image) {
|
|
1021
|
+
const inspect = await this.lzcDockerImageInspect(image);
|
|
1022
|
+
const repoDigests = inspect?.RepoDigests;
|
|
1023
|
+
if (!Array.isArray(repoDigests)) {
|
|
1024
|
+
return [];
|
|
1025
|
+
}
|
|
1026
|
+
return repoDigests.filter((item) => typeof item === 'string' && item.trim() !== '');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async lzcDockerImageInspect(image) {
|
|
1030
|
+
const output = await this.lzcDockerCapture(['image', 'inspect', image]);
|
|
1031
|
+
if (!output) {
|
|
1032
|
+
throw new Error(`lzc-docker image inspect output is empty: ${image}`);
|
|
1033
|
+
}
|
|
1034
|
+
let parsed;
|
|
1035
|
+
try {
|
|
1036
|
+
parsed = JSON.parse(output);
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
throw new Error(`failed to parse image inspect output for ${image}: ${error.message}`);
|
|
1039
|
+
}
|
|
1040
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed[0] || typeof parsed[0] !== 'object') {
|
|
1041
|
+
throw new Error(`invalid image inspect output for ${image}`);
|
|
1042
|
+
}
|
|
1043
|
+
return parsed[0];
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async lzcDockerCompose(argv) {
|
|
1047
|
+
await this.backendVersion020();
|
|
1048
|
+
const stream = this.isBuildRemoteMode()
|
|
1049
|
+
? spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker-compose', ...argv], { tty: true }), {
|
|
1050
|
+
shell: false,
|
|
1051
|
+
stdio: 'inherit',
|
|
1052
|
+
})
|
|
1053
|
+
: spawn(sshBinary(), [...sshCmdArgs(`box@${await resolveDomain(this.domain)}`), '-t', 'lzc-docker-compose', ...argv], {
|
|
1054
|
+
shell: true,
|
|
1055
|
+
stdio: 'inherit',
|
|
1056
|
+
});
|
|
264
1057
|
return new Promise((resolve, reject) => {
|
|
265
1058
|
stream.on('close', (code) => {
|
|
266
1059
|
code == 0 ? resolve() : reject();
|