@lazycatcloud/lzc-cli 1.3.12 → 1.3.13

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.
@@ -1,34 +1,35 @@
1
1
  // lzc-cli app devshell
2
- import path from "node:path"
3
- import fs from "node:fs"
4
- import logger from "loglevel"
5
- import spawn from "cross-spawn"
6
- import { LpkInstaller } from "./lpk_installer.js"
7
- import debounce from "lodash.debounce"
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import logger from 'loglevel';
5
+ import spawn from 'cross-spawn';
6
+ import { LpkInstaller } from './lpk_installer.js';
7
+ import debounce from 'lodash.debounce';
8
8
  import {
9
- mergeYamlInMemory,
10
- contextDirname,
11
- ensureDir,
12
- isFileExist,
13
- GitIgnore,
14
- md5String,
15
- md5File,
16
- loadFromYaml,
17
- isUserApp,
18
- createTemplateFileCommon,
19
- isDebugMode,
20
- resolveDomain,
21
- isWindows,
22
- isMacOs,
23
- isLinux,
24
- pkgInfo
25
- } from "../utils.js"
26
- import os from "node:os"
27
- import chokidar from "chokidar"
28
- import _ from "lodash"
29
- import { DebugBridge } from "../debug_bridge.js"
30
- import shellApi from "../shellapi.js"
31
- import { collectContextFromDockerFile } from "./lpk_devshell_docker.js"
9
+ mergeYamlInMemory,
10
+ contextDirname,
11
+ ensureDir,
12
+ isFileExist,
13
+ GitIgnore,
14
+ md5String,
15
+ md5File,
16
+ loadFromYaml,
17
+ isUserApp,
18
+ createTemplateFileCommon,
19
+ isDebugMode,
20
+ resolveDomain,
21
+ isWindows,
22
+ isMacOs,
23
+ isLinux,
24
+ pkgInfo,
25
+ } from '../utils.js';
26
+ import os from 'node:os';
27
+ import chokidar from 'chokidar';
28
+ import _ from 'lodash';
29
+ import { DebugBridge } from '../debug_bridge.js';
30
+ import shellApi from '../shellapi.js';
31
+ import { t } from '../i18n/index.js';
32
+ import { collectContextFromDockerFile } from './lpk_devshell_docker.js';
32
33
 
33
34
  // 判断是否需要重新构建
34
35
  // - 先判断 lzc-build.yml 是否发生改变
@@ -37,514 +38,424 @@ import { collectContextFromDockerFile } from "./lpk_devshell_docker.js"
37
38
  // - 根据 backend api 判断一个 appid 是否属于 running
38
39
  // - 根据在 backend api 中判断同步的目录下是否存在文件 判断当前运行的 app 是否已经有一个挂载的实例,避免重复挂载
39
40
  class AppDevShellMonitor {
40
- constructor(cwd, pkgId, buildConfigFile) {
41
- this.pwd = cwd ? path.resolve(cwd) : process.cwd()
42
- this.pkgId = pkgId
43
-
44
- this.optionsFilePath = path.join(this.pwd, buildConfigFile)
45
- this.options = loadFromYaml(this.optionsFilePath)
46
-
47
- this.manifestFilePath = this.options["manifest"]
48
- ? path.join(this.pwd, this.options["manifest"])
49
- : path.join(this.pwd, "lzc-manifest.yml")
50
-
51
- this.hashObject = {
52
- build: "",
53
- manifest: ""
54
- }
55
- this.cacheFilePath = undefined
56
- this.oldHash = undefined
57
- this.newHash = undefined
58
- this.bridge = new DebugBridge()
59
- }
60
-
61
- async init() {
62
- const pathId = await md5String(this.pwd)
63
- this.cacheFilePath = path.resolve(
64
- os.tmpdir(),
65
- "lzc-cli-devshell",
66
- pathId,
67
- "hash"
68
- )
69
- ensureDir(this.cacheFilePath)
70
-
71
- await this.updateHash()
72
- await this.bridge.init()
73
-
74
- return this
75
- }
76
-
77
- async shouldBuild() {
78
- return (
79
- this.change() ||
80
- (await this.bridge.status(this.pkgId)) === "NotInstalled" ||
81
- !(await this.bridge.isDevshell(this.pkgId))
82
- )
83
- }
84
-
85
- change() {
86
- logger.debug("oldHash", this.oldHash)
87
- logger.debug("newHash", this.newHash)
88
- return !_.isEqual(this.oldHash, this.newHash)
89
- }
90
-
91
- async updateHash() {
92
- this.oldHash = isFileExist(this.cacheFilePath)
93
- ? JSON.parse(fs.readFileSync(this.cacheFilePath))
94
- : {}
95
- const buildHash = isFileExist(this.optionsFilePath)
96
- ? await md5File(this.optionsFilePath)
97
- : ""
98
- const manifestHash = isFileExist(this.manifestFilePath)
99
- ? await md5File(this.manifestFilePath)
100
- : ""
101
- this.newHash = {
102
- build: buildHash,
103
- manifest: manifestHash,
104
- lzcVersion: pkgInfo.version
105
- }
106
- if (!_.isEqual(this.oldHash, this.newHash)) {
107
- fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.newHash))
108
- }
109
- }
41
+ constructor(cwd, pkgId, buildConfigFile) {
42
+ this.pwd = cwd ? path.resolve(cwd) : process.cwd();
43
+ this.pkgId = pkgId;
44
+
45
+ this.optionsFilePath = path.join(this.pwd, buildConfigFile);
46
+ this.options = loadFromYaml(this.optionsFilePath);
47
+
48
+ this.manifestFilePath = this.options['manifest'] ? path.join(this.pwd, this.options['manifest']) : path.join(this.pwd, 'lzc-manifest.yml');
49
+
50
+ this.hashObject = {
51
+ build: '',
52
+ manifest: '',
53
+ };
54
+ this.cacheFilePath = undefined;
55
+ this.oldHash = undefined;
56
+ this.newHash = undefined;
57
+ this.bridge = new DebugBridge();
58
+ }
59
+
60
+ async init() {
61
+ const pathId = await md5String(this.pwd);
62
+ this.cacheFilePath = path.resolve(os.tmpdir(), 'lzc-cli-devshell', pathId, 'hash');
63
+ ensureDir(this.cacheFilePath);
64
+
65
+ await this.updateHash();
66
+ await this.bridge.init();
67
+
68
+ return this;
69
+ }
70
+
71
+ async shouldBuild() {
72
+ return this.change() || (await this.bridge.status(this.pkgId)) === 'NotInstalled' || !(await this.bridge.isDevshell(this.pkgId));
73
+ }
74
+
75
+ change() {
76
+ logger.debug('oldHash', this.oldHash);
77
+ logger.debug('newHash', this.newHash);
78
+ return !_.isEqual(this.oldHash, this.newHash);
79
+ }
80
+
81
+ async updateHash() {
82
+ this.oldHash = isFileExist(this.cacheFilePath) ? JSON.parse(fs.readFileSync(this.cacheFilePath)) : {};
83
+ const buildHash = isFileExist(this.optionsFilePath) ? await md5File(this.optionsFilePath) : '';
84
+ const manifestHash = isFileExist(this.manifestFilePath) ? await md5File(this.manifestFilePath) : '';
85
+ this.newHash = {
86
+ build: buildHash,
87
+ manifest: manifestHash,
88
+ lzcVersion: pkgInfo.version,
89
+ };
90
+ if (!_.isEqual(this.oldHash, this.newHash)) {
91
+ fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.newHash));
92
+ }
93
+ }
110
94
  }
111
95
 
112
96
  export class AppDevShell {
113
- constructor(cwd, lpkBuild, forceBuild, buildConfigFile) {
114
- this.cwd = cwd
115
- this.lpkBuild = lpkBuild
116
- this.forceBuild = forceBuild
117
- this.buildConfigFile = buildConfigFile
118
- this.isUserApp = false
119
- this.monitor = undefined
120
- }
121
-
122
- async init() {
123
- const manifest = await this.lpkBuild.getManifest()
124
- this.monitor = await new AppDevShellMonitor(
125
- this.cwd,
126
- manifest["package"],
127
- this.buildConfigFile
128
- ).init()
129
- this.isUserApp = isUserApp(manifest)
130
- }
131
-
132
- async build() {
133
- // 先判断是否需要重新构建
134
- if (this.forceBuild || (await this.monitor.shouldBuild())) {
135
- logger.debug("build...")
136
- await this.devshellBuild()
137
- }
138
- }
139
-
140
- async devshellBuild() {
141
- this.lpkBuild.onBeforeBuildPackage(async (options) => {
142
- const devshell = options["devshell"]
143
- if (!devshell) {
144
- throw "devshell 模式下,devshell 字段必须要指定"
145
- }
146
-
147
- const routes = devshell["routes"]
148
- if (!routes || routes.length == 0) {
149
- throw "devshell 模式下,必须要指定 routes 内容"
150
- }
151
-
152
- return options
153
- })
154
-
155
- // 复制 busybox 到 devshell 中去
156
- this.lpkBuild.onBeforeTarContent(async (contentdir) => {
157
- const busyboxPath = path.join(
158
- contextDirname(import.meta.url),
159
- "..",
160
- "..",
161
- "template",
162
- "_lpk",
163
- "busybox-1.35.0"
164
- )
165
- let dest = path.join(contentdir, "devshell", "busybox")
166
- ensureDir(dest)
167
- fs.copyFileSync(busyboxPath, dest)
168
- })
169
-
170
- // 复制 init_debug_bridge.sh devshell 中去
171
- this.lpkBuild.onBeforeTarContent(async (contentdir) => {
172
- const initPath = path.join(
173
- contextDirname(import.meta.url),
174
- "..",
175
- "..",
176
- "template",
177
- "_lpk",
178
- "init_debug_bridge.sh"
179
- )
180
- let dest = path.join(contentdir, "devshell", "init_debug_bridge.sh")
181
- ensureDir(dest)
182
- fs.copyFileSync(initPath, dest)
183
- })
184
-
185
- // 复制 exec.sh devshell 中去
186
- this.lpkBuild.onBeforeTarContent(async (contentdir) => {
187
- const execScriptPath = path.join(
188
- contextDirname(import.meta.url),
189
- "..",
190
- "..",
191
- "template",
192
- "_lpk",
193
- "exec.sh"
194
- )
195
- let dest = path.join(contentdir, "devshell", "exec.sh")
196
- ensureDir(dest)
197
- fs.copyFileSync(execScriptPath, dest)
198
- })
199
-
200
- // 复制 rsyncd.conf 到 devshell 中去
201
- this.lpkBuild.onBeforeTarContent(async (contentdir) => {
202
- const execScriptPath = path.join(
203
- contextDirname(import.meta.url),
204
- "..",
205
- "..",
206
- "template",
207
- "_lpk",
208
- "rsyncd.conf"
209
- )
210
- let dest = path.join(contentdir, "devshell", "rsyncd.conf")
211
- ensureDir(dest)
212
- fs.copyFileSync(execScriptPath, dest)
213
- })
214
-
215
- // 复制 setupscript 脚本
216
- this.lpkBuild.onBeforeTarContent(async (contentdir, options) => {
217
- const devshell = options["devshell"]
218
- if (!devshell["setupscript"]) {
219
- return
220
- }
221
-
222
- logger.debug("处理 setupscript ")
223
- const dest = path.join(contentdir, "devshell", "setupscript")
224
- ensureDir(dest)
225
-
226
- // 先判断是否文件
227
- const filePath = path.resolve(devshell["setupscript"])
228
- if (isFileExist(filePath)) {
229
- fs.copyFileSync(filePath, dest)
230
- } else {
231
- fs.writeFileSync(dest, `#!/bin/sh\nset -ex\n${devshell["setupscript"]}`)
232
- }
233
- })
234
-
235
- // 在生成 manifest.yml 之前合并 devshell manifest 模板字段
236
- this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
237
- logger.debug("merge lzc-build.yml devshell routes field")
238
- const devshell = options["devshell"]
239
-
240
- const routes = devshell["routes"]
241
- logger.debug("options devshell delete 'routes' field")
242
- delete options["devshell"]["routes"]
243
-
244
- // 添加 devshell 必要路由,这里需要使用 /bin/sh 启动后面的脚本,因为在
245
- // Windows 上打包的文件将会丢失可执行权限
246
- routes.push(
247
- "/__debug.bridge=exec://80,/bin/sh /lzcapp/pkg/content/devshell/init_debug_bridge.sh"
248
- )
249
- routes.push("/__isdevshell=file:///lzcapp/pkg/devshell")
250
-
251
- // 如果 devshell 中的 router 和 manifest 中的 prefix 出现冲突
252
- // 优先使用 devshell 中的。
253
- routes.forEach((r) => {
254
- if (!r) {
255
- return
256
- }
257
-
258
- let prefix = r.split("=")[0]
259
- let index = manifest["application"]["routes"].findIndex((mr) => {
260
- if (!mr) {
261
- return false
262
- }
263
- return mr.split("=")[0] == prefix
264
- })
265
- if (index > -1) {
266
- manifest["application"]["routes"].splice(index, 1)
267
- }
268
- })
269
- const application = { routes }
270
- return mergeYamlInMemory([manifest, { application }])
271
- })
272
-
273
- // 在生成 manifest.yml 之前合并 lzc-build.yml devshell 字段的值
274
- // 并加上 health_check 字段, 当处于 devshell 的情况时,禁用 health_check
275
- // 避免应用永远处于 unhealth 导致状态卡在 starting
276
- this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
277
- logger.debug("merge lzc-build.yml devshell services\n", options)
278
- const userapp = this.isUserApp ? shellApi.uid + "." : ""
279
- const devshell = {
280
- application: {
281
- devshell: options["devshell"],
282
- health_check: {
283
- disable: true
284
- }
285
- }
286
- }
287
- return mergeYamlInMemory([manifest, devshell])
288
- })
289
-
290
- // 如果 services/devshell 中有 dependencies 字段,优先使用
291
- this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
292
- const config = manifest["application"]["devshell"]
293
- if (!config || !config["dependencies"]) {
294
- return manifest
295
- }
296
-
297
- const deps = config["dependencies"]
298
- if (deps.length == 0) {
299
- logger.warn("dependencies 内容为空,跳过 dependencies")
300
- delete manifest["application"]["devshell"]["dependencies"]
301
- return manifest
302
- }
303
-
304
- const depsStr = deps.sort().join(" ")
305
- logger.debug("开始创建 Dockerfile 文件")
306
-
307
- const tempDir = fs.mkdtempSync(".lzc-cli-build-dependencies")
308
- try {
309
- const dockerfilePath = path.join(
310
- contextDirname(import.meta.url),
311
- "..",
312
- "..",
313
- "template",
314
- "_lpk",
315
- "Dockerfile.in"
316
- )
317
- await createTemplateFileCommon(
318
- dockerfilePath,
319
- path.join(tempDir, "Dockerfile"),
320
- { dependencies: depsStr }
321
- )
322
-
323
- const label = `${await md5String(depsStr)}:latest`
324
- logger.debug(`开始在盒子中构建 ${label} 镜像 from ${tempDir}`)
325
-
326
- const contextTar = await collectContextFromDockerFile(
327
- tempDir,
328
- path.resolve(tempDir, "Dockerfile")
329
- )
330
- const bridge = new DebugBridge()
331
- await bridge.init()
332
- const tag = await bridge.buildImage(label, contextTar)
333
- delete manifest["application"]["devshell"]
334
- manifest["application"]["image"] = tag
335
- } finally {
336
- fs.rmSync(tempDir, { recursive: true })
337
- }
338
- return manifest
339
- })
340
-
341
- // 如果 services 中有 devshell 的字段,需要检测是否需要提前构建
342
- this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
343
- const application = manifest["application"]
344
- if (!application || !application["devshell"]) {
345
- return manifest
346
- }
347
-
348
- const config = manifest["application"]["devshell"]
349
- if (!config || !config["build"]) {
350
- return manifest
351
- }
352
-
353
- const label = `${manifest["package"]}-devshell:${manifest["version"]}`
354
- logger.debug(`开始在盒子中构建 ${label} 镜像`)
355
-
356
- const contextTar = await collectContextFromDockerFile(
357
- process.cwd(),
358
- path.resolve(process.cwd(), config["build"], "Dockerfile")
359
- )
360
-
361
- const bridge = new DebugBridge()
362
- await bridge.init()
363
- const tag = await bridge.buildImage(label, contextTar)
364
- delete manifest["application"]["devshell"]
365
- manifest["application"]["image"] = tag
366
- return manifest
367
- })
368
-
369
- // 如果 devshell 中指定了 image 字段将使用 image 字段
370
- this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
371
- const config = manifest["application"]
372
- if (config["devshell"] && config["devshell"]["image"]) {
373
- manifest["application"]["image"] = config["devshell"]["image"]
374
- delete manifest["application"]["devshell"]
375
- }
376
- return manifest
377
- })
378
-
379
- // 如果没有找到 devshell 中没有指定 image 不存在,将默认使用的 lzc-cli/devshell 容器
380
- this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
381
- delete manifest["application"]["devshell"]
382
-
383
- const config = manifest["application"]
384
- if (config["image"]) {
385
- return manifest
386
- }
387
-
388
- logger.debug("use default lzc-cli/devshell image")
389
- manifest["application"]["image"] =
390
- `registry.lazycat.cloud/lzc-cli/devshell:v0.0.5`
391
- return manifest
392
- })
393
-
394
- // devshell 模式下,默认打开后台常驻
395
- this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
396
- manifest["application"]["background_task"] = true
397
- return manifest
398
- })
399
-
400
- // 添加一个 devshell 的标记在 lpk 中,标记当前 lpk 为一个 debug 版本
401
- this.lpkBuild.onBeforeDumpLpk(async (options, cwd, destDir) => {
402
- fs.writeFileSync(path.resolve(destDir, "devshell"), "")
403
- })
404
-
405
- // 在构建生成 lpk 包后,调用 deploy 进行部署
406
- let installer = new LpkInstaller()
407
- await installer.init()
408
- await installer.deploy(this.lpkBuild, true)
409
- }
410
-
411
- async rsyncShell() {
412
- const manifest = await this.lpkBuild.getManifest()
413
- const pkgId = manifest["package"]
414
- const devshell = new DevShell(pkgId, this.isUserApp)
415
- try {
416
- await devshell.shell()
417
- } catch (e) {
418
- logger.error(`devshell 错误: ${e}`)
419
- }
420
- logger.debug("exit shell")
421
- // TODO: shell 在正常情况下,按 Ctrl-D 就会退出,回到原来的本地的 shell ,但
422
- // 现在会一直卡在退出状态后,必须要另外手动的指定 pkill node
423
- process.exit(0)
424
- }
97
+ constructor(cwd, lpkBuild, forceBuild, buildConfigFile) {
98
+ this.cwd = cwd;
99
+ this.lpkBuild = lpkBuild;
100
+ this.forceBuild = forceBuild;
101
+ this.buildConfigFile = buildConfigFile;
102
+ this.isUserApp = false;
103
+ this.monitor = undefined;
104
+ }
105
+
106
+ async init() {
107
+ const manifest = await this.lpkBuild.getManifest();
108
+ this.monitor = await new AppDevShellMonitor(this.cwd, manifest['package'], this.buildConfigFile).init();
109
+ this.isUserApp = isUserApp(manifest);
110
+ }
111
+
112
+ async build() {
113
+ // 先判断是否需要重新构建
114
+ if (this.forceBuild || (await this.monitor.shouldBuild())) {
115
+ logger.debug('build...');
116
+ await this.devshellBuild();
117
+ }
118
+ }
119
+
120
+ async devshellBuild() {
121
+ this.lpkBuild.onBeforeBuildPackage(async (options) => {
122
+ const devshell = options['devshell'];
123
+ if (!devshell) {
124
+ throw t('lzc_cli.lib.app.lpk_devshell.devshell_build_field_fail', 'devshell 模式下,devshell 字段必须要指定');
125
+ }
126
+
127
+ const routes = devshell['routes'];
128
+ if (!routes || routes.length == 0) {
129
+ throw t('lzc_cli.lib.app.lpk_devshell.devshell_build_routes_not_exists_fail', 'devshell 模式下,必须要指定 routes 内容');
130
+ }
131
+
132
+ return options;
133
+ });
134
+
135
+ // 复制 busybox 到 devshell 中去
136
+ this.lpkBuild.onBeforeTarContent(async (contentdir) => {
137
+ const busyboxPath = path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', 'busybox-1.35.0');
138
+ let dest = path.join(contentdir, 'devshell', 'busybox');
139
+ ensureDir(dest);
140
+ fs.copyFileSync(busyboxPath, dest);
141
+ });
142
+
143
+ // 复制 init_debug_bridge.sh 到 devshell 中去
144
+ this.lpkBuild.onBeforeTarContent(async (contentdir) => {
145
+ const initPath = path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', 'init_debug_bridge.sh');
146
+ let dest = path.join(contentdir, 'devshell', 'init_debug_bridge.sh');
147
+ ensureDir(dest);
148
+ fs.copyFileSync(initPath, dest);
149
+ });
150
+
151
+ // 复制 exec.sh 到 devshell 中去
152
+ this.lpkBuild.onBeforeTarContent(async (contentdir) => {
153
+ const execScriptPath = path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', 'exec.sh');
154
+ let dest = path.join(contentdir, 'devshell', 'exec.sh');
155
+ ensureDir(dest);
156
+ fs.copyFileSync(execScriptPath, dest);
157
+ });
158
+
159
+ // 复制 rsyncd.conf 到 devshell 中去
160
+ this.lpkBuild.onBeforeTarContent(async (contentdir) => {
161
+ const execScriptPath = path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', 'rsyncd.conf');
162
+ let dest = path.join(contentdir, 'devshell', 'rsyncd.conf');
163
+ ensureDir(dest);
164
+ fs.copyFileSync(execScriptPath, dest);
165
+ });
166
+
167
+ // 复制 setupscript 脚本
168
+ this.lpkBuild.onBeforeTarContent(async (contentdir, options) => {
169
+ const devshell = options['devshell'];
170
+ if (!devshell['setupscript']) {
171
+ return;
172
+ }
173
+
174
+ logger.debug('process setupscript');
175
+ const dest = path.join(contentdir, 'devshell', 'setupscript');
176
+ ensureDir(dest);
177
+
178
+ // 先判断是否文件
179
+ const filePath = path.resolve(devshell['setupscript']);
180
+ if (isFileExist(filePath)) {
181
+ fs.copyFileSync(filePath, dest);
182
+ } else {
183
+ fs.writeFileSync(dest, `#!/bin/sh\nset -ex\n${devshell['setupscript']}`);
184
+ }
185
+ });
186
+
187
+ // 在生成 manifest.yml 之前合并 devshell manifest 模板字段
188
+ this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
189
+ logger.debug('merge lzc-build.yml devshell routes field');
190
+ const devshell = options['devshell'];
191
+
192
+ const routes = devshell['routes'];
193
+ logger.debug("options devshell delete 'routes' field");
194
+ delete options['devshell']['routes'];
195
+
196
+ // 添加 devshell 必要路由,这里需要使用 /bin/sh 启动后面的脚本,因为在
197
+ // Windows 上打包的文件将会丢失可执行权限
198
+ routes.push('/__debug.bridge=exec://80,/bin/sh /lzcapp/pkg/content/devshell/init_debug_bridge.sh');
199
+ routes.push('/__isdevshell=file:///lzcapp/pkg/devshell');
200
+
201
+ // 如果 devshell 中的 router 和 manifest 中的 prefix 出现冲突
202
+ // 优先使用 devshell 中的。
203
+ routes.forEach((r) => {
204
+ if (!r) {
205
+ return;
206
+ }
207
+
208
+ let prefix = r.split('=')[0];
209
+ let index = manifest['application']['routes'].findIndex((mr) => {
210
+ if (!mr) {
211
+ return false;
212
+ }
213
+ return mr.split('=')[0] == prefix;
214
+ });
215
+ if (index > -1) {
216
+ manifest['application']['routes'].splice(index, 1);
217
+ }
218
+ });
219
+ const application = { routes };
220
+ return mergeYamlInMemory([manifest, { application }]);
221
+ });
222
+
223
+ // 在生成 manifest.yml 之前合并 lzc-build.yml devshell 字段的值
224
+ // 并加上 health_check 字段, 当处于 devshell 的情况时,禁用 health_check
225
+ // 避免应用永远处于 unhealth 导致状态卡在 starting
226
+ this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
227
+ logger.debug('merge lzc-build.yml devshell services\n', options);
228
+ const userapp = this.isUserApp ? shellApi.uid + '.' : '';
229
+ const devshell = {
230
+ application: {
231
+ devshell: options['devshell'],
232
+ health_check: {
233
+ disable: true,
234
+ },
235
+ },
236
+ };
237
+ return mergeYamlInMemory([manifest, devshell]);
238
+ });
239
+
240
+ // 如果 services/devshell 中有 dependencies 字段,优先使用
241
+ this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
242
+ const config = manifest['application']['devshell'];
243
+ if (!config || !config['dependencies']) {
244
+ return manifest;
245
+ }
246
+
247
+ const deps = config['dependencies'];
248
+ if (deps.length == 0) {
249
+ logger.warn(t('lzc_cli.lib.app.lpk_devshell.devshell_build_skip_dependencies_tips', 'dependencies 内容为空,跳过 dependencies'));
250
+ delete manifest['application']['devshell']['dependencies'];
251
+ return manifest;
252
+ }
253
+
254
+ const depsStr = deps.sort().join(' ');
255
+ logger.debug(t('lzc_cli.lib.app.lpk_devshell.devshell_build_dockerfile_tips', '开始创建 Dockerfile 文件'));
256
+
257
+ const tempDir = fs.mkdtempSync('.lzc-cli-build-dependencies');
258
+ try {
259
+ const dockerfilePath = path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', 'Dockerfile.in');
260
+ await createTemplateFileCommon(dockerfilePath, path.join(tempDir, 'Dockerfile'), { dependencies: depsStr });
261
+
262
+ const label = `${await md5String(depsStr)}:latest`;
263
+ logger.debug(t('lzc_cli.lib.app.lpk_devshell.devshell_build_image_for_box_tips', `开始在盒子中构建 {{label}} 镜像 from {{tempDir}}`, { label, tempDir }));
264
+
265
+ const contextTar = await collectContextFromDockerFile(tempDir, path.resolve(tempDir, 'Dockerfile'));
266
+ const bridge = new DebugBridge();
267
+ await bridge.init();
268
+ const tag = await bridge.buildImage(label, contextTar);
269
+ delete manifest['application']['devshell'];
270
+ manifest['application']['image'] = tag;
271
+ } finally {
272
+ fs.rmSync(tempDir, { recursive: true });
273
+ }
274
+ return manifest;
275
+ });
276
+
277
+ // 如果 services 中有 devshell 的字段,需要检测是否需要提前构建
278
+ this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
279
+ const application = manifest['application'];
280
+ if (!application || !application['devshell']) {
281
+ return manifest;
282
+ }
283
+
284
+ const config = manifest['application']['devshell'];
285
+ if (!config || !config['build']) {
286
+ return manifest;
287
+ }
288
+
289
+ const label = `${manifest['package']}-devshell:${manifest['version']}`;
290
+ logger.debug(t('lzc_cli.lib.app.lpk_devshell.devshell_build_label_image_box_tips', `开始在盒子中构建 {{label}} 镜像`, { label }));
291
+
292
+ const contextTar = await collectContextFromDockerFile(process.cwd(), path.resolve(process.cwd(), config['build'], 'Dockerfile'));
293
+
294
+ const bridge = new DebugBridge();
295
+ await bridge.init();
296
+ const tag = await bridge.buildImage(label, contextTar);
297
+ delete manifest['application']['devshell'];
298
+ manifest['application']['image'] = tag;
299
+ return manifest;
300
+ });
301
+
302
+ // 如果 devshell 中指定了 image 字段将使用 image 字段
303
+ this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
304
+ const config = manifest['application'];
305
+ if (config['devshell'] && config['devshell']['image']) {
306
+ manifest['application']['image'] = config['devshell']['image'];
307
+ delete manifest['application']['devshell'];
308
+ }
309
+ return manifest;
310
+ });
311
+
312
+ // 如果没有找到 devshell 中没有指定 image 不存在,将默认使用的 lzc-cli/devshell 容器
313
+ this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
314
+ delete manifest['application']['devshell'];
315
+
316
+ const config = manifest['application'];
317
+ if (config['image']) {
318
+ return manifest;
319
+ }
320
+
321
+ logger.debug('use default lzc-cli/devshell image');
322
+ manifest['application']['image'] = `registry.lazycat.cloud/lzc-cli/devshell:v0.0.5`;
323
+ return manifest;
324
+ });
325
+
326
+ // devshell 模式下,默认打开后台常驻
327
+ this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
328
+ manifest['application']['background_task'] = true;
329
+ return manifest;
330
+ });
331
+
332
+ // 添加一个 devshell 的标记在 lpk 中,标记当前 lpk 为一个 debug 版本
333
+ this.lpkBuild.onBeforeDumpLpk(async (options, cwd, destDir) => {
334
+ fs.writeFileSync(path.resolve(destDir, 'devshell'), '');
335
+ });
336
+
337
+ // 在构建生成 lpk 包后,调用 deploy 进行部署
338
+ let installer = new LpkInstaller();
339
+ await installer.init();
340
+ await installer.deploy(this.lpkBuild, true);
341
+ }
342
+
343
+ async rsyncShell() {
344
+ const manifest = await this.lpkBuild.getManifest();
345
+ const pkgId = manifest['package'];
346
+ const devshell = new DevShell(pkgId, this.isUserApp);
347
+ try {
348
+ await devshell.shell();
349
+ } catch (e) {
350
+ logger.error(`devshell fail: ${e}`);
351
+ }
352
+ logger.debug('exit shell');
353
+ // TODO: shell 在正常情况下,按 Ctrl-D 就会退出,回到原来的本地的 shell ,但
354
+ // 现在会一直卡在退出状态后,必须要另外手动的指定 pkill node
355
+ process.exit(0);
356
+ }
425
357
  }
426
358
 
427
359
  class DevShell {
428
- constructor(appId, isUserApp) {
429
- this.appId = appId
430
- this.isUserApp = isUserApp
431
- }
432
-
433
- async syncProject(appId) {
434
- const resolvedIp = await resolveDomain(
435
- `dev.${shellApi.boxname}.heiyu.space`
436
- )
437
- const rsyncDebug = isDebugMode() ? "-P" : ""
438
- const destDir = `${appId}${this.isUserApp ? "/" + shellApi.uid : ""}`
439
- const dest = `rsync://${shellApi.uid}@[${resolvedIp}]:874/lzcapp_cache/${destDir}/devshell`
440
- const rsyncCmd = isWindows
441
- ? path.join(
442
- contextDirname(import.meta.url),
443
- "..",
444
- "..",
445
- "template",
446
- "_lpk",
447
- "win-rsync",
448
- "rsync.exe"
449
- )
450
- : `rsync`
451
-
452
- try {
453
- const rsyncArgs = [
454
- `${rsyncDebug}`,
455
- `--recursive`,
456
- `--relative`,
457
- `--perms`,
458
- `--update`,
459
- `-F --filter=':- .gitignore'`,
460
- `--ignore-errors`,
461
- `. ${dest}`
462
- ]
463
- logger.debug("同步代码: ", rsyncCmd, rsyncArgs.join(" "))
464
- const rsyncStream = spawn.sync(rsyncCmd, rsyncArgs, {
465
- env: { ...process.env, RSYNC_PASSWORD: "fakefakefake" },
466
- shell: true,
467
- stdio: ["ignore", isDebugMode() ? "inherit" : "ignore", "inherit"]
468
- })
469
- } catch (err) {
470
- logger.error("rsync 同步失败")
471
- logger.debug(err)
472
- }
473
- }
474
-
475
- // fallback fs.watch on not darwin and windows platform
476
- // FILEPATH directory or file path
477
- // CALLBACK => function(eventType, filename)
478
- // fs.watch 虽然不支持递归,但可以直接监听整个文件夹的变动
479
- async fallbackWatch(filepath, gitignore, callback) {
480
- if (gitignore.contain(filepath)) {
481
- return Promise.resolve()
482
- }
483
-
484
- if (filepath.endsWith(".git") || filepath.endsWith(".lazycat")) {
485
- return Promise.resolve()
486
- }
487
-
488
- fs.watch(filepath, callback(filepath))
489
-
490
- // 如果为一个文件夹,则扫描当中是否含有子文件夹
491
- if (isDirSync(filepath)) {
492
- return gitignore.readdir(filepath, (err, files) => {
493
- if (err) {
494
- throw err
495
- }
496
-
497
- if (files.length <= 0) {
498
- return
499
- }
500
-
501
- files.forEach((f) => {
502
- if (f.isDirectory()) {
503
- this.fallbackWatch(path.join(filepath, f.name), gitignore, callback)
504
- }
505
- })
506
- })
507
- }
508
- }
509
-
510
- // 监听非.gitignore文件
511
- // TODO: 目前仅仅监听process.cwd()以下的文件
512
- async watchFile(appId) {
513
- const ignore = new GitIgnore(process.cwd())
514
- await ignore.collect()
515
- chokidar
516
- .watch(".", {
517
- ignored: (path) => {
518
- if ([".git", ".lazycat"].some((p) => path.startsWith(p))) return true
519
-
520
- return ignore.contain(path)
521
- },
522
- ignoreInitial: true
523
- })
524
- .on(
525
- "all",
526
- debounce(() => {
527
- this.syncProject(appId)
528
- }, 1000)
529
- )
530
- }
531
-
532
- async shell() {
533
- try {
534
- // 监听文件
535
- await this.watchFile(this.appId)
536
- await this.connectShell(async () => {
537
- // 在连接成功的时候,同步一次文件
538
- await this.syncProject(this.appId)
539
- })
540
- } catch (e) {
541
- return Promise.reject(e)
542
- }
543
- }
544
-
545
- async connectShell(onconnect = null) {
546
- const bridge = new DebugBridge()
547
- await bridge.init()
548
- await bridge.devshell(this.appId, this.isUserApp, onconnect)
549
- }
360
+ constructor(appId, isUserApp) {
361
+ this.appId = appId;
362
+ this.isUserApp = isUserApp;
363
+ }
364
+
365
+ async syncProject(appId) {
366
+ const resolvedIp = await resolveDomain(`dev.${shellApi.boxname}.heiyu.space`);
367
+ const rsyncDebug = isDebugMode() ? '-P' : '';
368
+ const destDir = `${appId}${this.isUserApp ? '/' + shellApi.uid : ''}`;
369
+ const dest = `rsync://${shellApi.uid}@[${resolvedIp}]:874/lzcapp_cache/${destDir}/devshell`;
370
+ const rsyncCmd = isWindows ? path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', 'win-rsync', 'rsync.exe') : `rsync`;
371
+
372
+ try {
373
+ const rsyncArgs = [`${rsyncDebug}`, `--recursive`, `--relative`, `--perms`, `--update`, `-F --filter=':- .gitignore'`, `--ignore-errors`, `. ${dest}`];
374
+ logger.debug(t('lzc_cli.lib.app.lpk_devshell.sync_project_code_tips', '同步代码: '), rsyncCmd, rsyncArgs.join(' '));
375
+ const rsyncStream = spawn.sync(rsyncCmd, rsyncArgs, {
376
+ env: { ...process.env, RSYNC_PASSWORD: 'fakefakefake' },
377
+ shell: true,
378
+ stdio: ['ignore', isDebugMode() ? 'inherit' : 'ignore', 'inherit'],
379
+ });
380
+ } catch (err) {
381
+ logger.error(t('lzc_cli.lib.app.lpk_devshell.sync_project_rsync_tips', 'rsync 同步失败'));
382
+ logger.debug(err);
383
+ }
384
+ }
385
+
386
+ // fallback fs.watch on not darwin and windows platform
387
+ // FILEPATH directory or file path
388
+ // CALLBACK => function(eventType, filename)
389
+ // fs.watch 虽然不支持递归,但可以直接监听整个文件夹的变动
390
+ async fallbackWatch(filepath, gitignore, callback) {
391
+ if (gitignore.contain(filepath)) {
392
+ return Promise.resolve();
393
+ }
394
+
395
+ if (filepath.endsWith('.git') || filepath.endsWith('.lazycat')) {
396
+ return Promise.resolve();
397
+ }
398
+
399
+ fs.watch(filepath, callback(filepath));
400
+
401
+ // 如果为一个文件夹,则扫描当中是否含有子文件夹
402
+ if (isDirSync(filepath)) {
403
+ return gitignore.readdir(filepath, (err, files) => {
404
+ if (err) {
405
+ throw err;
406
+ }
407
+
408
+ if (files.length <= 0) {
409
+ return;
410
+ }
411
+
412
+ files.forEach((f) => {
413
+ if (f.isDirectory()) {
414
+ this.fallbackWatch(path.join(filepath, f.name), gitignore, callback);
415
+ }
416
+ });
417
+ });
418
+ }
419
+ }
420
+
421
+ // 监听非.gitignore文件
422
+ // TODO: 目前仅仅监听process.cwd()以下的文件
423
+ async watchFile(appId) {
424
+ const ignore = new GitIgnore(process.cwd());
425
+ await ignore.collect();
426
+ chokidar
427
+ .watch('.', {
428
+ ignored: (path) => {
429
+ if (['.git', '.lazycat'].some((p) => path.startsWith(p))) return true;
430
+
431
+ return ignore.contain(path);
432
+ },
433
+ ignoreInitial: true,
434
+ })
435
+ .on(
436
+ 'all',
437
+ debounce(() => {
438
+ this.syncProject(appId);
439
+ }, 1000),
440
+ );
441
+ }
442
+
443
+ async shell() {
444
+ try {
445
+ // 监听文件
446
+ await this.watchFile(this.appId);
447
+ await this.connectShell(async () => {
448
+ // 在连接成功的时候,同步一次文件
449
+ await this.syncProject(this.appId);
450
+ });
451
+ } catch (e) {
452
+ return Promise.reject(e);
453
+ }
454
+ }
455
+
456
+ async connectShell(onconnect = null) {
457
+ const bridge = new DebugBridge();
458
+ await bridge.init();
459
+ await bridge.devshell(this.appId, this.isUserApp, onconnect);
460
+ }
550
461
  }