@abtnode/core 1.17.8-beta-20260108-224855-28496abb → 1.17.8-beta-20260111-112953-aed5ff39
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/team/access-key-manager.js +104 -0
- package/lib/api/team/invitation-manager.js +461 -0
- package/lib/api/team/notification-manager.js +189 -0
- package/lib/api/team/oauth-manager.js +60 -0
- package/lib/api/team/org-crud-manager.js +202 -0
- package/lib/api/team/org-manager.js +56 -0
- package/lib/api/team/org-member-manager.js +403 -0
- package/lib/api/team/org-query-manager.js +126 -0
- package/lib/api/team/org-resource-manager.js +186 -0
- package/lib/api/team/passport-manager.js +670 -0
- package/lib/api/team/rbac-manager.js +335 -0
- package/lib/api/team/session-manager.js +540 -0
- package/lib/api/team/store-manager.js +198 -0
- package/lib/api/team/tag-manager.js +230 -0
- package/lib/api/team/user-auth-manager.js +132 -0
- package/lib/api/team/user-manager.js +78 -0
- package/lib/api/team/user-query-manager.js +299 -0
- package/lib/api/team/user-social-manager.js +354 -0
- package/lib/api/team/user-update-manager.js +224 -0
- package/lib/api/team/verify-code-manager.js +161 -0
- package/lib/api/team.js +439 -3287
- package/lib/blocklet/manager/disk/auth-manager.js +68 -0
- package/lib/blocklet/manager/disk/backup-manager.js +288 -0
- package/lib/blocklet/manager/disk/cleanup-manager.js +157 -0
- package/lib/blocklet/manager/disk/component-manager.js +83 -0
- package/lib/blocklet/manager/disk/config-manager.js +191 -0
- package/lib/blocklet/manager/disk/controller-manager.js +64 -0
- package/lib/blocklet/manager/disk/delete-reset-manager.js +328 -0
- package/lib/blocklet/manager/disk/download-manager.js +96 -0
- package/lib/blocklet/manager/disk/env-config-manager.js +311 -0
- package/lib/blocklet/manager/disk/federated-manager.js +651 -0
- package/lib/blocklet/manager/disk/hook-manager.js +124 -0
- package/lib/blocklet/manager/disk/install-component-manager.js +95 -0
- package/lib/blocklet/manager/disk/install-core-manager.js +448 -0
- package/lib/blocklet/manager/disk/install-download-manager.js +313 -0
- package/lib/blocklet/manager/disk/install-manager.js +36 -0
- package/lib/blocklet/manager/disk/install-upgrade-manager.js +340 -0
- package/lib/blocklet/manager/disk/job-manager.js +467 -0
- package/lib/blocklet/manager/disk/lifecycle-manager.js +26 -0
- package/lib/blocklet/manager/disk/notification-manager.js +343 -0
- package/lib/blocklet/manager/disk/query-manager.js +562 -0
- package/lib/blocklet/manager/disk/settings-manager.js +507 -0
- package/lib/blocklet/manager/disk/start-manager.js +611 -0
- package/lib/blocklet/manager/disk/stop-restart-manager.js +292 -0
- package/lib/blocklet/manager/disk/update-manager.js +153 -0
- package/lib/blocklet/manager/disk.js +669 -5796
- package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +5 -0
- package/lib/blocklet/manager/lock.js +18 -0
- package/lib/event/index.js +28 -24
- package/lib/router/helper.js +5 -1
- package/lib/util/blocklet/app-utils.js +192 -0
- package/lib/util/blocklet/blocklet-loader.js +258 -0
- package/lib/util/blocklet/config-manager.js +232 -0
- package/lib/util/blocklet/did-document.js +240 -0
- package/lib/util/blocklet/environment.js +555 -0
- package/lib/util/blocklet/health-check.js +449 -0
- package/lib/util/blocklet/install-utils.js +365 -0
- package/lib/util/blocklet/logo.js +57 -0
- package/lib/util/blocklet/meta-utils.js +269 -0
- package/lib/util/blocklet/port-manager.js +141 -0
- package/lib/util/blocklet/process-manager.js +504 -0
- package/lib/util/blocklet/runtime-info.js +105 -0
- package/lib/util/blocklet/validation.js +418 -0
- package/lib/util/blocklet.js +98 -3066
- package/lib/util/wallet-app-notification.js +40 -0
- package/package.json +22 -22
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
/**
|
|
3
|
+
* Port Manager Module
|
|
4
|
+
*
|
|
5
|
+
* Functions for managing blocklet port allocation and conflict resolution
|
|
6
|
+
* Extracted from blocklet.js for better modularity
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet:port-manager');
|
|
10
|
+
const { DBCache, getAbtNodeRedisAndSQLiteUrl } = require('@abtnode/db-cache');
|
|
11
|
+
const { isPortTaken } = require('@abtnode/util/lib/port');
|
|
12
|
+
const { forEachComponentV2 } = require('@blocklet/meta/lib/util');
|
|
13
|
+
|
|
14
|
+
const { shouldSkipComponent } = require('./process-manager');
|
|
15
|
+
|
|
16
|
+
// Lock for port assignment to prevent race conditions in multi-process environment
|
|
17
|
+
const portAssignLock = new DBCache(() => ({
|
|
18
|
+
...getAbtNodeRedisAndSQLiteUrl(),
|
|
19
|
+
prefix: 'blocklet-port-assign-lock',
|
|
20
|
+
ttl: 1000 * 30, // 30 seconds timeout
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure ports shape matches between ports and greenPorts
|
|
25
|
+
* @param {object} _states - States (unused)
|
|
26
|
+
* @param {object} portsA - Source ports
|
|
27
|
+
* @param {object} portsB - Target ports to fill
|
|
28
|
+
*/
|
|
29
|
+
const ensurePortsShape = (_states, portsA, portsB) => {
|
|
30
|
+
if (!portsA || Object.keys(portsA).length === 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (Object.keys(portsB).length === 0) {
|
|
34
|
+
for (const key of Object.keys(portsA)) {
|
|
35
|
+
portsB[key] = portsA[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ensure app ports are not occupied, refresh if needed
|
|
42
|
+
* @param {object} options - Options
|
|
43
|
+
* @param {object} options.blocklet - Blocklet object
|
|
44
|
+
* @param {Array} options.componentDids - Component DIDs to check
|
|
45
|
+
* @param {object} options.states - State managers
|
|
46
|
+
* @param {object} options.manager - Blocklet manager
|
|
47
|
+
* @param {boolean} options.isGreen - Whether checking green ports
|
|
48
|
+
* @returns {Promise<object>} Updated blocklet
|
|
49
|
+
*/
|
|
50
|
+
const ensureAppPortsNotOccupied = async ({ blocklet, componentDids: inputDids, states, manager, isGreen = false }) => {
|
|
51
|
+
const { did } = blocklet.meta;
|
|
52
|
+
const lockName = `port-check-${did}`;
|
|
53
|
+
|
|
54
|
+
// ⚠️ 关键修复:使用 DBCache 锁确保端口分配的原子性
|
|
55
|
+
// 在多进程环境下,防止多个进程同时检查同一个端口
|
|
56
|
+
await portAssignLock.acquire(lockName);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const occupiedDids = new Set();
|
|
60
|
+
|
|
61
|
+
await forEachComponentV2(blocklet, async (b) => {
|
|
62
|
+
try {
|
|
63
|
+
if (shouldSkipComponent(b.meta.did, inputDids)) return;
|
|
64
|
+
|
|
65
|
+
if (!b.greenPorts) {
|
|
66
|
+
occupiedDids.add(b.meta.did);
|
|
67
|
+
b.greenPorts = {};
|
|
68
|
+
}
|
|
69
|
+
const { ports = {}, greenPorts } = b;
|
|
70
|
+
ensurePortsShape(states, ports, greenPorts);
|
|
71
|
+
|
|
72
|
+
const targetPorts = isGreen ? greenPorts : ports;
|
|
73
|
+
|
|
74
|
+
let currentOccupied = false;
|
|
75
|
+
for (const port of Object.values(targetPorts)) {
|
|
76
|
+
currentOccupied = await isPortTaken(port);
|
|
77
|
+
if (currentOccupied) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (currentOccupied) {
|
|
83
|
+
occupiedDids.add(b.meta.did);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.error('Failed to check ports occupied', { error, blockletDid: b.meta.did, isGreen });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (occupiedDids.size === 0) {
|
|
91
|
+
logger.info('No occupied ports detected, no refresh needed', { did, isGreen });
|
|
92
|
+
return blocklet;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const componentDids = Array.from(occupiedDids);
|
|
96
|
+
const {
|
|
97
|
+
refreshed,
|
|
98
|
+
componentDids: actuallyRefreshedDids,
|
|
99
|
+
isInitialAssignment,
|
|
100
|
+
} = await states.blocklet.refreshBlockletPorts(did, componentDids, isGreen);
|
|
101
|
+
|
|
102
|
+
// 只有真正刷新了端口才打印日志和更新环境
|
|
103
|
+
if (refreshed && actuallyRefreshedDids.length > 0) {
|
|
104
|
+
// 区分首次分配和冲突刷新,使用不同的日志信息
|
|
105
|
+
if (isInitialAssignment) {
|
|
106
|
+
logger.info('Assigned green ports for blue-green deployment', {
|
|
107
|
+
did,
|
|
108
|
+
componentDids: actuallyRefreshedDids,
|
|
109
|
+
isGreen,
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
logger.info('Refreshed component ports due to conflict', {
|
|
113
|
+
did,
|
|
114
|
+
componentDids: actuallyRefreshedDids,
|
|
115
|
+
isGreen,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await manager._updateBlockletEnvironment(did);
|
|
120
|
+
const newBlocklet = await manager.ensureBlocklet(did);
|
|
121
|
+
|
|
122
|
+
return newBlocklet;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
logger.info('Ports were detected as occupied but not actually occupied during refresh, no refresh needed', {
|
|
126
|
+
did,
|
|
127
|
+
componentDids,
|
|
128
|
+
isGreen,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return blocklet;
|
|
132
|
+
} finally {
|
|
133
|
+
await portAssignLock.releaseLock(lockName);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
portAssignLock,
|
|
139
|
+
ensurePortsShape,
|
|
140
|
+
ensureAppPortsNotOccupied,
|
|
141
|
+
};
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
/* eslint-disable no-await-in-loop */
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const get = require('lodash/get');
|
|
7
|
+
const omit = require('lodash/omit');
|
|
8
|
+
const shelljs = require('shelljs');
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet:process-manager');
|
|
11
|
+
const pm2 = require('@abtnode/util/lib/pm2/async-pm2');
|
|
12
|
+
const getPm2ProcessInfo = require('@abtnode/util/lib/get-pm2-process-info');
|
|
13
|
+
const { getSecurityNodeOptions } = require('@abtnode/util/lib/security');
|
|
14
|
+
const { killProcessOccupiedPorts } = require('@abtnode/util/lib/port');
|
|
15
|
+
const fetchPm2 = require('@abtnode/util/lib/pm2/fetch-pm2');
|
|
16
|
+
const { DBCache, getAbtNodeRedisAndSQLiteUrl } = require('@abtnode/db-cache');
|
|
17
|
+
const { BLOCKLET_MAX_MEM_LIMIT_IN_MB } = require('@abtnode/constant');
|
|
18
|
+
const {
|
|
19
|
+
BlockletStatus,
|
|
20
|
+
BlockletGroup,
|
|
21
|
+
BLOCKLET_MODES,
|
|
22
|
+
BLOCKLET_DEFAULT_PORT_NAME,
|
|
23
|
+
STATIC_SERVER_ENGINE_DID,
|
|
24
|
+
} = require('@blocklet/constant');
|
|
25
|
+
const { forEachBlocklet, hasStartEngine, isExternalBlocklet } = require('@blocklet/meta/lib/util');
|
|
26
|
+
const { getBlockletEngine } = require('@blocklet/meta/lib/engine');
|
|
27
|
+
const { validateBlockletEntry } = require('@blocklet/meta/lib/entry');
|
|
28
|
+
|
|
29
|
+
const promiseSpawn = require('@abtnode/util/lib/promise-spawn');
|
|
30
|
+
|
|
31
|
+
const parseDockerOptionsFromPm2 = require('../docker/parse-docker-options-from-pm2');
|
|
32
|
+
const dockerRemoveByName = require('../docker/docker-remove-by-name');
|
|
33
|
+
const { createDockerNetwork } = require('../docker/docker-network');
|
|
34
|
+
const parseDockerName = require('../docker/parse-docker-name');
|
|
35
|
+
const { ensureBun } = require('../ensure-bun');
|
|
36
|
+
const { installExternalDependencies } = require('../install-external-dependencies');
|
|
37
|
+
|
|
38
|
+
const SCRIPT_ENGINES_WHITE_LIST = ['npm', 'npx', 'pnpm', 'yarn'];
|
|
39
|
+
|
|
40
|
+
const startLock = new DBCache(() => ({
|
|
41
|
+
...getAbtNodeRedisAndSQLiteUrl(),
|
|
42
|
+
prefix: 'blocklet-start-locks2',
|
|
43
|
+
ttl: 1000 * 60 * 3,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const statusMap = {
|
|
47
|
+
online: BlockletStatus.running,
|
|
48
|
+
launching: BlockletStatus.starting,
|
|
49
|
+
errored: BlockletStatus.error,
|
|
50
|
+
stopping: BlockletStatus.stopping,
|
|
51
|
+
stopped: BlockletStatus.stopped,
|
|
52
|
+
'waiting restart': BlockletStatus.restarting,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const noop = () => {
|
|
56
|
+
//
|
|
57
|
+
};
|
|
58
|
+
const noopAsync = async () => {
|
|
59
|
+
//
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isUsefulError = (err) =>
|
|
63
|
+
err &&
|
|
64
|
+
err.message !== 'process or namespace not found' &&
|
|
65
|
+
!/id unknown/.test(err.message) &&
|
|
66
|
+
!/^Process \d+ not found$/.test(err.message);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {*} processId
|
|
70
|
+
* @returns {BlockletStatus}
|
|
71
|
+
*/
|
|
72
|
+
const getProcessState = async (processId) => {
|
|
73
|
+
// eslint-disable-next-line no-use-before-define
|
|
74
|
+
const info = await getProcessInfo(processId);
|
|
75
|
+
if (!statusMap[info.pm2_env.status]) {
|
|
76
|
+
logger.error('Cannot find the blocklet status for pm2 status mapping', {
|
|
77
|
+
pm2Status: info.pm2_env.status,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return BlockletStatus.error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return statusMap[info.pm2_env.status];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getProcessInfo = (processId, { throwOnNotExist = true, timeout = 10_000 } = {}) =>
|
|
87
|
+
getPm2ProcessInfo(processId, { printError: logger.error.bind(logger), throwOnNotExist, timeout });
|
|
88
|
+
|
|
89
|
+
const deleteProcess = (processId) => {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
pm2.delete(processId, async (err) => {
|
|
92
|
+
if (isUsefulError(err)) {
|
|
93
|
+
logger.error('blocklet process delete failed', { processId, error: err });
|
|
94
|
+
return reject(err);
|
|
95
|
+
}
|
|
96
|
+
await dockerRemoveByName(processId);
|
|
97
|
+
return resolve(processId);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const reloadProcess = (processId) =>
|
|
103
|
+
new Promise((resolve, reject) => {
|
|
104
|
+
pm2.reload(processId, (err) => {
|
|
105
|
+
if (err) {
|
|
106
|
+
if (isUsefulError(err)) {
|
|
107
|
+
logger.error('blocklet reload failed', { processId, error: err });
|
|
108
|
+
}
|
|
109
|
+
return reject(err);
|
|
110
|
+
}
|
|
111
|
+
return resolve(processId);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const shouldSkipComponent = (componentDid, whiteList) => {
|
|
116
|
+
if (!whiteList || !Array.isArray(whiteList)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const arr = whiteList.filter(Boolean);
|
|
121
|
+
|
|
122
|
+
if (!arr.length) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return !arr.includes(componentDid);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Start all precesses of a blocklet
|
|
131
|
+
* @param {*} blocklet should contain env props
|
|
132
|
+
* @param {object} options Start options
|
|
133
|
+
* @param {object} options.getComponentStartEngine Function to get component start engine
|
|
134
|
+
* @param {object} options.getRuntimeEnvironments Function to get runtime environments
|
|
135
|
+
*/
|
|
136
|
+
const startBlockletProcess = async (
|
|
137
|
+
blocklet,
|
|
138
|
+
{
|
|
139
|
+
preFlight = noop,
|
|
140
|
+
preStart = noop,
|
|
141
|
+
postStart = noopAsync,
|
|
142
|
+
nodeEnvironments,
|
|
143
|
+
nodeInfo,
|
|
144
|
+
e2eMode,
|
|
145
|
+
skippedProcessIds = [],
|
|
146
|
+
componentDids,
|
|
147
|
+
configSynchronizer,
|
|
148
|
+
onlyStart = false,
|
|
149
|
+
isGreen = false,
|
|
150
|
+
getComponentStartEngine,
|
|
151
|
+
getRuntimeEnvironments,
|
|
152
|
+
} = {}
|
|
153
|
+
) => {
|
|
154
|
+
if (!blocklet) {
|
|
155
|
+
throw new Error('blocklet should not be empty');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const dockerNetworkName = parseDockerName(blocklet?.meta?.did, 'docker-network');
|
|
159
|
+
await createDockerNetwork(dockerNetworkName);
|
|
160
|
+
|
|
161
|
+
blocklet.children.forEach((component) => {
|
|
162
|
+
if (!componentDids.includes(component.meta.did)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const envItem of component.environments) {
|
|
166
|
+
const envKey = envItem.key || envItem.name;
|
|
167
|
+
if (envKey !== 'BLOCKLET_PORT') {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (isGreen) {
|
|
171
|
+
if (component.greenPorts?.BLOCKLET_PORT) {
|
|
172
|
+
envItem.value = component.greenPorts.BLOCKLET_PORT;
|
|
173
|
+
}
|
|
174
|
+
} else if (component.ports?.BLOCKLET_PORT) {
|
|
175
|
+
envItem.value = component.ports.BLOCKLET_PORT;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const startBlockletTask = async (b, { ancestors }) => {
|
|
181
|
+
// 需要在在这里传入字符串类型,否则进程中如法转化成 Date 对象
|
|
182
|
+
const now = `${new Date()}`;
|
|
183
|
+
if (b.meta.group === BlockletGroup.gateway) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!hasStartEngine(b.meta)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { processId, logsDir, appDir } = b.env;
|
|
192
|
+
|
|
193
|
+
if (skippedProcessIds.includes(processId)) {
|
|
194
|
+
logger.info('skip start skipped process', { processId });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (shouldSkipComponent(b.meta.did, componentDids)) {
|
|
199
|
+
logger.info('skip start process not selected', { processId });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (b.mode !== BLOCKLET_MODES.DEVELOPMENT) {
|
|
204
|
+
validateBlockletEntry(appDir, b.meta);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { cwd, script, args, environmentObj, interpreter, interpreterArgs } = getComponentStartEngine(b, {
|
|
208
|
+
e2eMode,
|
|
209
|
+
});
|
|
210
|
+
if (!script) {
|
|
211
|
+
logger.info('skip start process without script', { processId });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// get env
|
|
216
|
+
const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors, isGreen);
|
|
217
|
+
const startedAt = Date.now();
|
|
218
|
+
|
|
219
|
+
await installExternalDependencies({ appDir: env?.BLOCKLET_APP_DIR, nodeInfo });
|
|
220
|
+
await preFlight(b, { env: { ...env } });
|
|
221
|
+
await preStart(b, { env: { ...env } });
|
|
222
|
+
|
|
223
|
+
// kill process if port is occupied
|
|
224
|
+
try {
|
|
225
|
+
const { ports, greenPorts } = b;
|
|
226
|
+
await killProcessOccupiedPorts({
|
|
227
|
+
ports: isGreen ? greenPorts : ports,
|
|
228
|
+
pm2ProcessId: processId,
|
|
229
|
+
printError: logger.error.bind(logger),
|
|
230
|
+
});
|
|
231
|
+
} catch (error) {
|
|
232
|
+
logger.error('Failed to killProcessOccupiedPorts', { error });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// start process
|
|
236
|
+
const maxMemoryRestart = get(nodeInfo, 'runtimeConfig.blockletMaxMemoryLimit', BLOCKLET_MAX_MEM_LIMIT_IN_MB);
|
|
237
|
+
const processIdName = isGreen ? `${processId}-green` : processId;
|
|
238
|
+
/**
|
|
239
|
+
* @type {pm2.StartOptions}
|
|
240
|
+
*/
|
|
241
|
+
const options = {
|
|
242
|
+
namespace: 'blocklets',
|
|
243
|
+
name: processIdName,
|
|
244
|
+
cwd,
|
|
245
|
+
log_date_format: '(YYYY-MM-DD HH:mm:ss)',
|
|
246
|
+
output: path.join(logsDir, 'output.log'),
|
|
247
|
+
error: path.join(logsDir, 'error.log'),
|
|
248
|
+
// wait_ready: process.env.NODE_ENV !== 'test',
|
|
249
|
+
wait_ready: false,
|
|
250
|
+
listen_timeout: 3000,
|
|
251
|
+
max_memory_restart: `${maxMemoryRestart}M`,
|
|
252
|
+
max_restarts: b.mode === BLOCKLET_MODES.DEVELOPMENT ? 0 : 3,
|
|
253
|
+
min_uptime: 10_000,
|
|
254
|
+
exp_backoff_restart_delay: 300,
|
|
255
|
+
env: omit(
|
|
256
|
+
{
|
|
257
|
+
...environmentObj,
|
|
258
|
+
...env,
|
|
259
|
+
NODE_ENV: 'production',
|
|
260
|
+
BLOCKLET_START_AT: now,
|
|
261
|
+
NODE_OPTIONS: await getSecurityNodeOptions(b, nodeInfo.enableFileSystemIsolation),
|
|
262
|
+
},
|
|
263
|
+
// should only inject appSk and appPsk to the blocklet environment when unsafe mode enabled
|
|
264
|
+
['1', 1].includes(env.UNSAFE_MODE) ? [] : ['BLOCKLET_APP_SK', 'BLOCKLET_APP_PSK']
|
|
265
|
+
),
|
|
266
|
+
script,
|
|
267
|
+
args,
|
|
268
|
+
interpreter,
|
|
269
|
+
interpreterArgs,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const clusterMode = get(b.meta, 'capabilities.clusterMode', false);
|
|
273
|
+
if (clusterMode && b.mode !== BLOCKLET_MODES.DEVELOPMENT) {
|
|
274
|
+
const clusterSize = Number(blocklet.configObj.BLOCKLET_CLUSTER_SIZE) || +process.env.ABT_NODE_MAX_CLUSTER_SIZE;
|
|
275
|
+
options.execMode = 'cluster';
|
|
276
|
+
options.mergeLogs = true;
|
|
277
|
+
options.instances = Math.max(Math.min(os.cpus().length, clusterSize), 1);
|
|
278
|
+
options.env.BLOCKLET_CLUSTER_SIZE = options.instances;
|
|
279
|
+
if (options.instances !== clusterSize) {
|
|
280
|
+
logger.warn(`Fallback cluster size to ${options.instances} for ${processId}, ignore custom ${clusterSize}`);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
delete options.env.BLOCKLET_CLUSTER_SIZE;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (b.mode === BLOCKLET_MODES.DEVELOPMENT) {
|
|
287
|
+
options.env.NODE_ENV = e2eMode ? 'e2e' : 'development';
|
|
288
|
+
options.env.IS_E2E = e2eMode ? '1' : undefined;
|
|
289
|
+
options.env.BROWSER = 'none';
|
|
290
|
+
options.env.PORT = options.env[BLOCKLET_DEFAULT_PORT_NAME];
|
|
291
|
+
|
|
292
|
+
if (process.platform === 'win32') {
|
|
293
|
+
const [cmd, ...argList] = options.script.split(' ').filter(Boolean);
|
|
294
|
+
|
|
295
|
+
if (!SCRIPT_ENGINES_WHITE_LIST.includes(cmd)) {
|
|
296
|
+
throw new Error(`${cmd} script is not supported, ${SCRIPT_ENGINES_WHITE_LIST.join(', ')} are supported`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { stdout: nodejsBinPath } = shelljs.which('node');
|
|
300
|
+
|
|
301
|
+
const cmdPath = path.join(path.dirname(nodejsBinPath), 'node_modules', cmd);
|
|
302
|
+
|
|
303
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cmdPath, 'package.json'), 'utf8'));
|
|
304
|
+
const cmdBinPath = pkg.bin[cmd];
|
|
305
|
+
|
|
306
|
+
options.script = path.resolve(cmdPath, cmdBinPath);
|
|
307
|
+
options.args = [...argList].join(' ');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await configSynchronizer.syncComponentConfig(b.meta.did, blocklet.meta.did, {
|
|
312
|
+
serverSk: nodeEnvironments.ABT_NODE_SK,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (options.interpreter === 'bun') {
|
|
316
|
+
options.exec_interpreter = await ensureBun();
|
|
317
|
+
options.exec_mode = 'fork';
|
|
318
|
+
delete options.instances;
|
|
319
|
+
delete options.mergeLogs;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let nextOptions;
|
|
323
|
+
if (b.mode === BLOCKLET_MODES.DEVELOPMENT) {
|
|
324
|
+
nextOptions = options;
|
|
325
|
+
} else {
|
|
326
|
+
nextOptions = await parseDockerOptionsFromPm2({
|
|
327
|
+
options,
|
|
328
|
+
nodeInfo,
|
|
329
|
+
isExternal: isExternalBlocklet(b),
|
|
330
|
+
meta: b.meta,
|
|
331
|
+
ports: isGreen ? b.greenPorts : b.ports,
|
|
332
|
+
onlyStart,
|
|
333
|
+
dockerNetworkName,
|
|
334
|
+
rootBlocklet: blocklet,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
await fetchPm2({ ...nextOptions, pmx: false }, nodeEnvironments.ABT_NODE_SK);
|
|
339
|
+
|
|
340
|
+
const status = await getProcessState(processIdName);
|
|
341
|
+
if (status === BlockletStatus.error) {
|
|
342
|
+
throw new Error(`process ${processIdName} is not running within 3 seconds`);
|
|
343
|
+
}
|
|
344
|
+
logger.info('done start blocklet', { processId: processIdName, status, time: Date.now() - startedAt });
|
|
345
|
+
|
|
346
|
+
if (nextOptions.env.connectInternalDockerNetwork) {
|
|
347
|
+
try {
|
|
348
|
+
await promiseSpawn(nextOptions.env.connectInternalDockerNetwork, { mute: true });
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logger.warn('blocklet connect internal docker network failed', { processId: processIdName, error: err });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// run hook
|
|
355
|
+
postStart(b, { env }).catch((err) => {
|
|
356
|
+
logger.error('blocklet post start failed', { processId: processIdName, error: err });
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
await forEachBlocklet(
|
|
361
|
+
blocklet,
|
|
362
|
+
/**
|
|
363
|
+
*
|
|
364
|
+
* @param {import('@blocklet/server-js').BlockletState} b
|
|
365
|
+
* @param {*} param1
|
|
366
|
+
* @returns
|
|
367
|
+
*/
|
|
368
|
+
async (b, { ancestors }) => {
|
|
369
|
+
const lockName = `${blocklet.meta.did}-${b.meta.did}`;
|
|
370
|
+
|
|
371
|
+
// 如果锁存在,则跳过执行
|
|
372
|
+
if (!(await startLock.hasExpired(lockName))) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
await startLock.acquire(lockName);
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
await startBlockletTask(b, { ancestors });
|
|
379
|
+
} finally {
|
|
380
|
+
startLock.releaseLock(lockName);
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
{ parallel: true, concurrencyLimit: 3 }
|
|
384
|
+
);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Stop all precesses of a blocklet
|
|
389
|
+
* @param {*} blocklet should contain env props
|
|
390
|
+
*/
|
|
391
|
+
const stopBlockletProcess = (
|
|
392
|
+
blocklet,
|
|
393
|
+
{ preStop = noop, skippedProcessIds = [], componentDids, isGreen = false, isStopGreenAndBlue = false } = {}
|
|
394
|
+
) => {
|
|
395
|
+
// eslint-disable-next-line no-use-before-define
|
|
396
|
+
return deleteBlockletProcess(blocklet, {
|
|
397
|
+
preDelete: preStop,
|
|
398
|
+
skippedProcessIds,
|
|
399
|
+
componentDids,
|
|
400
|
+
isGreen,
|
|
401
|
+
isStopGreenAndBlue,
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Delete all precesses of a blocklet
|
|
407
|
+
* @param {*} blocklet should contain env props
|
|
408
|
+
*/
|
|
409
|
+
const deleteBlockletProcess = async (
|
|
410
|
+
blocklet,
|
|
411
|
+
{ preDelete = noop, skippedProcessIds = [], componentDids, isGreen = false, isStopGreenAndBlue = false } = {}
|
|
412
|
+
) => {
|
|
413
|
+
await forEachBlocklet(
|
|
414
|
+
blocklet,
|
|
415
|
+
async (b, { ancestors }) => {
|
|
416
|
+
// NOTICE: 如果不判断 group, 在 github action 中测试 disk.spec.js 时会报错, 但是在 mac 中跑测试不会报错
|
|
417
|
+
if (b.meta?.group === BlockletGroup.gateway) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (skippedProcessIds.includes(b.env.processId)) {
|
|
422
|
+
logger.info(`skip delete skipped process ${b.env.processId}`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (shouldSkipComponent(b.meta?.did, componentDids)) {
|
|
427
|
+
logger.info(`skip delete process not selected: ${b.meta.did}`, { processId: b.env.processId });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!hasStartEngine(b.meta)) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Skip deleting static-server engine processes since they were never started
|
|
436
|
+
if (b.meta?.group === 'static') {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const engine = getBlockletEngine(b.meta);
|
|
440
|
+
if (engine.interpreter === 'blocklet' && engine.source?.name === STATIC_SERVER_ENGINE_DID) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await preDelete(b, { ancestors });
|
|
445
|
+
if (isStopGreenAndBlue) {
|
|
446
|
+
await deleteProcess(`${b.env.processId}-green`);
|
|
447
|
+
await deleteProcess(b.env.processId);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const processId = isGreen ? `${b.env.processId}-green` : b.env.processId;
|
|
451
|
+
await deleteProcess(processId);
|
|
452
|
+
},
|
|
453
|
+
{ parallel: true }
|
|
454
|
+
);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Reload all precesses of a blocklet
|
|
459
|
+
* @param {*} blocklet should contain env props
|
|
460
|
+
*/
|
|
461
|
+
const reloadBlockletProcess = (blocklet, { componentDids } = {}) =>
|
|
462
|
+
forEachBlocklet(
|
|
463
|
+
blocklet,
|
|
464
|
+
async (b) => {
|
|
465
|
+
if (b.meta.group === BlockletGroup.gateway) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (shouldSkipComponent(b.meta.did, componentDids)) {
|
|
470
|
+
logger.info('skip reload process', { processId: b.env.processId });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Skip reloading static-server engine processes since they were never started
|
|
475
|
+
if (b.meta?.group === 'static') {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const engine = getBlockletEngine(b.meta);
|
|
479
|
+
if (engine.interpreter === 'blocklet' && engine.source?.name === STATIC_SERVER_ENGINE_DID) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
await reloadProcess(b.env.processId);
|
|
484
|
+
logger.info('done reload process', { processId: b.env.processId });
|
|
485
|
+
},
|
|
486
|
+
{ parallel: false }
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
module.exports = {
|
|
490
|
+
startBlockletProcess,
|
|
491
|
+
stopBlockletProcess,
|
|
492
|
+
deleteBlockletProcess,
|
|
493
|
+
reloadBlockletProcess,
|
|
494
|
+
getProcessState,
|
|
495
|
+
getProcessInfo,
|
|
496
|
+
deleteProcess,
|
|
497
|
+
reloadProcess,
|
|
498
|
+
shouldSkipComponent,
|
|
499
|
+
startLock,
|
|
500
|
+
statusMap,
|
|
501
|
+
noop,
|
|
502
|
+
noopAsync,
|
|
503
|
+
isUsefulError,
|
|
504
|
+
};
|