@lazycatcloud/lzc-cli 1.1.7 → 1.1.9

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.
Files changed (105) hide show
  1. package/README.md +69 -11
  2. package/lib/api.js +71 -36
  3. package/lib/app/index.js +79 -23
  4. package/lib/app/lpk_build.js +96 -52
  5. package/lib/app/lpk_create.js +63 -41
  6. package/lib/app/lpk_create_generator.js +202 -0
  7. package/lib/app/lpk_devshell.js +393 -328
  8. package/lib/app/lpk_devshell_docker.js +211 -0
  9. package/lib/app/lpk_installer.js +63 -26
  10. package/lib/app/lpk_log.js +68 -0
  11. package/lib/app/lpk_status.js +18 -0
  12. package/lib/app/lpk_uninstall.js +19 -0
  13. package/lib/appstore/index.js +37 -0
  14. package/lib/appstore/login.js +137 -0
  15. package/lib/appstore/publish.js +62 -0
  16. package/lib/autologin.js +0 -80
  17. package/lib/box/api/clientapi.js +1322 -0
  18. package/lib/box/api/empty.js +35 -0
  19. package/lib/box/check_qemu.js +1 -0
  20. package/lib/box/index.js +41 -94
  21. package/lib/box/qemu_vm_mgr.js +208 -239
  22. package/lib/box/schemes/vm_box_system_debian.json +1 -1
  23. package/lib/docker-compose.js +1 -2
  24. package/lib/env.js +23 -142
  25. package/lib/key.js +1 -0
  26. package/lib/sdk.js +10 -25
  27. package/lib/utils.js +156 -233
  28. package/package.json +19 -11
  29. package/scripts/cli.js +14 -135
  30. package/template/_lpk/README.md +31 -0
  31. package/template/_lpk/exec.sh +19 -0
  32. package/template/_lpk/golang.manifest.yml.in +16 -0
  33. package/template/_lpk/lazycat.png +0 -0
  34. package/template/_lpk/lite.manifest.yml.in +19 -0
  35. package/template/_lpk/local_devshell/Dockerfile +16 -0
  36. package/template/_lpk/local_devshell/build.sh +5 -0
  37. package/template/_lpk/local_devshell/entrypoint.sh +87 -0
  38. package/template/{_lazycat/debug/shell → _lpk/local_devshell}/sshd_config +8 -8
  39. package/template/_lpk/manifest.yml.in +0 -1
  40. package/template/{vue/lzc-build.yml → _lpk/vue.lzc-build.yml.in} +9 -1
  41. package/template/golang/README.md +0 -2
  42. package/template/golang/_gitignore +2 -0
  43. package/template/golang/build.sh +6 -0
  44. package/template/golang/lazycat.png +0 -0
  45. package/template/golang/lzc-build.yml +9 -1
  46. package/template/golang/rego.go +15 -16
  47. package/template/golang/rego_test.go +13 -0
  48. package/template/ionic_vue3/lazycat.png +0 -0
  49. package/template/ionic_vue3/lzc-build.yml +9 -1
  50. package/template/lite/error_pages/502.html.tpl +13 -0
  51. package/template/lite/lazycat.png +0 -0
  52. package/template/lite/lzc-build.yml +60 -0
  53. package/cmds/app.js +0 -133
  54. package/cmds/config.js +0 -55
  55. package/cmds/create.js +0 -55
  56. package/cmds/dev.js +0 -130
  57. package/cmds/init.js +0 -125
  58. package/cmds/log.js +0 -103
  59. package/cmds/publish.js +0 -116
  60. package/lib/archiver.js +0 -105
  61. package/lib/box/hportal.js +0 -120
  62. package/lib/builder.js +0 -313
  63. package/lib/dev.js +0 -314
  64. package/lib/generator.js +0 -146
  65. package/template/_lazycat/_gitignore +0 -1
  66. package/template/_lazycat/app-config +0 -1
  67. package/template/_lazycat/debug/devforward/50x.html +0 -30
  68. package/template/_lazycat/debug/devforward/Dockerfile +0 -16
  69. package/template/_lazycat/debug/devforward/docker-compose.override.yml.in +0 -11
  70. package/template/_lazycat/debug/devforward/entrypoint.sh +0 -10
  71. package/template/_lazycat/debug/devforward/nginx.conf.template +0 -56
  72. package/template/_lazycat/debug/devforward/sshd_config +0 -116
  73. package/template/_lazycat/debug/shell/50x.html +0 -32
  74. package/template/_lazycat/debug/shell/Dockerfile +0 -18
  75. package/template/_lazycat/debug/shell/build.sh +0 -15
  76. package/template/_lazycat/debug/shell/docker-compose.override.yml.in +0 -13
  77. package/template/_lazycat/debug/shell/entrypoint.sh +0 -12
  78. package/template/_lazycat/docker-compose.yml.in +0 -15
  79. package/template/_lazycat/icon.svg +0 -1
  80. package/template/_lazycat/screenshot.png +0 -0
  81. package/template/_lpk/sync/Dockerfile +0 -16
  82. package/template/_lpk/sync/build.sh +0 -5
  83. package/template/_lpk/sync/entrypoint.sh +0 -8
  84. package/template/_lpk/sync/sshd_config +0 -117
  85. package/template/_lpk/sync.manifest.yml.in +0 -3
  86. package/template/release/golang/Dockerfile +0 -18
  87. package/template/release/golang/build.sh +0 -13
  88. package/template/release/ionic_vue3/Dockerfile +0 -10
  89. package/template/release/ionic_vue3/build.sh +0 -7
  90. package/template/release/ionic_vue3/docker-compose.yml.in +0 -3
  91. package/template/release/vue/Dockerfile +0 -10
  92. package/template/release/vue/build.sh +0 -10
  93. package/template/release/vue/docker-compose.yml.in +0 -3
  94. package/template/vue/README.md +0 -29
  95. package/template/vue/_dockerignore +0 -1
  96. package/template/vue/babel.config.js +0 -3
  97. package/template/vue/package.json +0 -43
  98. package/template/vue/public/favicon.ico +0 -0
  99. package/template/vue/public/index.html +0 -33
  100. package/template/vue/src/App.vue +0 -39
  101. package/template/vue/src/main.js +0 -8
  102. package/template/vue/src/todo.vue +0 -640
  103. package/template/vue/src/top-bar.vue +0 -100
  104. package/template/vue/src/webdav.vue +0 -183
  105. package/template/vue/vue.config.js +0 -21
@@ -2,9 +2,10 @@
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
4
  import logger from "loglevel";
5
- import { spawnSync, execSync } from "child_process";
5
+ import { spawn, execSync } from "node:child_process";
6
6
  import { LpkBuild } from "./lpk_build.js";
7
7
  import { LpkInstaller } from "./lpk_installer.js";
8
+ import debounce from "lodash.debounce";
8
9
  import {
9
10
  mergeYamlInMemory,
10
11
  contextDirname,
@@ -14,7 +15,10 @@ import {
14
15
  sleep,
15
16
  envTemplateFile,
16
17
  md5String,
18
+ md5File,
17
19
  createTemplateFileCommon,
20
+ loadFromYaml,
21
+ FileLocker,
18
22
  } from "../utils.js";
19
23
  import yaml from "js-yaml";
20
24
  import Key from "../key.js";
@@ -22,18 +26,153 @@ import os from "node:os";
22
26
  import { sdkEnv } from "../env.js";
23
27
  import commandExists from "command-exists";
24
28
  import chokidar from "chokidar";
25
- import execa from "execa";
29
+ import _ from "lodash";
30
+ import BoxAPI from "../api.js";
31
+ import inquirer from "inquirer";
32
+ import { SdkDocker } from "./lpk_devshell_docker.js";
26
33
 
27
- function sdkSSHAddress() {
34
+ function sdkSSHHost() {
28
35
  const sdkUrl = sdkEnv.sdkUrl;
29
36
  let url = new URL(sdkUrl);
30
- return `box@${url.hostname}:2222`;
37
+ return `box@${url.hostname}`;
38
+ }
39
+
40
+ function sdkSSHPort() {
41
+ return 2222;
42
+ }
43
+
44
+ function sdkSSHAddress() {
45
+ const host = sdkSSHHost();
46
+ const port = sdkSSHPort();
47
+ return `${host}:${port}`;
48
+ }
49
+
50
+ // 如果本地的机器没有 docker, 将会使用 rsync 同步
51
+ export function fallbackToRsync() {
52
+ return !commandExists.sync("docker");
53
+ }
54
+
55
+ // 判断是否需要重新构建
56
+ // - 先判断 lzc-build.yml 是否发生改变
57
+ // - 再判断 lzc-build.yml 中的 manifest 中指定的文件是否发生改变
58
+ // - 文件的 md5 缓存在 /tmp/lzc-cli-devshell/$PKGID/hash => {}
59
+ // - 根据 backend api 判断一个 appid 是否属于 running
60
+ // - 根据在 backend api 中判断同步的目录下是否存在文件 判断当前运行的 app 是否已经有一个挂载的实例,避免重复挂载
61
+ class AppDevShellMonitor {
62
+ constructor(cwd, pkgId, rsyncMode = false) {
63
+ this.pwd = cwd ? path.resolve(cwd) : process.cwd();
64
+ this.rsyncMode = rsyncMode;
65
+
66
+ this.pkgId = pkgId;
67
+ this.boxapi = new BoxAPI(pkgId, sdkEnv.sdkUrl);
68
+
69
+ this.optionsFilePath = path.join(this.pwd, "lzc-build.yml");
70
+ this.options = loadFromYaml(this.optionsFilePath);
71
+
72
+ this.manifestFilePath = this.options["manifest"]
73
+ ? path.join(this.pwd, this.options["manifest"])
74
+ : path.join(this.pwd, "lzc-manifest.yml");
75
+
76
+ this.hashObject = {
77
+ build: "",
78
+ manifest: "",
79
+ };
80
+ this.cacheFilePath = undefined;
81
+ this.oldHash = undefined;
82
+ this.newHash = undefined;
83
+ }
84
+
85
+ async init() {
86
+ const pathId = await md5String(this.pwd);
87
+ this.cacheFilePath = path.resolve(
88
+ os.tmpdir(),
89
+ "lzc-cli-devshell",
90
+ pathId,
91
+ "hash"
92
+ );
93
+ ensureDir(this.cacheFilePath);
94
+
95
+ await this.updateHash();
96
+
97
+ return this;
98
+ }
99
+
100
+ async shouldBuild() {
101
+ return (
102
+ this.change() ||
103
+ (await this.notRunning()) ||
104
+ (await this.noMount()) ||
105
+ (await this.noDevshell())
106
+ );
107
+ }
108
+
109
+ async shouldRemount() {
110
+ try {
111
+ return await this.noMount();
112
+ } catch {
113
+ return true;
114
+ }
115
+ }
116
+
117
+ change() {
118
+ logger.debug("oldHash", this.oldHash);
119
+ logger.debug("newHash", this.newHash);
120
+ return !_.isEqual(this.oldHash, this.newHash);
121
+ }
122
+
123
+ async updateHash() {
124
+ this.oldHash = isFileExist(this.cacheFilePath)
125
+ ? JSON.parse(fs.readFileSync(this.cacheFilePath))
126
+ : {};
127
+ const buildHash = isFileExist(this.optionsFilePath)
128
+ ? await md5File(this.optionsFilePath)
129
+ : "";
130
+ const manifestHash = isFileExist(this.manifestFilePath)
131
+ ? await md5File(this.manifestFilePath)
132
+ : "";
133
+ this.newHash = {
134
+ build: buildHash,
135
+ manifest: manifestHash,
136
+ };
137
+ if (!_.isEqual(this.oldHash, this.newHash)) {
138
+ fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.newHash));
139
+ }
140
+ }
141
+
142
+ async notRunning() {
143
+ try {
144
+ const { status } = await this.boxapi.status();
145
+ return status !== "running";
146
+ } catch {
147
+ return true;
148
+ }
149
+ }
150
+
151
+ async noMount() {
152
+ let hasMount = await this.boxapi.hasMount();
153
+ if (hasMount && this.rsyncMode) {
154
+ throw "sshfs docker 进程还在运行中,如果需要使用rsync模式,请先停止;或者继续使用sshfs模式";
155
+ }
156
+ if (this.rsyncMode) {
157
+ // rsync mode skip mount check
158
+ return false;
159
+ }
160
+ return !hasMount;
161
+ }
162
+
163
+ async noDevshell() {
164
+ return !(await this.boxapi.isDevshell());
165
+ }
31
166
  }
32
167
 
33
168
  export class AppDevShell {
34
- constructor(cwd, builder) {
169
+ constructor(cwd, builder, rsyncMode, forceBuild = false) {
35
170
  this.cwd = cwd ? path.resolve(cwd) : process.cwd();
36
171
  this.lpkBuild = builder;
172
+ this.monitor = undefined;
173
+ this.sshfsProcess = undefined;
174
+ this.rsyncMode = rsyncMode;
175
+ this.forceBuild = forceBuild;
37
176
  }
38
177
 
39
178
  async init() {
@@ -41,9 +180,28 @@ export class AppDevShell {
41
180
  this.lpkBuild = new LpkBuild(this.cwd);
42
181
  await this.lpkBuild.init();
43
182
  }
183
+ const manifest = await this.lpkBuild.getManifest();
184
+ this.monitor = await new AppDevShellMonitor(
185
+ this.cwd,
186
+ manifest["package"],
187
+ this.rsyncMode
188
+ ).init();
189
+ }
190
+
191
+ async build() {
192
+ // 先判断是否需要重新构建
193
+ if (this.forceBuild || (await this.monitor.shouldBuild())) {
194
+ logger.debug("build...");
195
+ await this.devshellBuild();
196
+ }
197
+ // 判断是否需要重新启动 sshfs 容器
198
+ if (!this.rsyncMode && (await this.monitor.shouldRemount())) {
199
+ logger.debug("mount...");
200
+ this.sshfsProcess = await this.sshfs();
201
+ }
44
202
  }
45
203
 
46
- async shellWithBuild() {
204
+ async devshellBuild() {
47
205
  // 确保 sdk key ,并上传到 sdk
48
206
  const k = new Key();
49
207
  await k.ensure(sdkEnv.sdkUrl);
@@ -55,9 +213,32 @@ export class AppDevShell {
55
213
  logger.debug("devshell delete 'contentdir' field");
56
214
  delete options["buildscript"];
57
215
  delete options["contentdir"];
216
+
217
+ const devshell = options["devshell"];
218
+ if (!devshell) {
219
+ throw "devshell 模式下,devshell 字段必须要指定";
220
+ }
221
+
222
+ const routes = devshell["routes"];
223
+ if (!routes || routes.length == 0) {
224
+ throw "devshell 模式下,必须要指定 routes 内容";
225
+ }
226
+
58
227
  return options;
59
228
  });
60
229
 
230
+ // 复制 exec.sh 到 devshell 中去
231
+ this.lpkBuild.onBeforeTarContent(async (contentdir) => {
232
+ const execScriptPath = path.join(
233
+ contextDirname(import.meta.url),
234
+ "../../template/_lpk/exec.sh"
235
+ );
236
+ let dest = path.join(contentdir, "devshell", "exec.sh");
237
+ ensureDir(dest);
238
+ fs.copyFileSync(execScriptPath, dest);
239
+ fs.chmodSync(dest, 0o775);
240
+ });
241
+
61
242
  // 用 sdk ssh key 并复制到 contentdir 目录
62
243
  // docker 中 sshd 的配置中已改成 /lzcapp/pkg/content/devshell/authorized_keys
63
244
  this.lpkBuild.onBeforeTarContent(async (contentdir) => {
@@ -67,36 +248,36 @@ export class AppDevShell {
67
248
  fs.copyFileSync(publicKey, dest);
68
249
  });
69
250
 
70
- // 在生成 manifest.yml 之前合并 sync 模板字段
71
- this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
72
- const services = manifest["services"];
73
- if (services && Object.keys(services).some((key) => key === "sync")) {
74
- throw "自定义的 services 中与 devshell 中的 sync 名称冲突";
251
+ // 复制 setupscript 脚本
252
+ this.lpkBuild.onBeforeTarContent(async (contentdir, options) => {
253
+ const devshell = options["devshell"];
254
+ if (!devshell["setupscript"]) {
255
+ return;
75
256
  }
76
257
 
77
- const syncManifestPath = path.join(
78
- contextDirname(import.meta.url),
79
- "../../template/_lpk/sync.manifest.yml.in"
80
- );
81
- const syncManifest = yaml.load(
82
- await envTemplateFile(syncManifestPath, manifest)
83
- );
84
- return mergeYamlInMemory([manifest, syncManifest]);
258
+ logger.debug("处理 setupscript ");
259
+ const dest = path.join(contentdir, "devshell", "setupscript");
260
+ ensureDir(dest);
261
+
262
+ // 先判断是否文件
263
+ const filePath = path.resolve(devshell["setupscript"]);
264
+ if (isFileExist(filePath)) {
265
+ fs.copyFileSync(filePath, dest);
266
+ } else {
267
+ fs.writeFileSync(
268
+ dest,
269
+ `#!/bin/sh\nset -ex\n${devshell["setupscript"]}`
270
+ );
271
+ }
272
+ fs.chmodSync(dest, 0o775);
85
273
  });
86
274
 
87
275
  // 在生成 manifest.yml 之前合并 devshell manifest 模板字段
88
276
  this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
89
277
  logger.debug("merge lzc-build.yml devshell routes field");
90
278
  const devshell = options["devshell"];
91
- if (!devshell) {
92
- throw "devshell 模式下,devshell 字段必须要指定";
93
- }
94
279
 
95
280
  const routes = devshell["routes"];
96
- if (!routes || routes.length == 0) {
97
- throw "devshell 模式下,必须要指定 routes 内容";
98
- }
99
-
100
281
  logger.debug("options devshell delete 'routes' field");
101
282
  delete options["devshell"]["routes"];
102
283
  // 如果 devshell 中的 router 和 manifest 中的 prefix 出现冲突
@@ -148,23 +329,26 @@ export class AppDevShell {
148
329
 
149
330
  const depsStr = deps.sort().join(" ");
150
331
  logger.debug("开始创建 Dockerfile 文件");
151
- const tempDir = fs.mkdtempSync(".lzc-cli-build-dependencies");
152
- const dockerfilePath = path.join(
153
- contextDirname(import.meta.url),
154
- "../../template/_lpk/Dockerfile.in"
155
- );
156
- await createTemplateFileCommon(
157
- dockerfilePath,
158
- path.join(tempDir, "Dockerfile"),
159
- { dependencies: depsStr }
160
- );
161
-
162
332
  const tag = `${await md5String(depsStr)}:latest`;
163
- logger.debug(`开始在盒子中构建 ${tag} 镜像 from ${tempDir}`);
164
- const sdk = new sdkDocker();
165
- await sdk.buildImage(tag, tempDir, tempDir);
166
333
 
167
- fs.rmSync(tempDir, { recursive: true });
334
+ const tempDir = fs.mkdtempSync(".lzc-cli-build-dependencies");
335
+ try {
336
+ const dockerfilePath = path.join(
337
+ contextDirname(import.meta.url),
338
+ "../../template/_lpk/Dockerfile.in"
339
+ );
340
+ await createTemplateFileCommon(
341
+ dockerfilePath,
342
+ path.join(tempDir, "Dockerfile"),
343
+ { dependencies: depsStr }
344
+ );
345
+
346
+ logger.debug(`开始在盒子中构建 ${tag} 镜像 from ${tempDir}`);
347
+ const sdk = new SdkDocker();
348
+ await sdk.buildImage(tag, tempDir, tempDir);
349
+ } finally {
350
+ fs.rmSync(tempDir, { recursive: true });
351
+ }
168
352
 
169
353
  delete manifest["application"]["devshell"];
170
354
  manifest["application"]["image"] = tag;
@@ -187,7 +371,7 @@ export class AppDevShell {
187
371
 
188
372
  logger.debug(`开始在盒子中构建 ${tag} 镜像`);
189
373
 
190
- const sdk = new sdkDocker();
374
+ const sdk = new SdkDocker();
191
375
  await sdk.buildImage(tag, config["build"], process.cwd());
192
376
 
193
377
  delete manifest["application"]["devshell"];
@@ -195,66 +379,135 @@ export class AppDevShell {
195
379
  return manifest;
196
380
  });
197
381
 
382
+ // 如果 devshell 中指定了 image 字段将使用 image 字段
383
+ this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
384
+ const config = manifest["application"];
385
+ if (config["devshell"] && config["devshell"]["image"]) {
386
+ manifest["application"]["image"] = config["devshell"]["image"];
387
+ delete manifest["application"]["devshell"];
388
+ }
389
+ return manifest;
390
+ });
391
+
198
392
  // 如果没有找到 devshell 中没有指定 image 不存在,将默认使用的 lzc-cli/devshell 容器
199
393
  this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
394
+ delete manifest["application"]["devshell"];
395
+
200
396
  const config = manifest["application"];
201
397
  if (config["image"]) {
202
398
  return manifest;
203
399
  }
400
+
401
+ logger.debug("use default lzc-cli/devshell image");
204
402
  manifest["application"][
205
403
  "image"
206
404
  ] = `registry.lazycat.cloud/lzc-cli/devshell:latest`;
207
405
  return manifest;
208
406
  });
209
407
 
408
+ // 添加一个 devshell 的标记在 lpk 中,标记当前 lpk 为一个 debug 版本
409
+ this.lpkBuild.onBeforeDumpLpk(async (options, cwd, destDir) => {
410
+ fs.writeFileSync(path.resolve(destDir, "devshell"), "");
411
+ });
412
+
210
413
  // 在构建生成 lpk 包后,调用 deploy 进行部署
211
414
  let installer = new LpkInstaller();
212
415
  await installer.init();
213
416
  await installer.deploy(this.lpkBuild);
214
417
 
215
418
  await sleep(2000);
216
-
217
- // 通过 rsync 同步源码到应用容器中
218
- // TODO: 改成用 sshfs 的方式
219
- await this.shell(true);
220
419
  }
221
420
 
222
- async shell(build = false) {
421
+ async rsyncShell(runShell) {
223
422
  const k = new Key();
224
423
  const pairs = await k.getKeyPair();
225
424
  const manifest = await this.lpkBuild.getManifest();
226
- const devshell = new DevShell(pairs["private"], manifest["package"]);
227
- if (build || !devshell.isRsyncWatch()) {
425
+ const pkgId = manifest["package"];
426
+
427
+ let isSync = false;
428
+ try {
429
+ const locker = new FileLocker(pkgId);
430
+ locker.lock();
431
+ process.on("exit", () => {
432
+ logger.debug("filelock unlock");
433
+ locker.unlock();
434
+ });
435
+ isSync = true;
436
+ } catch (err) {
437
+ logger.debug("filelock catch");
438
+ logger.debug(err);
439
+ }
440
+
441
+ const devshell = new DevShell(pairs["private"], pkgId, runShell);
442
+ if (isSync) {
228
443
  await devshell.shell();
229
444
  } else {
230
- await devshell.directShell();
445
+ await devshell.connectShell();
231
446
  }
232
447
  logger.debug("exit shell");
233
448
  // TODO: shell 在正常情况下,按 Ctrl-D 就会退出,回到原来的本地的 shell ,但
234
449
  // 现在会一直卡在退出状态后,必须要另外手动的指定 pkill node
235
450
  process.exit(0);
236
451
  }
452
+
453
+ async sshfs() {
454
+ const k = new Key();
455
+ const pairs = await k.getKeyPair();
456
+ const manifest = await this.lpkBuild.getManifest();
457
+ const localDevshell = new LocalDevshell(pairs, sdkSSHAddress());
458
+ const syncP = localDevshell.run(manifest.package, this.cwd);
459
+ await syncP.ready();
460
+ return syncP;
461
+ }
462
+
463
+ async sshfsShell(runShell) {
464
+ const manifest = await this.lpkBuild.getManifest();
465
+ const sdk = new SdkDocker();
466
+ await sdk.interactiveShell(manifest.package, runShell);
467
+
468
+ if (this.sshfsProcess) {
469
+ // already exit
470
+ if (this.sshfsProcess.p.exitCode) {
471
+ return;
472
+ }
473
+
474
+ const questions = [
475
+ {
476
+ name: "stop",
477
+ type: "input",
478
+ default: "n",
479
+ message: "是否停止 sshfs 同步容器(y/n): ",
480
+ },
481
+ ];
482
+ const answers = await inquirer.prompt(questions);
483
+ if (answers.stop.toLowerCase() === "y") {
484
+ this.sshfsProcess.p.kill("SIGTERM");
485
+ } else {
486
+ this.sshfsProcess.p.unref();
487
+ // FIXME: why must process.exit()
488
+ process.exit(0);
489
+ }
490
+ }
491
+ }
237
492
  }
238
493
 
239
494
  class DevShell {
240
- constructor(keyFile, appId) {
495
+ constructor(keyFile, appId, runShell = "sh") {
241
496
  logger.debug("keyFile", keyFile);
242
497
  logger.debug("appid", appId);
243
498
 
244
- this.tempDir = path.join(os.tmpdir(), "lzc-cli-devshell", appId);
245
- if (!fs.existsSync(this.tmpdir)) {
246
- fs.mkdirSync(this.tempDir, { recursive: true });
247
- }
248
499
  this.keyFile = keyFile;
249
500
  this.appId = appId;
501
+ this.runShell = runShell;
250
502
  }
251
503
 
252
- async syncProject(keyFile, appAddr, appId) {
253
- let jump = sdkSSHAddress();
504
+ async syncProject(keyFile, appId) {
505
+ const host = sdkSSHHost();
506
+ const port = sdkSSHPort();
254
507
  // prettier-ignore
255
508
  let rsh = [
256
509
  "ssh",
257
- "-J", jump,
510
+ "-p", `${port}`,
258
511
  "-o", `"StrictHostKeyChecking=no"`,
259
512
  "-o", `"UserKnownHostsFile=/dev/null"`,
260
513
  "-o", `"ConnectionAttempts=3"`,
@@ -269,11 +522,18 @@ class DevShell {
269
522
  process.exit(1);
270
523
  }
271
524
 
525
+ let rsyncDebug = process.env.RSYNCDEBUG ? "-P" : "";
526
+ let storePath = `/var/lib/box-server/lzcapps/${appId}/lzcapp/cache/devshell`;
272
527
  // FIXME: 下方执行命令不确定是否有兼容性问题
273
- execSync(
274
- `rsync --rsh='${rsh}' -czrPR . root@${appAddr}:/lzcapp/cache/${appId} --filter=':- .gitignore' --exclude='node_modules' --exclude='.git' --delete`,
275
- { stdio: ["ignore", "ignore", "inherit"] }
276
- );
528
+ try {
529
+ execSync(
530
+ `rsync ${rsyncDebug} --rsh='${rsh}' --recursive --relative --perms --update . ${host}:${storePath} --filter=':- .gitignore' --exclude='node_modules' --exclude='.git' --delete --ignore-errors --usermap=:nobody --groupmap=*:nobody`,
531
+ { stdio: ["ignore", "inherit", "inherit"] }
532
+ );
533
+ } catch (err) {
534
+ logger.error("rsync 同步失败");
535
+ logger.debug(err);
536
+ }
277
537
  }
278
538
 
279
539
  // fallback fs.watch on not darwin and windows platform
@@ -317,7 +577,7 @@ class DevShell {
317
577
 
318
578
  // 监听非.gitignore文件
319
579
  // TODO: 目前仅仅监听process.cwd()以下的文件
320
- async watchFile(keyFile, appAddr, appId) {
580
+ async watchFile(keyFile, appId) {
321
581
  const ignore = new GitIgnore(process.cwd());
322
582
  await ignore.collect();
323
583
  chokidar
@@ -329,293 +589,98 @@ class DevShell {
329
589
  },
330
590
  ignoreInitial: true,
331
591
  })
332
- .on("all", (event, path) => {
333
- // console.log(event, path);
334
- this.syncProject(keyFile, appAddr, appId);
335
- });
336
- }
337
-
338
- // 保存一个dev shell状态的文件,如果重复执行dev shell
339
- // 会先判断这个临时文件是否存在,如果不存在重新部署,更新流程
340
- // 否则直接ssh连接
341
- async storeShellStatus(isWatch) {
342
- fs.writeFileSync(
343
- path.join(this.tempDir, ".shellStatus"),
344
- JSON.stringify({
345
- isWatch,
346
- })
347
- );
348
- }
349
-
350
- isRsyncWatch() {
351
- try {
352
- let obj = this.readShellStatus();
353
- return obj.isWatch;
354
- } catch {
355
- return false;
356
- }
357
- }
358
-
359
- async readShellStatus() {
360
- return JSON.parse(
361
- fs.readFileSync(path.join(this.tempDir, ".shellStatus"), "utf-8")
362
- );
363
- }
364
-
365
- async directShell() {
366
- const keyFile = this.keyFile;
367
- const appAddr = `sync.${this.appId}.lzcapp`;
368
-
369
- let isWatch;
370
- try {
371
- ({ isWatch } = await this.readShellStatus());
372
-
373
- if (!isWatch) {
374
- await this.syncProject(keyFile, appAddr, this.appId);
375
- // 注册watch函数
376
- await this.watchFile(keyFile, appAddr, this.appId);
377
-
378
- this.storeShellStatus(true);
379
- }
380
-
381
- await this.connectShell(this.appId);
382
- } catch (e) {
383
- return Promise.reject(e);
384
- } finally {
385
- if (!isWatch) {
386
- this.storeShellStatus(false);
387
- }
388
- }
592
+ .on(
593
+ "all",
594
+ debounce(() => {
595
+ this.syncProject(keyFile, appId);
596
+ }),
597
+ 1000
598
+ );
389
599
  }
390
-
391
600
  async shell() {
392
601
  let keyFile = this.keyFile;
393
- const appAddr = `sync.${this.appId}.lzcapp`;
394
-
395
602
  try {
396
603
  // 当进入shell的时候,都同步一次
397
- await this.syncProject(keyFile, appAddr, this.appId);
604
+ await this.syncProject(keyFile, this.appId);
398
605
  // 注册watch函数
399
- await this.watchFile(keyFile, appAddr, this.appId);
400
-
401
- this.storeShellStatus(true);
606
+ await this.watchFile(keyFile, this.appId);
402
607
 
403
- await this.connectShell(this.appId);
608
+ await this.connectShell();
404
609
  } catch (e) {
405
610
  console.log(e);
406
611
  // this.reset();
407
612
  return Promise.reject(e);
408
- } finally {
409
- // 当shell退出后,更新isWatch状态
410
- this.storeShellStatus(false);
411
613
  }
412
614
  }
413
615
 
414
- async connectShell(appId) {
415
- const sdk = new sdkDocker();
416
- const replacedAppId = appId.replaceAll(".", "_").replaceAll("-", "__");
417
- await sdk.interactiveShell(`lzc--${replacedAppId}-app-1`);
616
+ async connectShell() {
617
+ const sdk = new SdkDocker();
618
+ await sdk.interactiveShell(this.appId, this.runShell);
418
619
  }
419
620
  }
420
621
 
421
- // build strategies based on lzc-build.yml devshell field
422
- import { DockerClient, dockerPullLzcAppsImage } from "../sdk.js";
423
- import { DockerfileParser } from "dockerfile-ast";
424
- import glob from "fast-glob";
425
- import ignore from "@balena/dockerignore";
426
- import { createLogUpdate } from "log-update";
427
-
428
- class sdkDocker {
429
- constructor() {}
430
-
431
- // builder 需要在app 的场景下使用, 不过有两个路径要注意
432
- // 一个是 docker build 时的context. 这个一般是运行cli时候的路径
433
- async buildImage(tag, buildDir, contextDir) {
434
- const dockerfilePath = path.join(buildDir, "Dockerfile");
435
- try {
436
- await this.dockerRemoteBuildV2(contextDir, dockerfilePath, tag);
437
- } catch (e) {
438
- throw e;
439
- }
440
- }
441
-
442
- async collectContextFromDockerFile(contextDir, dockerfilePath) {
443
- if (!fs.existsSync(dockerfilePath)) {
444
- throw "未发现 Dockerfile";
445
- }
446
-
447
- let src = [path.relative(contextDir, dockerfilePath)];
448
-
449
- // 通过 COPY 和 ADD 获取所有context中的文件
450
- const ast = DockerfileParser.parse(fs.readFileSync(dockerfilePath, "utf8"));
451
- for (let a of ast.getInstructions()) {
452
- if (["COPY", "ADD"].includes(a.getInstruction())) {
453
- const from = a.getArguments()[0].getValue().replace(/^\//, "");
454
-
455
- const fromFullPath = path.join(contextDir, from);
456
-
457
- if (fs.existsSync(fromFullPath)) {
458
- const stat = fs.statSync(path.join(contextDir, from));
459
- if (stat.isDirectory()) {
460
- let files = await glob(path.join(from, "**"), {
461
- cwd: contextDir,
462
- });
463
- src = src.concat(files);
464
- } else if (stat.isFile()) {
465
- src.push(from);
466
- }
467
- } else {
468
- // try use glob
469
- let files = await glob(from, {
470
- cwd: contextDir,
471
- });
472
- src = src.concat(files);
473
- }
474
- }
475
- }
476
- // filter by dockerignore
477
- const dockerIgnoreFile = path.join(contextDir, ".dockerignore");
478
- //
479
- if (fs.existsSync(dockerIgnoreFile)) {
480
- let ig = ignore();
481
- let data = fs.readFileSync(dockerIgnoreFile, "utf8");
482
- ig.add(data.split("\n"));
483
- src = ig.filter(src);
484
- }
485
- return src;
486
- }
487
-
488
- /**
489
- * v2 版本会收集所有需要的context 文件,将其传入buildImage 方法中
490
- * @param contextDir
491
- * @param dockerfile
492
- * @param tag
493
- */
494
- async dockerRemoteBuildV2(contextDir, dockerfile, tag) {
495
- const src = await this.collectContextFromDockerFile(contextDir, dockerfile);
496
- const host = new URL(sdkEnv.sdkUrl).host;
497
- console.log(host);
498
- const docker = await new DockerClient(host).init();
499
- try {
500
- await dockerPullLzcAppsImage(host);
501
-
502
- return new Promise(async (resolve, reject) => {
503
- let refresher = new StatusRefresher();
504
-
505
- logger.info("开始在设备中构建镜像");
506
- const stream = await docker.buildImage(
507
- {
508
- context: contextDir,
509
- src: src,
510
- },
511
- {
512
- dockerfile: src[0],
513
- t: tag,
514
- }
515
- );
516
- docker.modem.followProgress(
517
- stream,
518
- (err, res) => {
519
- err ? reject(err) : resolve(res);
520
- },
521
- (res) => {
522
- if (res.status) {
523
- if (res.id) {
524
- if (["Downloading", "Extracting"].includes(res.status)) {
525
- refresher.updateStatus({
526
- id: res.id,
527
- content: `${res.status} ${res.progress}`,
528
- });
529
- } else {
530
- refresher.updateStatus({
531
- id: res.id,
532
- content: res.status,
533
- });
534
- }
535
- } else {
536
- process.stdout.write(res.status);
537
- process.stdout.write("\n");
538
- }
539
- }
540
-
541
- if (res.stream) {
542
- if (res.stream.startsWith("Step")) {
543
- refresher.reset();
544
- }
545
- process.stdout.write(res.stream);
546
- }
547
- if (res.error) {
548
- reject(res.error);
549
- }
550
- }
551
- );
552
- });
553
- } catch (err) {
554
- throw err;
555
- }
622
+ // 本地的 devshell 容器
623
+ // - 将当前项目目录通过 volumes 的方式绑定到 local_devshell
624
+ // - local_devshell 中通过 ssh 反代本地的 22 端口到 sdk 中 30222
625
+ // - sdk 中的 sshfs 通过连接 30222 端口进行同步
626
+ // - 结合 local_devshell 容器中的 entrypoint 脚本
627
+ class LocalDevshell {
628
+ constructor(pairs, sdkHost) {
629
+ this.publicKey = pairs["public"];
630
+ this.privateKey = pairs["private"];
631
+ this.sdkHost = sdkHost;
632
+ this.log = logger.getLogger("LocalDevshell");
556
633
  }
557
634
 
558
- async interactiveShell(containerName, shell = "sh") {
559
- const host = new URL(sdkEnv.sdkUrl).hostname;
560
- const docker = await new DockerClient(host).init();
561
- const container = docker.getContainer(containerName);
562
- const exec = await container.exec({
563
- Cmd: [shell],
564
- AttachStderr: true,
565
- AttachStdout: true,
566
- AttachStdin: true,
567
- Tty: true,
635
+ run(pkgId, projectPath) {
636
+ const host = this.sdkHost.split(":")[0];
637
+ const port = this.sdkHost.split(":")[1];
638
+ // prettier-ignore
639
+ const syncP = spawn(`docker`, [
640
+ `run`, `--rm`,
641
+ `--network=host`,
642
+ `-v`, `${projectPath}:/project/${pkgId}`,
643
+ `-v`, `${this.privateKey}:/sdk/privateKey`,
644
+ `-v`, `${this.publicKey}:/sdk/publicKey`,
645
+ `-e`, `PKGID=${pkgId}`,
646
+ `-e`, `SDK_HOST=${host}`,
647
+ `-e`, `SDK_PORT=${port}`,
648
+ `-e`, `HOSTUID=${process.getuid()}`,
649
+ `-e`, `HOSTGID=${process.getgid()}`,
650
+ `registry.lazycat.cloud/lzc-cli/local_devshell:0.0.4`,
651
+ ], { detached: true, stdio: ["ignore", "pipe", "pipe"] });
652
+ syncP.stdout.on("data", (data) => {
653
+ this.log.debug(data.toString());
568
654
  });
569
-
570
- const stream = await exec.start({ stdin: true });
571
-
572
- process.stdin.setRawMode(true);
573
- stream.pipe(process.stdout);
574
- process.stdin.pipe(stream);
575
-
576
- // prepare command
577
- stream.write("cd /lzcapp/cache\n");
578
- stream.write("ls\n");
579
-
580
- await new Promise((resolve) => {
581
- stream.on("end", () => {
582
- logger.info("exit");
583
- resolve();
584
- });
655
+ syncP.stderr.on("data", (data) => {
656
+ this.log.debug(data.toString());
585
657
  });
658
+ return {
659
+ p: syncP,
660
+ ready: async () => {
661
+ return new Promise((_resolve, _reject) => {
662
+ syncP.stdout.on("data", (data) => {
663
+ if (data.toString().search(/sshfs running/g) > -1) {
664
+ this.log.debug("sshfs ready");
665
+ _resolve();
666
+ }
667
+ });
586
668
 
587
- process.stdin.removeAllListeners();
588
- process.stdin.setRawMode(false);
589
- process.stdin.resume();
590
- }
591
- }
592
-
593
- class StatusRefresher {
594
- constructor() {
595
- this.reset();
596
- }
597
- refresh() {
598
- this.logUpdate(this.pulls.map((p) => `${p.id}: ${p.content}`).join("\n"));
599
- }
600
-
601
- reset() {
602
- this.pulls = [];
603
- this.logUpdate = createLogUpdate(process.stdout);
604
- }
669
+ syncP.on("error", (err) => {
670
+ this.log.error(err);
671
+ });
605
672
 
606
- updateStatus({ id, content }) {
607
- let index = this.pulls.findIndex((p) => p.id == id);
608
- if (index > -1) {
609
- this.pulls[index] = {
610
- id: id,
611
- content: content,
612
- };
613
- } else {
614
- this.pulls.push({
615
- id: id,
616
- content: content,
617
- });
618
- }
619
- this.refresh();
673
+ syncP.on("close", (code) => {
674
+ if (code != 0) {
675
+ this.log.debug(`docker exited with code ${code}`);
676
+ _reject();
677
+ } else {
678
+ this.log.debug("sshfs close");
679
+ _resolve();
680
+ }
681
+ });
682
+ });
683
+ },
684
+ };
620
685
  }
621
686
  }