@abtnode/core 1.16.31 → 1.16.32-beta-04a5da00

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/lib/api/node.js CHANGED
@@ -54,6 +54,7 @@ class NodeAPI {
54
54
 
55
55
  try {
56
56
  const updateResult = await this.state.updateNodeInfo(entity);
57
+ this.cache.del('info');
57
58
  return updateResult;
58
59
  } catch (error) {
59
60
  logger.error('update node info failed', { error, entity });
@@ -90,9 +90,10 @@ class BlockletDownloader extends EventEmitter {
90
90
  * set: (key: string, value: any) => Promise
91
91
  * get: (key: string) => Promise<any>
92
92
  * },
93
+ * getNodeInfo: () => Promise<{ enableDocker: boolean }>
93
94
  * }}
94
95
  */
95
- constructor({ installDir, downloadDir, cache, logger = defaultLogger }) {
96
+ constructor({ installDir, downloadDir, cache, logger = defaultLogger, getNodeInfo }) {
96
97
  super();
97
98
 
98
99
  this.installDir = installDir;
@@ -103,6 +104,7 @@ class BlockletDownloader extends EventEmitter {
103
104
  installDir,
104
105
  downloadDir,
105
106
  cache,
107
+ getNodeInfo,
106
108
  });
107
109
  }
108
110
 
@@ -23,15 +23,17 @@ class BundleDownloader extends EventEmitter {
23
23
  * set: (key: string, value: any) => Promise
24
24
  * get: (key: string) => Promise<any>
25
25
  * }
26
+ * getNodeInfo: () => Promise<{ enableDocker: boolean }>
26
27
  * }}
27
28
  */
28
- constructor({ installDir, downloadDir, cache, logger = defaultLogger }) {
29
+ constructor({ installDir, downloadDir, cache, logger = defaultLogger, getNodeInfo }) {
29
30
  super();
30
31
 
31
32
  this.installDir = installDir;
32
33
  this.downloadDir = downloadDir;
33
34
  this.cache = cache;
34
35
  this.logger = logger;
36
+ this.getNodeInfo = getNodeInfo;
35
37
 
36
38
  /**
37
39
  * { did: Map({ <childDid>: <downloadFile.cancelCtrl> }) }
@@ -51,7 +53,7 @@ class BundleDownloader extends EventEmitter {
51
53
  * @param {*} meta
52
54
  * @param {*} rootDid
53
55
  * @param {*} url
54
- * @param {{}} [context={}]
56
+ * @param {{}} [context={ nodeInfo: { enableDocker: boolean } }]
55
57
  * @return {*}
56
58
  * @memberof BlockletManager
57
59
  */
@@ -67,6 +69,7 @@ class BundleDownloader extends EventEmitter {
67
69
  }
68
70
 
69
71
  const onProgress = context.onProgress || (() => {});
72
+ const nodeInfo = await this.getNodeInfo();
70
73
 
71
74
  try {
72
75
  await lock.acquire();
@@ -104,6 +107,7 @@ class BundleDownloader extends EventEmitter {
104
107
  // resolve tarball and mv tarball to cache after resolved
105
108
  await resolveDownload(tarballPath, this.installDir, {
106
109
  removeTarFile: false,
110
+ nodeInfo,
107
111
  onProgress: ({ name: _name }) => {
108
112
  onProgress({ status: 'extracting', name: _name });
109
113
  },
@@ -19,9 +19,14 @@ const asyncFs = fs.promises;
19
19
  * @param {{
20
20
  * removeTarFile: boolean;
21
21
  * logger;
22
+ * nodeInfo: { enableDocker: boolean };
22
23
  * }} option
23
24
  */
24
- const resolveDownload = async (tarFile, dist, { removeTarFile = true, logger = defaultLogger, onProgress } = {}) => {
25
+ const resolveDownload = async (
26
+ tarFile,
27
+ dist,
28
+ { removeTarFile = true, logger = defaultLogger, onProgress, nodeInfo } = {}
29
+ ) => {
25
30
  const downloadDir = path.join(path.dirname(tarFile), path.basename(tarFile, path.extname(tarFile)));
26
31
  const tmp = `${downloadDir}-tmp`;
27
32
  try {
@@ -78,7 +83,7 @@ const resolveDownload = async (tarFile, dist, { removeTarFile = true, logger = d
78
83
 
79
84
  await fs.move(downloadDir, installDir, { overwrite: true });
80
85
 
81
- await installExternalDependencies({ appDir: installDir, forceInstall: true });
86
+ await installExternalDependencies({ appDir: installDir, forceInstall: true, enableDocker: nodeInfo?.enableDocker });
82
87
  } catch (error) {
83
88
  fs.removeSync(downloadDir);
84
89
  fs.removeSync(tmp);
@@ -91,7 +96,7 @@ const resolveDownload = async (tarFile, dist, { removeTarFile = true, logger = d
91
96
  const resolveDiffDownload = async (
92
97
  tarFile,
93
98
  dist,
94
- { meta: oldMeta, deleteSet, cwd = '/', logger = defaultLogger } = {}
99
+ { meta: oldMeta, deleteSet, cwd = '/', logger = defaultLogger, nodeInfo } = {}
95
100
  ) => {
96
101
  // eslint-disable-next-line no-param-reassign
97
102
  tarFile = path.join(cwd, tarFile);
@@ -145,7 +150,7 @@ const resolveDiffDownload = async (
145
150
  logger.info('Move downloadDir to installDir', { downloadDir, bundleDir });
146
151
  await fs.move(downloadDir, bundleDir, { overwrite: true });
147
152
 
148
- await installExternalDependencies({ appDir: bundleDir, forceInstall: true });
153
+ await installExternalDependencies({ appDir: bundleDir, forceInstall: true, enableDocker: nodeInfo?.enableDocker });
149
154
 
150
155
  return { meta, installDir: bundleDir };
151
156
  } catch (error) {
@@ -312,6 +312,7 @@ class DiskBlockletManager extends BaseBlockletManager {
312
312
  installDir: this.installDir,
313
313
  downloadDir: this.dataDirs.tmp,
314
314
  cache: states.cache,
315
+ getNodeInfo: () => states.node.read(),
315
316
  });
316
317
 
317
318
  this._rollbackCache = new RollbackCache({ dir: this.dataDirs.tmp });
@@ -66,10 +66,12 @@ const installComponentFromUpload = async ({
66
66
  throw new Error('Blocklet version changed when diff deploying');
67
67
  }
68
68
 
69
- meta = (await resolveDiffDownload(tarFile, manager.installDir, { deleteSet, meta: oldChild.meta })).meta;
69
+ const nodeInfo = await states.node.read();
70
+ meta = (await resolveDiffDownload(tarFile, manager.installDir, { deleteSet, meta: oldChild.meta, nodeInfo })).meta;
70
71
  } else {
71
72
  // full deploy
72
- meta = (await resolveDownload(tarFile, manager.installDir)).meta;
73
+ const nodeInfo = await states.node.read();
74
+ meta = (await resolveDownload(tarFile, manager.installDir, { nodeInfo })).meta;
73
75
  }
74
76
 
75
77
  if (meta.did === rootDid) {
@@ -17,6 +17,7 @@ const {
17
17
  const BaseState = require('./base');
18
18
  const { validateOwner } = require('../util');
19
19
  const { get: getDefaultConfigs } = require('../util/default-node-config');
20
+ const checkDockerInstalled = require('../util/docker/check-docker-installed');
20
21
 
21
22
  /**
22
23
  * @extends BaseState<import('@abtnode/models').ServerState>
@@ -70,7 +71,7 @@ class NodeState extends BaseState {
70
71
  if (dek) {
71
72
  doc.sk = security.decrypt(doc.sk, doc.did, dek);
72
73
  }
73
-
74
+ doc.isDockerInstalled = await checkDockerInstalled();
74
75
  return doc;
75
76
  }
76
77
 
@@ -138,6 +139,7 @@ class NodeState extends BaseState {
138
139
  if (dek) {
139
140
  doc.sk = security.decrypt(doc.sk, doc.did, dek);
140
141
  }
142
+ doc.isDockerInstalled = await checkDockerInstalled();
141
143
 
142
144
  if (doc.runtimeConfig && !doc.runtimeConfig.proxyMaxMemoryLimit) {
143
145
  doc.runtimeConfig.proxyMaxMemoryLimit = PROXY_MAX_MEM_LIMIT_IN_MB;
@@ -104,6 +104,8 @@ const isRequirementsSatisfied = require('./requirement');
104
104
  const { getDidDomainForBlocklet } = require('./get-domain-for-blocklet');
105
105
  const { expandBundle, findInterfacePortByName, prettyURL, templateReplace, getServerDidDomain } = require('./index');
106
106
  const { installExternalDependencies } = require('./install-external-dependencies');
107
+ const parseDockerOptionsFromPm2 = require('./docker/parse-docker-options-from-pm2');
108
+ const dockerRemoveByName = require('./docker/docker-remove-by-name');
107
109
 
108
110
  /**
109
111
  * get blocklet engine info, default is node
@@ -586,7 +588,7 @@ const startBlockletProcess = async (
586
588
  const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors);
587
589
  const startedAt = Date.now();
588
590
 
589
- await installExternalDependencies({ appDir: env?.BLOCKLET_APP_DIR });
591
+ await installExternalDependencies({ appDir: env?.BLOCKLET_APP_DIR, enableDocker: nodeInfo?.enableDocker });
590
592
 
591
593
  // run hook
592
594
  await preFlight(b, { env: { ...env } });
@@ -676,7 +678,8 @@ const startBlockletProcess = async (
676
678
  serverSk: nodeEnvironments.ABT_NODE_SK,
677
679
  });
678
680
 
679
- await pm2.startAsync(options);
681
+ const nextOptions = await parseDockerOptionsFromPm2(options, nodeInfo);
682
+ await pm2.startAsync(nextOptions);
680
683
 
681
684
  // eslint-disable-next-line no-use-before-define
682
685
  const status = await getProcessState(processId);
@@ -730,7 +733,6 @@ const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProces
730
733
  if (!hasStartEngine(b.meta)) {
731
734
  return;
732
735
  }
733
-
734
736
  await preDelete(b, { ancestors });
735
737
  // eslint-disable-next-line no-use-before-define
736
738
  await deleteProcess(b.env.processId);
@@ -785,16 +787,18 @@ const getProcessState = async (processId) => {
785
787
  const getProcessInfo = (processId, { throwOnNotExist = true } = {}) =>
786
788
  getPm2ProcessInfo(processId, { printError: logger.error.bind(logger), throwOnNotExist });
787
789
 
788
- const deleteProcess = (processId) =>
789
- new Promise((resolve, reject) => {
790
- pm2.delete(processId, (err) => {
790
+ const deleteProcess = (processId) => {
791
+ return new Promise((resolve, reject) => {
792
+ pm2.delete(processId, async (err) => {
791
793
  if (isUsefulError(err)) {
792
794
  logger.error('blocklet process delete failed', { processId, error: err });
793
795
  return reject(err);
794
796
  }
797
+ await dockerRemoveByName(processId);
795
798
  return resolve(processId);
796
799
  });
797
800
  });
801
+ };
798
802
 
799
803
  const reloadProcess = (processId) =>
800
804
  new Promise((resolve, reject) => {
@@ -0,0 +1,17 @@
1
+ const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
2
+ const promiseSpawn = require('../promise-spawn');
3
+ const promiseOnce = require('../promise-once');
4
+
5
+ async function checkDockerInstalled() {
6
+ try {
7
+ await promiseSpawn('docker --version');
8
+ const output = await promiseSpawn('docker ps -a');
9
+ logger.info(`Docker is installed: ${output.trim()}`);
10
+ return true;
11
+ } catch (error) {
12
+ logger.error('Docker is not installed or not available in the PATH or not Permission: ', error);
13
+ return false;
14
+ }
15
+ }
16
+
17
+ module.exports = promiseOnce(checkDockerInstalled);
@@ -0,0 +1,82 @@
1
+ const fs = require('fs/promises');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
5
+
6
+ const DOCKER_IMAGE_NAME = 'blocklet-node-app';
7
+
8
+ let isCreated = false;
9
+
10
+ async function createDockerImage() {
11
+ if (isCreated) {
12
+ return;
13
+ }
14
+ isCreated = true;
15
+
16
+ const dir = process.env.ABT_NODE_DATA_DIR;
17
+ if (!dir) {
18
+ logger.error('ABT_NODE_DATA_DIR is not set');
19
+ return;
20
+ }
21
+ await fs.mkdir(dir, { recursive: true });
22
+ // 生成 Dockerfile
23
+ const uid = process.getuid();
24
+ const gid = process.getgid();
25
+ const dockerfilePath = path.join(dir, 'Dockerfile');
26
+ const dockerfilePathDone = path.join(dir, `Dockerfile.done-${uid}-${gid}`);
27
+ const [accessDone] = await Promise.allSettled([fs.access(`${dockerfilePathDone}`)]);
28
+ if (accessDone.status === 'fulfilled') {
29
+ return;
30
+ }
31
+ const dockerfileContent = `
32
+ FROM node:22-alpine
33
+
34
+
35
+ # RUN apk add --no-cache shadow
36
+ RUN apk add --no-cache socat
37
+
38
+ RUN node -v
39
+ RUN npm i -g pnpm
40
+ RUN npm i -g nr
41
+
42
+ # 使用主机用户的 home 目录
43
+ WORKDIR /blocklet
44
+
45
+ RUN mkdir -p /blocklet/app
46
+ RUN mkdir -p /blocklet/data
47
+ RUN mkdir -p /blocklet/logs
48
+ RUN mkdir -p /blocklet/cache
49
+ RUN mkdir -p /data/bin/meilisearch
50
+ RUN chmod -R 775 /blocklet
51
+ RUN chown -R ${uid}:${gid} /blocklet
52
+ RUN chown -R ${uid}:${gid} /data/bin/meilisearch
53
+ # USER ${uid}:${gid}
54
+ `;
55
+
56
+ try {
57
+ await fs.access(dockerfilePath);
58
+ await fs.unlink(dockerfilePath);
59
+ } catch (_) {
60
+ // File doesn't exist, no need to remove
61
+ }
62
+
63
+ await fs.writeFile(dockerfilePath, dockerfileContent);
64
+ try {
65
+ execSync(`docker build -f ${path.join(dir, 'Dockerfile')} --no-cache -t ${DOCKER_IMAGE_NAME} .`, {
66
+ stdio: 'inherit',
67
+ encoding: 'utf-8',
68
+ });
69
+ } catch (error) {
70
+ // 如果 docker 会报错: ERROR: failed to solve: failed to read dockerfile: open /var/snap/docker/common/var-lib-docker/tmp/buildkit-mount875872699/Dockerfile: no such file or directory
71
+ // 表示 docker 没有权限访问 ABT_NODE_DATA_DIR 目录, 后续的一系列操作也会失败
72
+ logger.error('Failed to build Docker image. Error:', error.message);
73
+ throw error;
74
+ }
75
+ await fs.rename(dockerfilePath, dockerfilePathDone);
76
+ logger.info('Success build docker image');
77
+ }
78
+
79
+ module.exports = {
80
+ createDockerImage,
81
+ DOCKER_IMAGE_NAME,
82
+ };
@@ -0,0 +1,71 @@
1
+ const pm2 = require('@abtnode/util/lib/async-pm2');
2
+ const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
3
+
4
+ const checkDockerInstalled = require('./check-docker-installed');
5
+ const parseDockerName = require('./parse-docker-name');
6
+ const promiseSpawn = require('../promise-spawn');
7
+ const promiseDebounce = require('../promise-debounce');
8
+
9
+ async function dockerContainerPrune() {
10
+ const isDockerInstalled = await checkDockerInstalled();
11
+ if (!isDockerInstalled) {
12
+ return;
13
+ }
14
+
15
+ try {
16
+ const containerList = await promiseSpawn('docker ps -a --format {{.Names}}');
17
+ const containerNames = containerList.split('\n').filter((v) => {
18
+ const name = v.trim().replace(/^"|"$/g, '');
19
+ return name.startsWith('blocklet-');
20
+ });
21
+
22
+ // 获取 PM2 进程列表
23
+ const pm2List = await new Promise((resolve, reject) => {
24
+ pm2.list((err, list) => {
25
+ if (err) reject(err);
26
+ else resolve(list.filter((process) => process.pm2_env.status !== 'stopped'));
27
+ });
28
+ });
29
+
30
+ const pm2Names = new Set(pm2List.map((process) => parseDockerName(process.name)));
31
+ const containersToStop = containerNames.filter((containerName) => !pm2Names.has(containerName));
32
+
33
+ for (const containerName of containersToStop) {
34
+ try {
35
+ // eslint-disable-next-line no-await-in-loop
36
+ await promiseSpawn(`docker rm -f ${containerName}`);
37
+ } catch (_) {
38
+ //
39
+ }
40
+ }
41
+ } catch (e) {
42
+ logger.error('dockerContainerPrune failed', { error: e });
43
+ }
44
+ }
45
+
46
+ const debounceDockerContainerPrune = promiseDebounce(dockerContainerPrune, 4000);
47
+
48
+ // 添加 PM2 事件监听
49
+ pm2.launchBus((err, bus) => {
50
+ if (err) {
51
+ logger.error('Failed to launch PM2 bus', { error: err });
52
+ return;
53
+ }
54
+
55
+ // 监听进程在线状态变更
56
+ bus.on('process:event', (packet) => {
57
+ if (['stop', 'exit', 'restart'].includes(packet.event)) {
58
+ debounceDockerContainerPrune();
59
+ }
60
+ });
61
+
62
+ // 监听新进程启动
63
+ bus.on('process:spawn', () => {
64
+ debounceDockerContainerPrune();
65
+ });
66
+
67
+ logger.info('PM2 listeners set up successfully');
68
+ });
69
+
70
+ // 初始执行一次清理
71
+ debounceDockerContainerPrune();
@@ -0,0 +1,20 @@
1
+ const promiseSpawn = require('../promise-spawn');
2
+ const checkDockerInstalled = require('./check-docker-installed');
3
+ const parseDockerName = require('./parse-docker-name');
4
+
5
+ async function dockerRemoveByName(name) {
6
+ const isDockerInstalled = await checkDockerInstalled();
7
+ if (!isDockerInstalled) {
8
+ return;
9
+ }
10
+ const containerName = parseDockerName(name);
11
+ // const checkContainer = await promiseSpawn(`docker ps -q -f name=${containerName}`);
12
+ // if (checkContainer) { // }
13
+ try {
14
+ await promiseSpawn(`docker rm -f ${containerName} > /dev/null 2>&1 || true`);
15
+ } catch (_) {
16
+ //
17
+ }
18
+ }
19
+
20
+ module.exports = dockerRemoveByName;
@@ -0,0 +1,10 @@
1
+ function parseDockerName(input) {
2
+ // docker 容器名称不能包含特殊字符
3
+ const name = input
4
+ .replace(/[^a-zA-Z0-9_-]/g, '-')
5
+ .replace(/^"|"$/g, '')
6
+ .replace(/^blocklet-/, '');
7
+ return `blocklet-${name}`;
8
+ }
9
+
10
+ module.exports = parseDockerName;
@@ -0,0 +1,138 @@
1
+ require('./docker-container-prune');
2
+
3
+ const { stringify: stringifyEnvFile } = require('envfile');
4
+ const path = require('path');
5
+ const fs = require('fs/promises');
6
+ const os = require('os');
7
+ const { existsSync } = require('fs-extra');
8
+ const which = require('which');
9
+ const { NODE_MODES } = require('@abtnode/constant');
10
+ const parseDockerName = require('./parse-docker-name');
11
+ const { DOCKER_IMAGE_NAME, createDockerImage } = require('./create-docker-image');
12
+
13
+ const meiliSearchPathAlt = '/data/bin/meilisearch';
14
+
15
+ function findExecutable(executable) {
16
+ try {
17
+ const fullPath = which.sync(executable);
18
+ return fullPath;
19
+ } catch (error) {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ async function mkdirpWithPermissions(dir, mode = 0o755) {
25
+ try {
26
+ await fs.mkdir(dir, { recursive: true, mode });
27
+ } catch (error) {
28
+ if (error.code !== 'EEXIST') {
29
+ throw error;
30
+ }
31
+ }
32
+ }
33
+
34
+ async function parseDockerOptionsFromPm2(input, nodeInfo) {
35
+ // serverless 模式下,不支持跳过 docker
36
+ if (input.env.SKIP_DOCKER && nodeInfo.mode !== NODE_MODES.SERVERLESS) {
37
+ return input;
38
+ }
39
+
40
+ // 环境变量或配置项启用了 docker, 就尝试使用 docker
41
+ const useDocker = input.env.USE_DOCKER || nodeInfo.enableDocker;
42
+
43
+ // 如果 node 没有安装 docker,并且没有指定使用 docker,则不使用 docker
44
+ if (!useDocker || !nodeInfo.isDockerInstalled) {
45
+ return input;
46
+ }
47
+ await createDockerImage();
48
+ const options = { ...input };
49
+ const name = parseDockerName(options.name);
50
+
51
+ // 要确保 BLOCKLET_APP_DIR 等环境变量在 docker 容器内正常使用
52
+ options.env.BLOCKLET_DOCKER_CPUS = options.env.BLOCKLET_DOCKER_CPUS || '2';
53
+ options.env.BLOCKLET_DOCKER_MEMORY = options.env.BLOCKLET_DOCKER_MEMORY || '2g';
54
+ options.env.BLOCKLET_DOCKER_DISK_SIZE = options.env.BLOCKLET_DOCKER_DISK_SIZE || '0g';
55
+ options.env.BLOCKLET_DOCKER_NAME = name;
56
+ options.env.INTERNAL_HOST = 'host.docker.internal';
57
+
58
+ const dockerEnv = { ...options.env };
59
+ const baseDir = '/blocklet';
60
+ dockerEnv.BLOCKLET_APP_DIR = path.join(baseDir, 'app');
61
+ dockerEnv.BLOCKLET_DATA_DIR = path.join(baseDir, options.env.BLOCKLET_REAL_NAME);
62
+ dockerEnv.BLOCKLET_LOG_DIR = path.join(baseDir, 'logs');
63
+ dockerEnv.BLOCKLET_CACHE_DIR = path.join(baseDir, 'cache');
64
+ dockerEnv.BLOCKLET_DOCKER_NAME = name;
65
+ dockerEnv.BLOCKLET_HOST = 'host.docker.internal';
66
+ const envVars = stringifyEnvFile(dockerEnv);
67
+
68
+ const dockerEnvFile = path.join(options.env.BLOCKLET_DATA_DIR, 'docker-env');
69
+ await fs.writeFile(dockerEnvFile, envVars);
70
+ const runScript = /(npm|yarn|pnpm|bun)/g.test(options.script) ? options.script : `node ${options.script}`;
71
+ const networkHost = os.platform() === 'linux' ? '--network host' : '';
72
+ const port = os.platform() === 'linux' ? '' : `-p ${options.env.BLOCKLET_PORT}:${options.env.BLOCKLET_PORT}`;
73
+ const diskSize =
74
+ options.env.BLOCKLET_DOCKER_DISK_SIZE !== '0g' ? `--storage-opt size=${options.env.BLOCKLET_DOCKER_DISK_SIZE}` : '';
75
+ const socatCommand =
76
+ os.platform() === 'linux'
77
+ ? ''
78
+ : [options.env.ABT_NODE_SERVICE_PORT, options.env.ABT_NODE_PORT]
79
+ .concat((process.env.BLOCKLET_DOCKET_SHARE_PORTS || '').split(','))
80
+ .map((v) => `socat TCP-LISTEN:${v},reuseaddr,fork TCP:host.docker.internal:${v} & `)
81
+ .join(' ');
82
+
83
+ options.args = [];
84
+
85
+ const bindMounts = [
86
+ { source: options.env.BLOCKLET_APP_DIR, target: dockerEnv.BLOCKLET_APP_DIR },
87
+ { source: options.env.BLOCKLET_DATA_DIR, target: dockerEnv.BLOCKLET_DATA_DIR },
88
+ { source: options.env.BLOCKLET_LOG_DIR, target: dockerEnv.BLOCKLET_LOG_DIR },
89
+ { source: options.env.BLOCKLET_CACHE_DIR, target: dockerEnv.BLOCKLET_CACHE_DIR },
90
+ {
91
+ source: path.join(options.env.BLOCKLET_APP_DIR, 'docker_node_modules'),
92
+ target: `${dockerEnv.BLOCKLET_APP_DIR}/node_modules`,
93
+ },
94
+ ];
95
+ let mount = '';
96
+ for (const bindMount of bindMounts) {
97
+ if (!existsSync(bindMount.source)) {
98
+ // eslint-disable-next-line no-await-in-loop
99
+ await mkdirpWithPermissions(bindMount.source);
100
+ }
101
+ // 使用 --mount 需要系统有 mnt 磁盘挂载配置
102
+ // mount += ` --mount type=bind,source=${bindMount.source},target=${bindMount.target},bind-propagation=rshared`;
103
+ mount += ` -v ${bindMount.source}:${bindMount.target}:rw `;
104
+ }
105
+ if (!existsSync(path.join(options.env.BLOCKLET_APP_DIR, 'node_modules'))) {
106
+ await mkdirpWithPermissions(path.join(options.env.BLOCKLET_APP_DIR, 'node_modules'));
107
+ }
108
+ if (existsSync(meiliSearchPathAlt)) {
109
+ mount += ` -v ${meiliSearchPathAlt}:${meiliSearchPathAlt}:rw `;
110
+ } else {
111
+ const meiliSearchPath = findExecutable('meilisearch');
112
+ if (meiliSearchPath) {
113
+ mount += ` -v ${meiliSearchPath}:${meiliSearchPathAlt}:rw `;
114
+ }
115
+ }
116
+
117
+ options.script = `docker rm -f ${name} > /dev/null 2>&1 || true && docker run -u $(id -u):$(id -g) --name ${name} \
118
+ ${port} \
119
+ ${mount} \
120
+ -v ${path.join(options.env.BLOCKLET_APP_DIR, 'node_modules')} \
121
+ ${diskSize} \
122
+ --read-only \
123
+ --cpus="${dockerEnv.BLOCKLET_DOCKER_CPUS}" \
124
+ --memory="${dockerEnv.BLOCKLET_DOCKER_MEMORY}" \
125
+ --env-file ${dockerEnvFile} \
126
+ --read-only --cap-drop=ALL --security-opt no-new-privileges \
127
+ ${networkHost} \
128
+ ${DOCKER_IMAGE_NAME} \
129
+ sh -c \
130
+ '${socatCommand} \
131
+ cd ${dockerEnv.BLOCKLET_APP_DIR} && npm install && ${runScript}'`;
132
+
133
+ options.env = {};
134
+
135
+ return options;
136
+ }
137
+
138
+ module.exports = parseDockerOptionsFromPm2;
@@ -0,0 +1,10 @@
1
+ const promiseSpawn = require('../spawn-promise');
2
+ const promiseOnce = require('../promise-once');
3
+
4
+ async function shardNetwork() {
5
+ await promiseSpawn(
6
+ 'docker network ls | grep -q blocklet-shared-network || docker network create blocklet-shared-network'
7
+ );
8
+ }
9
+
10
+ module.exports = promiseOnce(shardNetwork);
@@ -6,13 +6,17 @@ function isDependencyInstalled(appDir, dependency) {
6
6
  return fs.existsSync(path.resolve(appDir, 'node_modules', dependency));
7
7
  }
8
8
 
9
- function installExternalDependencies({ appDir, forceInstall = false } = {}) {
9
+ function installExternalDependencies({ appDir, forceInstall = false, enableDocker } = {}) {
10
10
  if (!appDir) {
11
11
  throw new Error('appDir is required');
12
12
  }
13
13
  if (!fs.existsSync(appDir)) {
14
14
  throw new Error(`not a correct appDir directory: ${appDir}`);
15
15
  }
16
+ if (enableDocker) {
17
+ // 暂时注释, 需要等到 prestart 同步迁移到 docker 之后再return
18
+ // return;
19
+ }
16
20
 
17
21
  // 读取 BLOCKLET_APP_DIR 的 package.json
18
22
  const packageJsonPath = path.resolve(appDir, 'package.json');
@@ -0,0 +1,29 @@
1
+ // debounce bing q promise 过程中不会重复执行
2
+ function promiseDebounce(fn, delay = 1000) {
3
+ let timeoutId = null;
4
+ let isRunning = false;
5
+
6
+ return (...args) => {
7
+ return new Promise((resolve, reject) => {
8
+ if (timeoutId) {
9
+ clearTimeout(timeoutId);
10
+ }
11
+
12
+ timeoutId = setTimeout(async () => {
13
+ if (isRunning) return;
14
+
15
+ isRunning = true;
16
+ try {
17
+ const result = await Promise.resolve(fn(...args));
18
+ resolve(result);
19
+ } catch (error) {
20
+ reject(error);
21
+ } finally {
22
+ isRunning = false;
23
+ }
24
+ }, delay);
25
+ });
26
+ };
27
+ }
28
+
29
+ module.exports = promiseDebounce;
@@ -0,0 +1,14 @@
1
+ function promiseOnce(fn) {
2
+ let executed = false;
3
+ let result;
4
+
5
+ return async (...args) => {
6
+ if (!executed) {
7
+ executed = true;
8
+ result = await fn(...args);
9
+ }
10
+ return result;
11
+ };
12
+ }
13
+
14
+ module.exports = promiseOnce;
@@ -0,0 +1,49 @@
1
+ const { spawn } = require('child_process');
2
+
3
+ /**
4
+ * 将命令字符串拆分为命令和参数数组
5
+ * @param {string} commandString - 完整的命令字符串
6
+ * @returns {{ command: string, args: string[] }} - 拆分后的命令和参数
7
+ */
8
+ function parseCommand(commandString) {
9
+ const [command, ...args] = commandString.split(' ');
10
+ return { command, args };
11
+ }
12
+
13
+ /**
14
+ * 通用的命令执行函数,支持字符串输入
15
+ * @param {string} commandString - 完整的命令字符串
16
+ * @param {object} [options={}] - 可选的 spawn 配置项
17
+ * @returns {Promise<string>} - 执行结果
18
+ */
19
+ function promiseSpawn(commandString, options = {}) {
20
+ return new Promise((resolve, reject) => {
21
+ const { command, args } = parseCommand(commandString);
22
+ const childProcess = spawn(command, args, options);
23
+
24
+ let stdoutData = '';
25
+ let stderrData = '';
26
+
27
+ childProcess.stdout.on('data', (data) => {
28
+ stdoutData += data;
29
+ });
30
+
31
+ childProcess.stderr.on('data', (data) => {
32
+ stderrData += data;
33
+ });
34
+
35
+ childProcess.on('close', (code) => {
36
+ if (code === 0) {
37
+ resolve(stdoutData);
38
+ } else {
39
+ reject(new Error(`Command failed with code ${code}: ${stderrData}`));
40
+ }
41
+ });
42
+
43
+ childProcess.on('error', (err) => {
44
+ reject(err);
45
+ });
46
+ });
47
+ }
48
+
49
+ module.exports = promiseSpawn;
@@ -38,6 +38,8 @@ const nodeInfoSchema = Joi.object({
38
38
  en: { 'string.uriCustomScheme': 'NFT Domain must be a valid URL' },
39
39
  }),
40
40
  autoUpgrade: Joi.boolean(),
41
+ enableDocker: Joi.boolean().optional().default(false),
42
+ isDockerInstalled: Joi.boolean().optional().default(false),
41
43
  enableWelcomePage: Joi.boolean(),
42
44
  enableFileSystemIsolation: Joi.boolean(),
43
45
  diskAlertThreshold: Joi.number()
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.31",
6
+ "version": "1.16.32-beta-04a5da00",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,19 +19,19 @@
19
19
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
20
  "license": "Apache-2.0",
21
21
  "dependencies": {
22
- "@abtnode/analytics": "1.16.31",
23
- "@abtnode/auth": "1.16.31",
24
- "@abtnode/certificate-manager": "1.16.31",
25
- "@abtnode/constant": "1.16.31",
26
- "@abtnode/cron": "1.16.31",
27
- "@abtnode/logger": "1.16.31",
28
- "@abtnode/models": "1.16.31",
29
- "@abtnode/queue": "1.16.31",
30
- "@abtnode/rbac": "1.16.31",
31
- "@abtnode/router-provider": "1.16.31",
32
- "@abtnode/static-server": "1.16.31",
33
- "@abtnode/timemachine": "1.16.31",
34
- "@abtnode/util": "1.16.31",
22
+ "@abtnode/analytics": "1.16.32-beta-04a5da00",
23
+ "@abtnode/auth": "1.16.32-beta-04a5da00",
24
+ "@abtnode/certificate-manager": "1.16.32-beta-04a5da00",
25
+ "@abtnode/constant": "1.16.32-beta-04a5da00",
26
+ "@abtnode/cron": "1.16.32-beta-04a5da00",
27
+ "@abtnode/logger": "1.16.32-beta-04a5da00",
28
+ "@abtnode/models": "1.16.32-beta-04a5da00",
29
+ "@abtnode/queue": "1.16.32-beta-04a5da00",
30
+ "@abtnode/rbac": "1.16.32-beta-04a5da00",
31
+ "@abtnode/router-provider": "1.16.32-beta-04a5da00",
32
+ "@abtnode/static-server": "1.16.32-beta-04a5da00",
33
+ "@abtnode/timemachine": "1.16.32-beta-04a5da00",
34
+ "@abtnode/util": "1.16.32-beta-04a5da00",
35
35
  "@arcblock/did": "1.18.135",
36
36
  "@arcblock/did-auth": "1.18.135",
37
37
  "@arcblock/did-ext": "^1.18.135",
@@ -42,13 +42,13 @@
42
42
  "@arcblock/pm2-events": "^0.0.5",
43
43
  "@arcblock/validator": "^1.18.135",
44
44
  "@arcblock/vc": "1.18.135",
45
- "@blocklet/constant": "1.16.31",
46
- "@blocklet/env": "1.16.31",
47
- "@blocklet/meta": "1.16.31",
48
- "@blocklet/resolver": "1.16.31",
49
- "@blocklet/sdk": "1.16.31",
50
- "@blocklet/store": "1.16.31",
51
- "@did-space/client": "^0.5.31",
45
+ "@blocklet/constant": "1.16.32-beta-04a5da00",
46
+ "@blocklet/env": "1.16.32-beta-04a5da00",
47
+ "@blocklet/meta": "1.16.32-beta-04a5da00",
48
+ "@blocklet/resolver": "1.16.32-beta-04a5da00",
49
+ "@blocklet/sdk": "1.16.32-beta-04a5da00",
50
+ "@blocklet/store": "1.16.32-beta-04a5da00",
51
+ "@did-space/client": "^0.5.32",
52
52
  "@fidm/x509": "^1.2.1",
53
53
  "@ocap/mcrypto": "1.18.135",
54
54
  "@ocap/util": "1.18.135",
@@ -61,6 +61,7 @@
61
61
  "cross-spawn": "^7.0.3",
62
62
  "deep-diff": "^1.0.2",
63
63
  "detect-port": "^1.5.1",
64
+ "envfile": "^7.1.0",
64
65
  "escape-string-regexp": "^4.0.0",
65
66
  "fast-glob": "^3.3.2",
66
67
  "filesize": "^10.1.1",
@@ -95,6 +96,7 @@
95
96
  "ufo": "^1.5.3",
96
97
  "uuid": "^9.0.1",
97
98
  "valid-url": "^1.0.9",
99
+ "which": "^2.0.2",
98
100
  "xbytes": "^1.8.0"
99
101
  },
100
102
  "devDependencies": {
@@ -103,5 +105,5 @@
103
105
  "jest": "^29.7.0",
104
106
  "unzipper": "^0.10.11"
105
107
  },
106
- "gitHead": "03edde3e617b126c1873caa67b1d828ee1419a70"
108
+ "gitHead": "f609e3593366aace895c936b00f9623002e29b32"
107
109
  }