@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 +1 -0
- package/lib/blocklet/downloader/blocklet-downloader.js +3 -1
- package/lib/blocklet/downloader/bundle-downloader.js +6 -2
- package/lib/blocklet/downloader/resolve-download.js +9 -4
- package/lib/blocklet/manager/disk.js +1 -0
- package/lib/blocklet/manager/helper/install-component-from-upload.js +4 -2
- package/lib/states/node.js +3 -1
- package/lib/util/blocklet.js +10 -6
- package/lib/util/docker/check-docker-installed.js +17 -0
- package/lib/util/docker/create-docker-image.js +82 -0
- package/lib/util/docker/docker-container-prune.js +71 -0
- package/lib/util/docker/docker-remove-by-name.js +20 -0
- package/lib/util/docker/parse-docker-name.js +10 -0
- package/lib/util/docker/parse-docker-options-from-pm2.js +138 -0
- package/lib/util/docker/shard-network.js +10 -0
- package/lib/util/install-external-dependencies.js +5 -1
- package/lib/util/promise-debounce.js +29 -0
- package/lib/util/promise-once.js +14 -0
- package/lib/util/promise-spawn.js +49 -0
- package/lib/validators/node.js +2 -0
- package/package.json +24 -22
package/lib/api/node.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/lib/states/node.js
CHANGED
|
@@ -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;
|
package/lib/util/blocklet.js
CHANGED
|
@@ -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
|
|
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,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,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;
|
package/lib/validators/node.js
CHANGED
|
@@ -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.
|
|
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.
|
|
23
|
-
"@abtnode/auth": "1.16.
|
|
24
|
-
"@abtnode/certificate-manager": "1.16.
|
|
25
|
-
"@abtnode/constant": "1.16.
|
|
26
|
-
"@abtnode/cron": "1.16.
|
|
27
|
-
"@abtnode/logger": "1.16.
|
|
28
|
-
"@abtnode/models": "1.16.
|
|
29
|
-
"@abtnode/queue": "1.16.
|
|
30
|
-
"@abtnode/rbac": "1.16.
|
|
31
|
-
"@abtnode/router-provider": "1.16.
|
|
32
|
-
"@abtnode/static-server": "1.16.
|
|
33
|
-
"@abtnode/timemachine": "1.16.
|
|
34
|
-
"@abtnode/util": "1.16.
|
|
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.
|
|
46
|
-
"@blocklet/env": "1.16.
|
|
47
|
-
"@blocklet/meta": "1.16.
|
|
48
|
-
"@blocklet/resolver": "1.16.
|
|
49
|
-
"@blocklet/sdk": "1.16.
|
|
50
|
-
"@blocklet/store": "1.16.
|
|
51
|
-
"@did-space/client": "^0.5.
|
|
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": "
|
|
108
|
+
"gitHead": "f609e3593366aace895c936b00f9623002e29b32"
|
|
107
109
|
}
|