@abtnode/core 1.17.8-beta-20260109-075740-5f484e08 → 1.17.8-beta-20260113-015027-32a1cec4

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 (65) hide show
  1. package/lib/api/team/access-key-manager.js +104 -0
  2. package/lib/api/team/invitation-manager.js +461 -0
  3. package/lib/api/team/notification-manager.js +189 -0
  4. package/lib/api/team/oauth-manager.js +60 -0
  5. package/lib/api/team/org-crud-manager.js +202 -0
  6. package/lib/api/team/org-manager.js +56 -0
  7. package/lib/api/team/org-member-manager.js +403 -0
  8. package/lib/api/team/org-query-manager.js +126 -0
  9. package/lib/api/team/org-resource-manager.js +186 -0
  10. package/lib/api/team/passport-manager.js +670 -0
  11. package/lib/api/team/rbac-manager.js +335 -0
  12. package/lib/api/team/session-manager.js +540 -0
  13. package/lib/api/team/store-manager.js +198 -0
  14. package/lib/api/team/tag-manager.js +230 -0
  15. package/lib/api/team/user-auth-manager.js +132 -0
  16. package/lib/api/team/user-manager.js +78 -0
  17. package/lib/api/team/user-query-manager.js +299 -0
  18. package/lib/api/team/user-social-manager.js +354 -0
  19. package/lib/api/team/user-update-manager.js +224 -0
  20. package/lib/api/team/verify-code-manager.js +161 -0
  21. package/lib/api/team.js +439 -3287
  22. package/lib/blocklet/manager/disk/auth-manager.js +68 -0
  23. package/lib/blocklet/manager/disk/backup-manager.js +288 -0
  24. package/lib/blocklet/manager/disk/cleanup-manager.js +157 -0
  25. package/lib/blocklet/manager/disk/component-manager.js +83 -0
  26. package/lib/blocklet/manager/disk/config-manager.js +191 -0
  27. package/lib/blocklet/manager/disk/controller-manager.js +64 -0
  28. package/lib/blocklet/manager/disk/delete-reset-manager.js +328 -0
  29. package/lib/blocklet/manager/disk/download-manager.js +96 -0
  30. package/lib/blocklet/manager/disk/env-config-manager.js +311 -0
  31. package/lib/blocklet/manager/disk/federated-manager.js +651 -0
  32. package/lib/blocklet/manager/disk/hook-manager.js +124 -0
  33. package/lib/blocklet/manager/disk/install-component-manager.js +95 -0
  34. package/lib/blocklet/manager/disk/install-core-manager.js +448 -0
  35. package/lib/blocklet/manager/disk/install-download-manager.js +313 -0
  36. package/lib/blocklet/manager/disk/install-manager.js +36 -0
  37. package/lib/blocklet/manager/disk/install-upgrade-manager.js +340 -0
  38. package/lib/blocklet/manager/disk/job-manager.js +467 -0
  39. package/lib/blocklet/manager/disk/lifecycle-manager.js +26 -0
  40. package/lib/blocklet/manager/disk/notification-manager.js +343 -0
  41. package/lib/blocklet/manager/disk/query-manager.js +562 -0
  42. package/lib/blocklet/manager/disk/settings-manager.js +507 -0
  43. package/lib/blocklet/manager/disk/start-manager.js +611 -0
  44. package/lib/blocklet/manager/disk/stop-restart-manager.js +292 -0
  45. package/lib/blocklet/manager/disk/update-manager.js +153 -0
  46. package/lib/blocklet/manager/disk.js +669 -5796
  47. package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +5 -0
  48. package/lib/blocklet/manager/lock.js +18 -0
  49. package/lib/event/index.js +28 -24
  50. package/lib/util/blocklet/app-utils.js +192 -0
  51. package/lib/util/blocklet/blocklet-loader.js +258 -0
  52. package/lib/util/blocklet/config-manager.js +232 -0
  53. package/lib/util/blocklet/did-document.js +240 -0
  54. package/lib/util/blocklet/environment.js +555 -0
  55. package/lib/util/blocklet/health-check.js +449 -0
  56. package/lib/util/blocklet/install-utils.js +365 -0
  57. package/lib/util/blocklet/logo.js +57 -0
  58. package/lib/util/blocklet/meta-utils.js +269 -0
  59. package/lib/util/blocklet/port-manager.js +141 -0
  60. package/lib/util/blocklet/process-manager.js +504 -0
  61. package/lib/util/blocklet/runtime-info.js +105 -0
  62. package/lib/util/blocklet/validation.js +418 -0
  63. package/lib/util/blocklet.js +98 -3066
  64. package/lib/util/wallet-app-notification.js +40 -0
  65. package/package.json +22 -22
@@ -1,233 +1,89 @@
1
1
  /* eslint-disable camelcase */
2
- /* eslint-disable no-await-in-loop */
3
2
 
4
- const fs = require('fs-extra');
5
- const path = require('node:path');
6
- const shelljs = require('shelljs');
7
- const os = require('node:os');
8
- const tar = require('tar');
9
- const get = require('lodash/get');
10
- const isNil = require('lodash/isNil');
11
- const uniq = require('lodash/uniq');
12
- const cloneDeep = require('lodash/cloneDeep');
13
- const mergeWith = require('lodash/mergeWith');
14
- const toLower = require('lodash/toLower');
15
- const isEmpty = require('lodash/isEmpty');
16
- const omit = require('lodash/omit');
17
- const pick = require('lodash/pick');
18
- const streamToPromise = require('stream-to-promise');
19
- const { Throttle } = require('stream-throttle');
20
- const { slugify } = require('transliteration');
21
- const ssri = require('ssri');
22
- const diff = require('deep-diff');
23
- const createArchive = require('archiver');
24
- const isUrl = require('is-url');
25
- const semver = require('semver');
26
- const { chainInfo: chainInfoSchema } = require('@arcblock/did-connect-js/lib/schema');
27
-
28
- const { types } = require('@ocap/mcrypto');
29
- const { urlPathFriendly } = require('@blocklet/meta/lib/url-path-friendly');
30
- const { fromSecretKey, fromPublicKey } = require('@ocap/wallet');
31
- const { toHex, isHex, toDid, toAddress, toBuffer } = require('@ocap/util');
32
- const { isValid: isValidDid, isEthereumDid } = require('@arcblock/did');
33
3
  const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
34
- const pm2 = require('@abtnode/util/lib/pm2/async-pm2');
35
- const sleep = require('@abtnode/util/lib/sleep');
36
- const { isCustomDomain } = require('@abtnode/util/lib/url-evaluation');
37
- const getPm2ProcessInfo = require('@abtnode/util/lib/get-pm2-process-info');
38
- const { formatEnv, getSecurityNodeOptions, decrypt } = require('@abtnode/util/lib/security');
39
- const ensureEndpointHealthy = require('@abtnode/util/lib/ensure-endpoint-healthy');
40
- const getFolderSize = require('@abtnode/util/lib/get-folder-size');
41
- const normalizePathPrefix = require('@abtnode/util/lib/normalize-path-prefix');
42
- const hashFiles = require('@abtnode/util/lib/hash-files');
43
- const didDocument = require('@abtnode/util/lib/did-document');
44
- const { DBCache } = require('@abtnode/db-cache');
45
- const {
46
- BLOCKLET_MAX_MEM_LIMIT_IN_MB,
47
- BLOCKLET_INSTALL_TYPE,
48
- APP_STRUCT_VERSION,
49
- BLOCKLET_CACHE_TTL,
50
- AIGNE_CONFIG_ENCRYPT_SALT,
51
- SLOT_FOR_IP_DNS_SITE,
52
- } = require('@abtnode/constant');
53
- const { BLOCKLET_THEME_LIGHT, BLOCKLET_THEME_DARK } = require('@blocklet/theme');
54
4
  const {
55
5
  parseComponents,
56
6
  ensureMeta,
57
7
  filterDuplicateComponents,
58
8
  validateBlockletMeta,
59
- getComponentConfig,
60
- parseOptionalComponents,
61
9
  filterRequiredComponents,
62
10
  } = require('@blocklet/resolver');
63
- const formatBackSlash = require('@abtnode/util/lib/format-back-slash');
64
- const { killProcessOccupiedPorts, isPortTaken } = require('@abtnode/util/lib/port');
65
- const { getComponentApiKey } = require('@abtnode/util/lib/blocklet');
66
- const { toSvg: createDidLogo } = require('@arcblock/did-motif');
67
- const { createBlockiesSvg } = require('@blocklet/meta/lib/blockies');
68
- const formatName = require('@abtnode/util/lib/format-name');
69
- const { hasMountPoint, getBlockletEngine } = require('@blocklet/meta/lib/engine');
70
- const { fixAvatar } = require('@blocklet/sdk/lib/util/user');
71
-
72
- const SCRIPT_ENGINES_WHITE_LIST = ['npm', 'npx', 'pnpm', 'yarn'];
73
-
11
+ const { getBlockletEngine } = require('@blocklet/meta/lib/engine');
12
+ const { forEachBlocklet, getDisplayName, findWebInterface, isExternalBlocklet } = require('@blocklet/meta/lib/util');
13
+ const { getBlockletMetaFromUrl } = require('@blocklet/meta/lib/util-meta');
14
+ const { findInterfacePortByName } = require('./index');
15
+ const { getProcessInfo, getProcessState, shouldSkipComponent } = require('./blocklet/process-manager');
74
16
  const {
75
- BlockletStatus,
76
- BlockletSource,
77
- BlockletGroup,
78
- BLOCKLET_MODES,
79
- BLOCKLET_BUNDLE_FILE,
80
- BLOCKLET_ENTRY_FILE,
81
- BLOCKLET_DEFAULT_PORT_NAME,
82
- BLOCKLET_INTERFACE_TYPE_WEB,
83
- BLOCKLET_CONFIGURABLE_KEY,
84
- fromBlockletStatus,
85
- BLOCKLET_PREFERENCE_FILE,
86
- BLOCKLET_PREFERENCE_PREFIX,
87
- BLOCKLET_RESOURCE_DIR,
88
- BLOCKLET_TENANT_MODES,
89
- PROJECT,
90
- BLOCKLET_INTERFACE_TYPE_DOCKER,
91
- STATIC_SERVER_ENGINE_DID,
92
- } = require('@blocklet/constant');
93
- const { validateBlockletEntry } = require('@blocklet/meta/lib/entry');
94
- const { getBlockletInfo } = require('@blocklet/meta/lib/info');
95
- const { getApplicationWallet: getBlockletWallet } = require('@blocklet/meta/lib/wallet');
17
+ getHealthyCheckTimeout,
18
+ shouldCheckHealthy,
19
+ isBlockletPortHealthy,
20
+ checkBlockletProcessHealthy: _checkBlockletProcessHealthy,
21
+ } = require('./blocklet/health-check');
96
22
  const {
97
- forEachBlocklet,
98
- getDisplayName,
99
- findWebInterface,
100
- forEachBlockletSync,
101
- forEachChildSync,
102
- forEachComponentV2,
103
- forEachComponentV2Sync,
104
- getSharedConfigObj,
105
- getComponentName,
106
- getBlockletAppIdList,
107
- getChainInfo,
108
- isInProgress,
109
- isRunning,
110
- hasStartEngine,
111
- isEnvShareable,
112
- isExternalBlocklet,
113
- } = require('@blocklet/meta/lib/util');
114
- const { getComponentsInternalInfo } = require('@blocklet/meta/lib/blocklet');
115
- const { titleSchema, descriptionSchema, logoSchema } = require('@blocklet/meta/lib/schema');
116
- const { getBlockletMetaFromUrl } = require('@blocklet/meta/lib/util-meta');
117
- const { getComponentProcessId } = require('@blocklet/meta/lib/get-component-process-id');
118
- const { isInServerlessMode } = require('@abtnode/util/lib/serverless');
119
- const { getDidDomainForBlocklet } = require('@abtnode/util/lib/get-domain-for-blocklet');
120
- const md5 = require('@abtnode/util/lib/md5');
121
- const fetchPm2 = require('@abtnode/util/lib/pm2/fetch-pm2');
122
-
123
- const promiseSpawn = require('@abtnode/util/lib/promise-spawn');
124
- const { getAbtNodeRedisAndSQLiteUrl } = require('@abtnode/db-cache');
125
- const { validate: validateEngine, get: getEngine } = require('../blocklet/manager/engine');
126
-
127
- const isRequirementsSatisfied = require('./requirement');
23
+ expandTarball,
24
+ verifyIntegrity,
25
+ getAppDirs,
26
+ pruneBlockletBundle,
27
+ getTypeFromInstallParams,
28
+ getDiffFiles,
29
+ getBundleDir,
30
+ needBlockletDownload,
31
+ } = require('./blocklet/install-utils');
32
+ const { getDiskInfo, getRuntimeInfo } = require('./blocklet/runtime-info');
128
33
  const {
129
- expandBundle,
130
- findInterfacePortByName,
131
- prettyURL,
132
- templateReplace,
133
- getServerDidDomain,
134
- APP_CONFIG_IMAGE_KEYS,
135
- replaceDomainSlot,
136
- } = require('./index');
137
- const { installExternalDependencies } = require('./install-external-dependencies');
138
- const parseDockerOptionsFromPm2 = require('./docker/parse-docker-options-from-pm2');
139
- const dockerRemoveByName = require('./docker/docker-remove-by-name');
140
- const getDockerRuntimeInfo = require('./docker/get-docker-runtime-info');
141
- const parseDockerName = require('./docker/parse-docker-name');
142
- const { createDockerNetwork } = require('./docker/docker-network');
143
- const { ensureBun } = require('./ensure-bun');
144
- const { getFromCache: getAccessibleExternalNodeIp } = require('./get-accessible-external-node-ip');
145
-
146
- /**
147
- * Get actual listening port from Docker container or process
148
- * @param {string} processId - PM2 process ID
149
- * @param {object} blocklet - Blocklet object with meta and env
150
- * @returns {Promise<number|null>} Actual port number or null if not found
151
- */
152
- const getActualListeningPort = async (processId, blocklet) => {
153
- try {
154
- // eslint-disable-next-line no-use-before-define
155
- const info = await getProcessInfo(processId, { timeout: 3_000 });
156
- const dockerName = info.pm2_env?.env?.dockerName;
157
-
158
- if (dockerName) {
159
- // For Docker containers, get actual port from docker inspect
160
- try {
161
- // Get port mapping from docker inspect
162
- const inspectCmd = `docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} {{end}}' ${dockerName}`;
163
- const portMappings = await promiseSpawn(inspectCmd, { mute: true });
164
-
165
- if (portMappings) {
166
- const ports = portMappings.trim().split(/\s+/).filter(Boolean);
167
- for (const portMapping of ports) {
168
- const port = parseInt(portMapping.split('/')[0], 10);
169
- if (port && !Number.isNaN(port)) {
170
- try {
171
- const portCmd = `docker port ${dockerName} ${portMapping}`;
172
- const hostPortOutput = await promiseSpawn(portCmd, { mute: true });
173
- const match = hostPortOutput.match(/:(\d+)$/);
174
- if (match) {
175
- const actualPort = parseInt(match[1], 10);
176
- if (actualPort && !Number.isNaN(actualPort)) {
177
- logger.info('Got actual Docker port from container', {
178
- processId,
179
- dockerName,
180
- containerPort: port,
181
- hostPort: actualPort,
182
- });
183
- return actualPort;
184
- }
185
- }
186
- } catch (err) {
187
- logger.debug('Failed to get port from docker port command', { error: err.message });
188
- }
189
- }
190
- }
191
- }
192
-
193
- // Fallback: try to get from NetworkSettings.Ports directly
194
- const inspectPortsCmd = `docker inspect --format='{{json .NetworkSettings.Ports}}' ${dockerName}`;
195
- const portsJson = await promiseSpawn(inspectPortsCmd, { mute: true });
196
- if (portsJson) {
197
- const ports = JSON.parse(portsJson);
198
- // Find the primary port (usually BLOCKLET_PORT)
199
- const webInterface = (blocklet?.meta?.interfaces || []).find(
200
- (x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB || x.type === BLOCKLET_INTERFACE_TYPE_DOCKER
201
- );
202
- const expectedContainerPort = webInterface?.containerPort || webInterface?.port;
203
-
204
- if (expectedContainerPort) {
205
- const portKey = `${expectedContainerPort}/tcp`;
206
- if (ports[portKey] && ports[portKey][0]) {
207
- const hostPort = parseInt(ports[portKey][0].HostPort, 10);
208
- if (hostPort && !Number.isNaN(hostPort)) {
209
- logger.info('Got actual Docker port from NetworkSettings', {
210
- processId,
211
- dockerName,
212
- containerPort: expectedContainerPort,
213
- hostPort,
214
- });
215
- return hostPort;
216
- }
217
- }
218
- }
219
- }
220
- } catch (error) {
221
- logger.debug('Failed to get Docker port mapping', { error: error.message, processId, dockerName });
222
- }
223
- }
224
-
225
- return null;
226
- } catch (error) {
227
- logger.debug('Failed to get actual listening port', { error: error.message, processId });
228
- return null;
229
- }
230
- };
34
+ validateBlocklet: _validateBlocklet,
35
+ validateBlockletChainInfo,
36
+ checkDuplicateComponents,
37
+ validateAppConfig,
38
+ checkDuplicateAppSk,
39
+ checkDuplicateMountPoint,
40
+ resolveMountPointConflict,
41
+ validateStore,
42
+ validateInServerless,
43
+ checkStructVersion,
44
+ checkVersionCompatibility,
45
+ } = require('./blocklet/validation');
46
+ const {
47
+ shouldEnableSlpDomain,
48
+ getSlpDid,
49
+ getBlockletKnownAs,
50
+ publishDidDocument,
51
+ updateDidDocument,
52
+ updateDidDocumentStateOnly,
53
+ } = require('./blocklet/did-document');
54
+ const {
55
+ getComponentDirs,
56
+ getComponentStartEngine,
57
+ getBlockletConfigObj,
58
+ getAppSystemEnvironments,
59
+ getAppOverwrittenEnvironments,
60
+ getComponentSystemEnvironments,
61
+ fillBlockletConfigs,
62
+ getRuntimeEnvironments,
63
+ } = require('./blocklet/environment');
64
+ const {
65
+ getConfigFromPreferences,
66
+ getAppConfigsFromComponent,
67
+ getConfigsFromInput,
68
+ removeAppConfigsFromComponent,
69
+ getPackComponent,
70
+ getPackConfig,
71
+ copyPackImages,
72
+ } = require('./blocklet/config-manager');
73
+ const { mergeMeta, getUpdateMetaList, getFixedBundleSource, getBlockletStatus } = require('./blocklet/meta-utils');
74
+ const { updateBlockletFallbackLogo, ensureAppLogo } = require('./blocklet/logo');
75
+ const {
76
+ createDataArchive,
77
+ isBlockletAppSkUsed,
78
+ isRotatingAppSk,
79
+ getBlockletURLForLauncher,
80
+ getBlockletDidDomainList,
81
+ getComponentNamesWithVersion,
82
+ isDevelopmentMode,
83
+ getHookArgs,
84
+ } = require('./blocklet/app-utils');
85
+ const { ensureBlockletExpanded, getBlocklet } = require('./blocklet/blocklet-loader');
86
+ const { ensureAppPortsNotOccupied } = require('./blocklet/port-manager');
231
87
 
232
88
  /**
233
89
  * get blocklet engine info, default is node
@@ -236,2864 +92,41 @@ const getActualListeningPort = async (processId, blocklet) => {
236
92
  */
237
93
  const getBlockletEngineNameByPlatform = (meta) => getBlockletEngine(meta).interpreter;
238
94
 
239
- const startLock = new DBCache(() => ({
240
- ...getAbtNodeRedisAndSQLiteUrl(),
241
- prefix: 'blocklet-start-locks2',
242
- ttl: 1000 * 60 * 3,
243
- }));
244
-
245
- // Lock for port assignment to prevent race conditions in multi-process environment
246
- const portAssignLock = new DBCache(() => ({
247
- ...getAbtNodeRedisAndSQLiteUrl(),
248
- prefix: 'blocklet-port-assign-lock',
249
- ttl: 1000 * 30, // 30 seconds timeout
250
- }));
251
-
252
- const blockletCache = new DBCache(() => ({
253
- prefix: 'blocklet-state',
254
- ttl: BLOCKLET_CACHE_TTL,
255
- ...getAbtNodeRedisAndSQLiteUrl(),
256
- }));
257
-
258
- const getVersionScope = (meta) => {
259
- if (meta.dist?.integrity) {
260
- const safeHash = meta.dist.integrity
261
- .replace('sha512-', '')
262
- .slice(0, 8)
263
- .replace(/[^a-zA-Z0-9]/g, '');
264
- return `${meta.version}-${safeHash}`;
265
- }
266
-
267
- return meta.version;
268
- };
269
-
270
- const deleteBlockletCache = async (did) => {
271
- await blockletCache.del(did);
272
- };
273
-
274
- const noop = () => {
275
- //
276
- };
277
- const noopAsync = async () => {
278
- //
279
- };
280
-
281
- const statusMap = {
282
- online: BlockletStatus.running,
283
- launching: BlockletStatus.starting,
284
- errored: BlockletStatus.error,
285
- stopping: BlockletStatus.stopping,
286
- stopped: BlockletStatus.stopped,
287
- 'waiting restart': BlockletStatus.restarting,
288
- };
289
-
290
- const PRIVATE_NODE_ENVS = [
291
- 'ABT_NODE_UPDATER_PORT',
292
- 'ABT_NODE_SESSION_TTL',
293
- 'ABT_NODE_ROUTER_PROVIDER',
294
- 'ABT_NODE_DATA_DIR',
295
- 'ABT_NODE_TOKEN_SECRET',
296
- 'ABT_NODE_SK',
297
- 'ABT_NODE_SESSION_SECRET',
298
- 'ABT_NODE_BASE_URL',
299
- 'ABT_NODE_LOG_LEVEL',
300
- 'ABT_NODE_LOG_DIR',
301
- // in /core/cli/bin/blocklet.js
302
- 'CLI_MODE',
303
- 'ABT_NODE_HOME',
304
- 'PM2_HOME',
305
- 'ABT_NODE_CONFIG_FILE',
306
- ];
307
-
308
- /**
309
- * @returns { dataDir, logsDir, cacheDir, appDir }
310
- * dataDir: dataDirs.data/name (root component) or dataDirs.data/name/childName (child component)
311
- * logsDir: dataDirs.log/name
312
- * cacheDir: dataDirs.cache/name (root component) or dataDirs.cache/name/childName (child component)
313
- * appDir: component bundle dir
314
- */
315
- const getComponentDirs = (component, { dataDirs, ensure = false, ancestors = [] } = {}) => {
316
- const componentName = getComponentName(component, ancestors);
317
-
318
- const logsDir = path.join(dataDirs.logs, componentName);
319
- const dataDir = path.join(dataDirs.data, componentName);
320
- const cacheDir = path.join(dataDirs.cache, componentName);
321
-
322
- let appDir = null;
323
- if (component.source === BlockletSource.local) {
324
- appDir = component.deployedFrom;
325
- } else {
326
- // eslint-disable-next-line no-use-before-define
327
- appDir = getBundleDir(dataDirs.blocklets, component.meta);
328
- }
329
-
330
- if (!appDir) {
331
- throw new Error('Can not determine blocklet directory, maybe invalid deployment from local blocklets');
332
- }
333
-
334
- if (ensure) {
335
- try {
336
- fs.mkdirSync(dataDir, { recursive: true });
337
- fs.mkdirSync(path.join(dataDir, PROJECT.DIR), { recursive: true });
338
- fs.mkdirSync(logsDir, { recursive: true });
339
- fs.mkdirSync(cacheDir, { recursive: true });
340
- fs.mkdirSync(appDir, { recursive: true }); // prevent getDiskInfo failed from custom blocklet
341
- } catch (err) {
342
- logger.error('make blocklet dir failed', { error: err });
343
- }
344
- }
345
-
346
- return { dataDir, logsDir, cacheDir, appDir };
347
- };
348
-
349
- /**
350
- * @param component {import('@blocklet/server-js').ComponentState & { environmentObj: {[key: string]: string } } }
351
- * @returns {{cwd, script, args, environmentObj, interpreter, interpreterArgs}: { args: []}}
352
- * @return {*}
353
- */
354
- const getComponentStartEngine = (component, { e2eMode = false } = {}) => {
355
- if (!hasStartEngine(component.meta)) {
356
- return {};
357
- }
358
-
359
- const { appDir } = component.env;
360
-
361
- const cwd = appDir;
362
-
363
- // get app dirs
364
- const { group } = component.meta;
365
-
366
- let startFromDevEntry = '';
367
- if (component.mode === BLOCKLET_MODES.DEVELOPMENT && component.meta.scripts) {
368
- startFromDevEntry = component.meta.scripts.dev;
369
- if (e2eMode && component.meta.scripts.e2eDev) {
370
- startFromDevEntry = component.meta.scripts.e2eDev;
371
- }
372
- }
373
-
374
- const blockletEngineInfo = getBlockletEngine(component.meta);
375
- if (blockletEngineInfo.interpreter === 'blocklet') {
376
- return {};
377
- }
378
-
379
- let script = null;
380
- let interpreter;
381
- let interpreterArgs = [];
382
- const environmentObj = {};
383
- let args = [];
384
-
385
- if (startFromDevEntry) {
386
- script = startFromDevEntry;
387
- } else if (group === 'dapp') {
388
- script = blockletEngineInfo.source || BLOCKLET_ENTRY_FILE;
389
- args = blockletEngineInfo.args || [];
390
- }
391
-
392
- if (component.mode !== BLOCKLET_MODES.DEVELOPMENT) {
393
- const engine = getEngine(blockletEngineInfo.interpreter);
394
- interpreter = engine.interpreter === 'node' ? undefined : engine.interpreter;
395
- interpreterArgs = interpreterArgs.concat(engine.args ? [engine.args] : []);
396
- }
397
-
398
- return {
399
- cwd,
400
- script,
401
- args,
402
- environmentObj,
403
- interpreter,
404
- interpreterArgs: interpreterArgs.join(' ').trim(),
405
- };
406
- };
407
-
408
- const getBlockletConfigObj = (blocklet, { excludeSecure } = {}) => {
409
- const obj = (blocklet?.configs || [])
410
- .filter((x) => {
411
- if (excludeSecure) {
412
- return !x.secure;
413
- }
414
- return true;
415
- })
416
- .reduce((acc, x) => {
417
- acc[x.key] = templateReplace(x.value, blocklet);
418
- return acc;
419
- }, {});
420
-
421
- return obj;
422
- };
423
-
424
- const getAppSystemEnvironments = (blocklet, nodeInfo, dataDirs) => {
425
- const { did, name, title, description } = blocklet.meta;
426
- const keys = Object.keys(BLOCKLET_CONFIGURABLE_KEY);
427
- const result = getBlockletInfo(
428
- {
429
- meta: blocklet.meta,
430
- environments: keys.map((key) => ({ key, value: blocklet.configObj[key] })).filter((x) => x.value),
431
- },
432
- nodeInfo.sk
433
- );
434
-
435
- const { wallet } = result;
436
- const appSk = toHex(wallet.secretKey);
437
- const appPk = toHex(wallet.publicKey);
438
-
439
- const appId = wallet.address;
440
- const appName = title || name || result.name;
441
- const appDescription = description || result.description;
442
-
443
- const isMigrated = Array.isArray(blocklet.migratedFrom) && blocklet.migratedFrom.length > 0;
444
- const appPid = blocklet.appPid || appId;
445
- const appPsk = toHex(isMigrated ? blocklet.migratedFrom[0].appSk : appSk);
446
-
447
- /* 获取 did domain 方式:
448
- * 1. 先从 site 里读
449
- * 2. 如果没有,再拼接
450
- */
451
-
452
- const pidDomain = getDidDomainForBlocklet({ did: appPid, didDomain: nodeInfo.didDomain });
453
- const domainAliases = get(blocklet, 'site.domainAliases') || [];
454
-
455
- let didDomain = domainAliases.find((item) => toLower(item.value) === toLower(pidDomain));
456
-
457
- if (!didDomain) {
458
- didDomain = domainAliases.find(
459
- (item) => item.value.endsWith(nodeInfo.didDomain) || item.value.endsWith('did.staging.arcblock.io') // did.staging.arcblock.io 是旧 did domain, 但主要存在于比较旧的节点中, 需要做兼容
460
- );
461
- }
462
-
463
- const appUrl = didDomain ? prettyURL(didDomain.value, true) : `https://${pidDomain}`;
464
-
465
- return {
466
- BLOCKLET_DID: did, // BLOCKLET_DID is always same as BLOCKLET_APP_PID in structV2 application
467
- BLOCKLET_APP_PK: appPk,
468
- BLOCKLET_APP_SK: appSk,
469
- BLOCKLET_APP_ID: appId,
470
- BLOCKLET_APP_PSK: appPsk, // permanent sk even the blocklet has been migrated
471
- BLOCKLET_APP_PID: appPid, // permanent did even the blocklet has been migrated
472
- BLOCKLET_APP_NAME: appName,
473
- BLOCKLET_APP_NAME_SLUG: urlPathFriendly(slugify(appName)),
474
- BLOCKLET_APP_DESCRIPTION: appDescription,
475
- BLOCKLET_APP_URL: appUrl,
476
- BLOCKLET_APP_DATA_DIR: path.join(dataDirs.data, blocklet.meta.name),
477
- BLOCKLET_APP_TENANT_MODE: result.tenantMode || BLOCKLET_TENANT_MODES.SINGLE,
478
- BLOCKLET_APP_SALT: blocklet.settings?.session?.salt || '',
479
- };
480
- };
481
-
482
- const getAppOverwrittenEnvironments = (blocklet, nodeInfo) => {
483
- const result = {};
484
- if (!blocklet || !blocklet.configObj) {
485
- return result;
486
- }
487
-
488
- Object.keys(BLOCKLET_CONFIGURABLE_KEY).forEach((x) => {
489
- if (!blocklet.configObj[x]) {
490
- return;
491
- }
492
-
493
- result[x] = blocklet.configObj[x];
494
- });
495
-
496
- const keys = ['BLOCKLET_APP_SK', 'BLOCKLET_APP_CHAIN_TYPE', 'BLOCKLET_WALLET_TYPE'];
497
- const isAppDidRewritten = keys.some((key) => blocklet.configObj[key]);
498
- if (!isAppDidRewritten) {
499
- return result;
500
- }
501
-
502
- // We use user configuration here without any validation since the validation is done during update phase
503
- const { wallet } = getBlockletInfo(
504
- {
505
- meta: blocklet.meta,
506
- environments: keys.map((key) => ({ key, value: blocklet.configObj[key] })).filter((x) => x.value),
507
- },
508
- nodeInfo.sk
509
- );
510
- result.BLOCKLET_APP_ID = wallet.address;
511
-
512
- return result;
513
- };
514
-
515
- const getComponentSystemEnvironments = (blocklet) => {
516
- const { port, ports } = blocklet;
517
- const portEnvironments = {};
518
- if (port) {
519
- portEnvironments[BLOCKLET_DEFAULT_PORT_NAME] = port;
520
- }
521
-
522
- if (ports) {
523
- Object.assign(portEnvironments, ports);
524
- }
525
-
526
- return {
527
- BLOCKLET_REAL_DID: blocklet.env.id, // <appDid>/componentDid> e.g. xxxxx/xxxxx
528
- BLOCKLET_REAL_NAME: blocklet.env.name,
529
- BLOCKLET_COMPONENT_DID: blocklet.meta.did, // component meta did e.g. xxxxxx
530
- BLOCKLET_COMPONENT_VERSION: blocklet.meta.version,
531
- BLOCKLET_DATA_DIR: blocklet.env.dataDir,
532
- BLOCKLET_LOG_DIR: blocklet.env.logsDir,
533
- BLOCKLET_CACHE_DIR: blocklet.env.cacheDir,
534
- BLOCKLET_APP_DIR: blocklet.env.appDir,
535
- ...portEnvironments,
536
- };
537
- };
538
-
539
- /**
540
- * set 'configs', configObj', 'environmentObj' to blocklet TODO
541
- * @param {*} blocklet
542
- * @param {*} configs
543
- * @param {*} options - Optional: { rootBlocklet, nodeInfo, dataDirs }
544
- */
545
- const fillBlockletConfigs = (blocklet, configs, options = {}) => {
546
- blocklet.configs = configs || [];
547
- blocklet.configObj = getBlockletConfigObj(blocklet);
548
- blocklet.environments = blocklet.environments || [];
549
- blocklet.environmentObj = blocklet.environments.reduce((acc, x) => {
550
- acc[x.key] = templateReplace(x.value, blocklet);
551
- return acc;
552
- }, {});
553
-
554
- // After migration: ensure all component system environments are set from blocklet.env if available
555
- // This ensures children loaded from blocklet_children table have all required env vars in environmentObj
556
- if (blocklet.env) {
557
- try {
558
- const componentSystemEnvs = getComponentSystemEnvironments(blocklet);
559
- // Only set env vars that are not already set
560
- Object.entries(componentSystemEnvs).forEach(([key, value]) => {
561
- if (value !== undefined && value !== null && !blocklet.environmentObj[key]) {
562
- blocklet.environmentObj[key] = value;
563
- }
564
- });
565
- } catch (error) {
566
- // If getting component system environments fails, log warning but continue
567
- logger.warn('fillBlockletConfigs: failed to get component system environments', {
568
- blockletDid: blocklet.meta?.did,
569
- error: error.message,
570
- });
571
- }
572
- }
573
-
574
- // For children: also set app-level environment variables from root blocklet
575
- // This ensures children have app-level env vars like BLOCKLET_APP_ID
576
- const { rootBlocklet, nodeInfo, dataDirs } = options;
577
- if (rootBlocklet && nodeInfo && dataDirs && blocklet !== rootBlocklet) {
578
- try {
579
- const appSystemEnvs = getAppSystemEnvironments(rootBlocklet, nodeInfo, dataDirs);
580
- const appOverwrittenEnvs = getAppOverwrittenEnvironments(rootBlocklet, nodeInfo);
581
- // Only set env vars that are not already set
582
- Object.entries({ ...appSystemEnvs, ...appOverwrittenEnvs }).forEach(([key, value]) => {
583
- if (value !== undefined && value !== null && !blocklet.environmentObj[key]) {
584
- blocklet.environmentObj[key] = value;
585
- }
586
- });
587
- } catch (error) {
588
- // If getting app system environments fails, log warning but continue
589
- logger.warn('fillBlockletConfigs: failed to get app system environments', {
590
- blockletDid: blocklet.meta?.did,
591
- rootDid: rootBlocklet.meta?.did,
592
- error: error.message,
593
- });
594
- }
595
- }
596
- };
597
-
598
- const ensureBlockletExpanded = async (_meta, appDir) => {
599
- const bundlePath = path.join(appDir, BLOCKLET_BUNDLE_FILE);
600
- if (fs.existsSync(bundlePath)) {
601
- try {
602
- const nodeModulesPath = path.join(appDir, 'node_modules');
603
- if (fs.existsSync(nodeModulesPath)) {
604
- await fs.remove(nodeModulesPath);
605
- }
606
- await expandBundle(bundlePath, appDir);
607
- await fs.remove(bundlePath);
608
- } catch (err) {
609
- throw new Error(`Failed to expand blocklet bundle: ${err.message}`);
610
- }
611
- }
612
- };
613
-
614
- const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors, isGreen = false) => {
615
- const root = (ancestors || [])[0] || blocklet;
616
-
617
- const initialized = root?.settings?.initialized;
618
-
619
- const environmentObj = { ...(blocklet.environmentObj || {}) };
620
- if (isGreen && blocklet.greenPorts) {
621
- Object.entries(blocklet.greenPorts).forEach(([key, value]) => {
622
- if (!value) {
623
- return;
624
- }
625
- environmentObj[key] = value;
626
- if (key === BLOCKLET_DEFAULT_PORT_NAME || key === 'BLOCKLET_PORT') {
627
- environmentObj[BLOCKLET_DEFAULT_PORT_NAME] = value;
628
- }
629
- });
630
- }
631
-
632
- // pm2 will force inject env variables of daemon process to blocklet process
633
- // we can only rewrite these private env variables to empty
634
- const safeNodeEnvironments = PRIVATE_NODE_ENVS.reduce((o, x) => {
635
- o[x] = '';
636
- return o;
637
- }, {});
638
-
639
- // get devEnvironments, when blocklet is in dev mode
640
- const devEnvironments =
641
- blocklet.mode === BLOCKLET_MODES.DEVELOPMENT
642
- ? {
643
- BLOCKLET_DEV_MOUNT_POINT: blocklet?.mountPoint || '',
644
- }
645
- : {};
646
-
647
- // BLOCKLET_DEV_PORT should NOT in components of production mode
648
- if (process.env.BLOCKLET_DEV_PORT) {
649
- devEnvironments.BLOCKLET_DEV_PORT =
650
- blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? process.env.BLOCKLET_DEV_PORT : '';
651
- }
652
-
653
- const ports = {};
654
- forEachBlockletSync(root, (x) => {
655
- const webInterface = findWebInterface(x);
656
- const envObj = x.meta?.did === blocklet.meta?.did ? environmentObj : x.environmentObj;
657
- if (webInterface && envObj?.[webInterface.port]) {
658
- ports[envObj.BLOCKLET_REAL_NAME] = envObj[webInterface.port];
659
- }
660
- });
661
-
662
- const componentsInternalInfo = getComponentsInternalInfo(root);
663
-
664
- // use index 1 as the path to derive deterministic encryption key for blocklet
665
- const tmp = get(nodeEnvironments, 'ABT_NODE_SK')
666
- ? getBlockletWallet(blocklet.meta.did, nodeEnvironments.ABT_NODE_SK, undefined, 1)
667
- : null;
668
-
669
- // For Access Key authentication, components should use root app's wallet
670
- // This ensures consistent accessKeyId across parent and child components
671
- const accessWallet = get(nodeEnvironments, 'ABT_NODE_SK')
672
- ? getBlockletWallet(root.appDid || root.meta.did, nodeEnvironments.ABT_NODE_SK, undefined, 2)
673
- : null;
674
-
675
- const BLOCKLET_APP_IDS = getBlockletAppIdList(root).join(',');
676
-
677
- const componentApiKey = getComponentApiKey({
678
- serverSk: nodeEnvironments.ABT_NODE_SK,
679
- app: root,
680
- component: blocklet,
681
- });
682
-
683
- const blockletInfo = getBlockletInfo(blocklet, nodeEnvironments.ABT_NODE_SK, { returnWallet: true });
684
-
685
- const rootBlockletInfo =
686
- blocklet === root ? blockletInfo : getBlockletInfo(root, nodeEnvironments.ABT_NODE_SK, { returnWallet: true });
687
-
688
- const { wallet } = rootBlockletInfo;
689
- const appSk = toHex(wallet.secretKey);
690
- const appPk = toHex(wallet.publicKey);
691
-
692
- const ethWallet = fromSecretKey(appSk.slice(0, 66), 'ethereum');
693
- const ethPk = toHex(ethWallet.publicKey);
694
-
695
- const isMigrated = Array.isArray(root.migratedFrom) && root.migratedFrom.length > 0;
696
- const appPsk = toHex(isMigrated ? root.migratedFrom[0].appSk : appSk);
697
-
698
- // Calculate permanent public key (PPK)
699
- const appPpk = isMigrated ? toHex(fromSecretKey(appPsk, wallet.type).publicKey) : appPk;
700
- const ethPermanentWallet = fromSecretKey(appPsk.slice(0, 66), 'ethereum');
701
- const appPpkEth = toHex(ethPermanentWallet.publicKey);
702
-
703
- const env = {
704
- ...blocklet.configObj,
705
- ...getSharedConfigObj((ancestors || [])[0], blocklet, true),
706
- ...environmentObj,
707
- ...devEnvironments,
708
- BLOCKLET_MOUNT_POINTS: JSON.stringify(componentsInternalInfo),
709
- BLOCKLET_MODE: blocklet.mode || BLOCKLET_MODES.PRODUCTION,
710
- BLOCKLET_APP_EK: tmp?.secretKey,
711
- // for login token authentication
712
- BLOCKLET_SESSION_SECRET: rootBlockletInfo.secret,
713
- BLOCKLET_APP_VERSION: root.meta.version,
714
- BLOCKLET_APP_IDS,
715
- BLOCKLET_COMPONENT_API_KEY: componentApiKey,
716
- BLOCKLET_APP_ASK: accessWallet?.secretKey,
717
- ...nodeEnvironments,
718
- ...safeNodeEnvironments,
719
- BLOCKLET_APP_PPK: appPpk, // permanent pk corresponding to PSK
720
- BLOCKLET_APP_PPK_ETH: appPpkEth, // permanent pk corresponding to PSK for ethereum
721
- BLOCKLET_APP_PK: appPk,
722
- BLOCKLET_APP_PK_ETH: ethPk,
723
- };
724
-
725
- const aigne = get(root, 'settings.aigne', {});
726
- const salt = root.meta.did || AIGNE_CONFIG_ENCRYPT_SALT;
727
- if (!isNil(aigne) && aigne.provider) {
728
- const { key, accessKeyId, secretAccessKey, provider } = aigne;
729
- const selectedModel = !aigne.model || aigne.model === 'auto' ? undefined : aigne.model;
730
- env.BLOCKLET_AIGNE_API_MODEL = selectedModel;
731
- env.BLOCKLET_AIGNE_API_PROVIDER = aigne.provider;
732
- const credential = {
733
- apiKey: key ? decrypt(key, salt, '') : key || '',
734
- accessKeyId: accessKeyId && provider === 'bedrock' ? decrypt(accessKeyId, salt, '') : accessKeyId || '',
735
- secretAccessKey:
736
- secretAccessKey && provider === 'bedrock' ? decrypt(secretAccessKey, salt, '') : secretAccessKey || '',
737
- };
738
- env.BLOCKLET_AIGNE_API_CREDENTIAL = JSON.stringify(credential);
739
- env.BLOCKLET_AIGNE_API_URL = aigne.url || '';
740
- }
741
-
742
- if (root?.environmentObj?.BLOCKLET_APP_DATA_DIR) {
743
- env.BLOCKLET_APP_SHARE_DIR = path.join(root.environmentObj.BLOCKLET_APP_DATA_DIR, '.share');
744
- env.BLOCKLET_SHARE_DIR = path.join(root.environmentObj.BLOCKLET_APP_DATA_DIR, '.share', blocklet.meta.did);
745
- if (!fs.existsSync(env.BLOCKLET_APP_SHARE_DIR) && process.env.ABT_NODE_DATA_DIR) {
746
- fs.mkdirSync(env.BLOCKLET_APP_SHARE_DIR, { recursive: true });
747
- }
748
- }
749
-
750
- if (initialized) {
751
- env.initialized = initialized;
752
- }
753
-
754
- if (isGreen && blocklet.greenPorts?.[BLOCKLET_DEFAULT_PORT_NAME]) {
755
- env[BLOCKLET_DEFAULT_PORT_NAME] = blocklet.greenPorts[BLOCKLET_DEFAULT_PORT_NAME];
756
- }
757
-
758
- // ensure all envs are literals and do not contain line breaks
759
- Object.keys(env).forEach((key) => {
760
- env[key] = formatEnv(env[key]);
761
- });
762
-
763
- return env;
764
- };
765
-
766
- const isUsefulError = (err) =>
767
- err &&
768
- err.message !== 'process or namespace not found' &&
769
- !/id unknown/.test(err.message) &&
770
- !/^Process \d+ not found$/.test(err.message);
771
-
772
- const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately, componentDids } = {}) => {
773
- let minConsecutiveTime = 3000;
774
- if (process.env.NODE_ENV === 'test' && process.env.ABT_NODE_TEST_MIN_CONSECUTIVE_TIME !== undefined) {
775
- minConsecutiveTime = +process.env.ABT_NODE_TEST_MIN_CONSECUTIVE_TIME;
776
- } else if (checkHealthImmediately) {
777
- minConsecutiveTime = 3000;
778
- }
779
-
780
- if (process.env.BLOCKLET_START_TIMEOUT) {
781
- return {
782
- startTimeout: +process.env.BLOCKLET_START_TIMEOUT * 1000,
783
- minConsecutiveTime,
784
- };
785
- }
786
- if (blocklet.mode === BLOCKLET_MODES.DEVELOPMENT) {
787
- return {
788
- startTimeout: 3600 * 1000,
789
- minConsecutiveTime: 3000,
790
- };
791
- }
792
-
793
- const children = componentDids?.length
794
- ? blocklet.children.filter((child) => componentDids.includes(child.meta.did))
795
- : blocklet.children;
796
-
797
- // Let's wait for at least 1 minute for the blocklet to go live
798
- let startTimeout =
799
- Math.max(
800
- get(blocklet, 'meta.timeout.start', 60),
801
- ...(children || []).map((child) => child.meta?.timeout?.start || 0)
802
- ) * 1000;
803
-
804
- if (process.env.NODE_ENV === 'test') {
805
- startTimeout = 10 * 1000;
806
- }
807
-
808
- return {
809
- startTimeout,
810
- minConsecutiveTime,
811
- };
812
- };
813
-
814
- /**
815
- * Start all precesses of a blocklet
816
- * @param {*} blocklet should contain env props
817
- */
818
- const startBlockletProcess = async (
819
- blocklet,
820
- {
821
- preFlight = noop,
822
- preStart = noop,
823
- postStart = noopAsync,
824
- nodeEnvironments,
825
- nodeInfo,
826
- e2eMode,
827
- skippedProcessIds = [],
828
- componentDids,
829
- configSynchronizer,
830
- onlyStart = false,
831
- isGreen = false,
832
- } = {}
833
- ) => {
834
- if (!blocklet) {
835
- throw new Error('blocklet should not be empty');
836
- }
837
-
838
- const dockerNetworkName = parseDockerName(blocklet?.meta?.did, 'docker-network');
839
- await createDockerNetwork(dockerNetworkName);
840
-
841
- blocklet.children.forEach((component) => {
842
- if (!componentDids.includes(component.meta.did)) {
843
- return;
844
- }
845
- for (const envItem of component.environments) {
846
- const envKey = envItem.key || envItem.name;
847
- if (envKey !== 'BLOCKLET_PORT') {
848
- continue;
849
- }
850
- if (isGreen) {
851
- if (component.greenPorts?.BLOCKLET_PORT) {
852
- envItem.value = component.greenPorts.BLOCKLET_PORT;
853
- }
854
- } else if (component.ports?.BLOCKLET_PORT) {
855
- envItem.value = component.ports.BLOCKLET_PORT;
856
- }
857
- }
858
- });
859
-
860
- const startBlockletTask = async (b, { ancestors }) => {
861
- // 需要在在这里传入字符串类型,否则进程中如法转化成 Date 对象
862
- const now = `${new Date()}`;
863
- if (b.meta.group === BlockletGroup.gateway) {
864
- return;
865
- }
866
-
867
- if (!hasStartEngine(b.meta)) {
868
- return;
869
- }
870
-
871
- const { processId, logsDir, appDir } = b.env;
872
-
873
- if (skippedProcessIds.includes(processId)) {
874
- logger.info('skip start skipped process', { processId });
875
- return;
876
- }
877
-
878
- // eslint-disable-next-line no-use-before-define
879
- if (shouldSkipComponent(b.meta.did, componentDids)) {
880
- logger.info('skip start process not selected', { processId });
881
- return;
882
- }
883
-
884
- if (b.mode !== BLOCKLET_MODES.DEVELOPMENT) {
885
- validateBlockletEntry(appDir, b.meta);
886
- }
887
-
888
- const { cwd, script, args, environmentObj, interpreter, interpreterArgs } = getComponentStartEngine(b, {
889
- e2eMode,
890
- });
891
- if (!script) {
892
- logger.info('skip start process without script', { processId });
893
- return;
894
- }
895
-
896
- // get env
897
- const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors, isGreen);
898
- const startedAt = Date.now();
899
-
900
- await installExternalDependencies({ appDir: env?.BLOCKLET_APP_DIR, nodeInfo });
901
- await preFlight(b, { env: { ...env } });
902
- await preStart(b, { env: { ...env } });
903
-
904
- // kill process if port is occupied
905
- try {
906
- const { ports, greenPorts } = b;
907
- await killProcessOccupiedPorts({
908
- ports: isGreen ? greenPorts : ports,
909
- pm2ProcessId: processId,
910
- printError: logger.error.bind(logger),
911
- });
912
- } catch (error) {
913
- logger.error('Failed to killProcessOccupiedPorts', { error });
914
- }
915
-
916
- // start process
917
- const maxMemoryRestart = get(nodeInfo, 'runtimeConfig.blockletMaxMemoryLimit', BLOCKLET_MAX_MEM_LIMIT_IN_MB);
918
- const processIdName = isGreen ? `${processId}-green` : processId;
919
- /**
920
- * @type {pm2.StartOptions}
921
- */
922
- const options = {
923
- namespace: 'blocklets',
924
- name: processIdName,
925
- cwd,
926
- log_date_format: '(YYYY-MM-DD HH:mm:ss)',
927
- output: path.join(logsDir, 'output.log'),
928
- error: path.join(logsDir, 'error.log'),
929
- // wait_ready: process.env.NODE_ENV !== 'test',
930
- wait_ready: false,
931
- listen_timeout: 3000,
932
- max_memory_restart: `${maxMemoryRestart}M`,
933
- max_restarts: b.mode === BLOCKLET_MODES.DEVELOPMENT ? 0 : 3,
934
- min_uptime: 10_000,
935
- exp_backoff_restart_delay: 300,
936
- env: omit(
937
- {
938
- ...environmentObj,
939
- ...env,
940
- NODE_ENV: 'production',
941
- BLOCKLET_START_AT: now,
942
- NODE_OPTIONS: await getSecurityNodeOptions(b, nodeInfo.enableFileSystemIsolation),
943
- },
944
- // should only inject appSk and appPsk to the blocklet environment when unsafe mode enabled
945
- ['1', 1].includes(env.UNSAFE_MODE) ? [] : ['BLOCKLET_APP_SK', 'BLOCKLET_APP_PSK']
946
- ),
947
- script,
948
- args,
949
- interpreter,
950
- interpreterArgs,
951
- };
952
-
953
- const clusterMode = get(b.meta, 'capabilities.clusterMode', false);
954
- if (clusterMode && b.mode !== BLOCKLET_MODES.DEVELOPMENT) {
955
- const clusterSize = Number(blocklet.configObj.BLOCKLET_CLUSTER_SIZE) || +process.env.ABT_NODE_MAX_CLUSTER_SIZE;
956
- options.execMode = 'cluster';
957
- options.mergeLogs = true;
958
- options.instances = Math.max(Math.min(os.cpus().length, clusterSize), 1);
959
- options.env.BLOCKLET_CLUSTER_SIZE = options.instances;
960
- if (options.instances !== clusterSize) {
961
- logger.warn(`Fallback cluster size to ${options.instances} for ${processId}, ignore custom ${clusterSize}`);
962
- }
963
- } else {
964
- delete options.env.BLOCKLET_CLUSTER_SIZE;
965
- }
966
-
967
- if (b.mode === BLOCKLET_MODES.DEVELOPMENT) {
968
- options.env.NODE_ENV = e2eMode ? 'e2e' : 'development';
969
- options.env.IS_E2E = e2eMode ? '1' : undefined;
970
- options.env.BROWSER = 'none';
971
- options.env.PORT = options.env[BLOCKLET_DEFAULT_PORT_NAME];
972
-
973
- if (process.platform === 'win32') {
974
- const [cmd, ...argList] = options.script.split(' ').filter(Boolean);
975
-
976
- if (!SCRIPT_ENGINES_WHITE_LIST.includes(cmd)) {
977
- throw new Error(`${cmd} script is not supported, ${SCRIPT_ENGINES_WHITE_LIST.join(', ')} are supported`);
978
- }
979
-
980
- const { stdout: nodejsBinPath } = shelljs.which('node');
981
-
982
- const cmdPath = path.join(path.dirname(nodejsBinPath), 'node_modules', cmd);
983
-
984
- const pkg = JSON.parse(fs.readFileSync(path.join(cmdPath, 'package.json'), 'utf8'));
985
- const cmdBinPath = pkg.bin[cmd];
986
-
987
- options.script = path.resolve(cmdPath, cmdBinPath);
988
- options.args = [...argList].join(' ');
989
- }
990
- }
991
-
992
- await configSynchronizer.syncComponentConfig(b.meta.did, blocklet.meta.did, {
993
- serverSk: nodeEnvironments.ABT_NODE_SK,
994
- });
995
-
996
- if (options.interpreter === 'bun') {
997
- options.exec_interpreter = await ensureBun();
998
- options.exec_mode = 'fork';
999
- delete options.instances;
1000
- delete options.mergeLogs;
1001
- }
1002
-
1003
- let nextOptions;
1004
- if (b.mode === BLOCKLET_MODES.DEVELOPMENT) {
1005
- nextOptions = options;
1006
- } else {
1007
- nextOptions = await parseDockerOptionsFromPm2({
1008
- options,
1009
- nodeInfo,
1010
- isExternal: isExternalBlocklet(b),
1011
- meta: b.meta,
1012
- ports: isGreen ? b.greenPorts : b.ports,
1013
- onlyStart,
1014
- dockerNetworkName,
1015
- rootBlocklet: blocklet,
1016
- });
1017
- }
1018
-
1019
- await fetchPm2({ ...nextOptions, pmx: false }, nodeEnvironments.ABT_NODE_SK);
1020
-
1021
- // eslint-disable-next-line no-use-before-define
1022
- const status = await getProcessState(processIdName);
1023
- if (status === BlockletStatus.error) {
1024
- throw new Error(`process ${processIdName} is not running within 3 seconds`);
1025
- }
1026
- logger.info('done start blocklet', { processId: processIdName, status, time: Date.now() - startedAt });
1027
-
1028
- if (nextOptions.env.connectInternalDockerNetwork) {
1029
- try {
1030
- await promiseSpawn(nextOptions.env.connectInternalDockerNetwork, { mute: true });
1031
- } catch (err) {
1032
- logger.warn('blocklet connect internal docker network failed', { processId: processIdName, error: err });
1033
- }
1034
- }
1035
-
1036
- // run hook
1037
- postStart(b, { env }).catch((err) => {
1038
- logger.error('blocklet post start failed', { processId: processIdName, error: err });
1039
- });
1040
- };
1041
-
1042
- await forEachBlocklet(
1043
- blocklet,
1044
- /**
1045
- *
1046
- * @param {import('@blocklet/server-js').BlockletState} b
1047
- * @param {*} param1
1048
- * @returns
1049
- */
1050
- async (b, { ancestors }) => {
1051
- const lockName = `${blocklet.meta.did}-${b.meta.did}`;
1052
-
1053
- // 如果锁存在,则跳过执行
1054
- if (!(await startLock.hasExpired(lockName))) {
1055
- return;
1056
- }
1057
- await startLock.acquire(lockName);
1058
-
1059
- try {
1060
- await startBlockletTask(b, { ancestors });
1061
- } finally {
1062
- startLock.releaseLock(lockName);
1063
- }
1064
- },
1065
- { parallel: true, concurrencyLimit: 3 }
1066
- );
1067
- };
1068
-
1069
95
  /**
1070
- * Stop all precesses of a blocklet
1071
- * @param {*} blocklet should contain env props
96
+ * Wrapper for startBlockletProcess - injects local dependencies
1072
97
  */
1073
- const stopBlockletProcess = (
1074
- blocklet,
1075
- { preStop = noop, skippedProcessIds = [], componentDids, isGreen = false, isStopGreenAndBlue = false } = {}
1076
- ) => {
1077
- // eslint-disable-next-line no-use-before-define
1078
- return deleteBlockletProcess(blocklet, {
1079
- preDelete: preStop,
1080
- skippedProcessIds,
1081
- componentDids,
1082
- isGreen,
1083
- isStopGreenAndBlue,
98
+ const _startBlockletProcess = (blocklet, options = {}) => {
99
+ // eslint-disable-next-line global-require
100
+ const processManager = require('./blocklet/process-manager');
101
+ return processManager.startBlockletProcess(blocklet, {
102
+ ...options,
103
+ getComponentStartEngine,
104
+ getRuntimeEnvironments,
1084
105
  });
1085
106
  };
1086
107
 
1087
- /**
1088
- * Delete all precesses of a blocklet
1089
- * @param {*} blocklet should contain env props
1090
- */
1091
- const deleteBlockletProcess = async (
1092
- blocklet,
1093
- { preDelete = noop, skippedProcessIds = [], componentDids, isGreen = false, isStopGreenAndBlue = false } = {}
1094
- ) => {
1095
- await forEachBlocklet(
1096
- blocklet,
1097
- async (b, { ancestors }) => {
1098
- // NOTICE: 如果不判断 group, 在 github action 中测试 disk.spec.js 时会报错, 但是在 mac 中跑测试不会报错
1099
- if (b.meta?.group === BlockletGroup.gateway) {
1100
- return;
1101
- }
1102
-
1103
- if (skippedProcessIds.includes(b.env.processId)) {
1104
- logger.info(`skip delete skipped process ${b.env.processId}`);
1105
- return;
1106
- }
1107
-
1108
- // eslint-disable-next-line no-use-before-define
1109
- if (shouldSkipComponent(b.meta?.did, componentDids)) {
1110
- logger.info(`skip delete process not selected: ${b.meta.did}`, { processId: b.env.processId });
1111
- return;
1112
- }
1113
-
1114
- if (!hasStartEngine(b.meta)) {
1115
- return;
1116
- }
1117
-
1118
- // Skip deleting static-server engine processes since they were never started
1119
- if (b.meta?.group === 'static') {
1120
- return;
1121
- }
1122
- const engine = getBlockletEngine(b.meta);
1123
- if (engine.interpreter === 'blocklet' && engine.source?.name === STATIC_SERVER_ENGINE_DID) {
1124
- return;
1125
- }
1126
-
1127
- await preDelete(b, { ancestors });
1128
- if (isStopGreenAndBlue) {
1129
- // eslint-disable-next-line no-use-before-define
1130
- await deleteProcess(`${b.env.processId}-green`);
1131
- // eslint-disable-next-line no-use-before-define
1132
- await deleteProcess(b.env.processId);
1133
- return;
1134
- }
1135
- const processId = isGreen ? `${b.env.processId}-green` : b.env.processId;
1136
- // eslint-disable-next-line no-use-before-define
1137
- await deleteProcess(processId);
1138
- },
1139
- { parallel: true }
1140
- );
1141
- };
1142
-
1143
- /**
1144
- * Reload all precesses of a blocklet
1145
- * @param {*} blocklet should contain env props
1146
- */
1147
- const reloadBlockletProcess = (blocklet, { componentDids } = {}) =>
1148
- forEachBlocklet(
1149
- blocklet,
1150
- async (b) => {
1151
- if (b.meta.group === BlockletGroup.gateway) {
1152
- return;
1153
- }
1154
-
1155
- // eslint-disable-next-line no-use-before-define
1156
- if (shouldSkipComponent(b.meta.did, componentDids)) {
1157
- logger.info('skip reload process', { processId: b.env.processId });
1158
- return;
1159
- }
1160
-
1161
- // Skip reloading static-server engine processes since they were never started
1162
- if (b.meta?.group === 'static') {
1163
- return;
1164
- }
1165
- const engine = getBlockletEngine(b.meta);
1166
- if (engine.interpreter === 'blocklet' && engine.source?.name === STATIC_SERVER_ENGINE_DID) {
1167
- return;
1168
- }
108
+ const {
109
+ stopBlockletProcess: _stopBlockletProcess,
110
+ deleteBlockletProcess: _deleteBlockletProcess,
111
+ reloadBlockletProcess: _reloadBlockletProcess,
112
+ } = require('./blocklet/process-manager');
1169
113
 
1170
- // eslint-disable-next-line no-use-before-define
1171
- await reloadProcess(b.env.processId);
1172
- logger.info('done reload process', { processId: b.env.processId });
1173
- },
1174
- { parallel: false }
1175
- );
114
+ const validateBlocklet = (blocklet) => _validateBlocklet(blocklet, getBlockletEngineNameByPlatform);
1176
115
 
1177
116
  /**
1178
- * @param {*} processId
1179
- * @returns {BlockletStatus}
117
+ * Wrapper for checkBlockletProcessHealthy - injects local dependency findInterfacePortByName
1180
118
  */
1181
- const getProcessState = async (processId) => {
1182
- // eslint-disable-next-line no-use-before-define
1183
- const info = await getProcessInfo(processId);
1184
- if (!statusMap[info.pm2_env.status]) {
1185
- logger.error('Cannot find the blocklet status for pm2 status mapping', {
1186
- pm2Status: info.pm2_env.status,
1187
- });
1188
-
1189
- return BlockletStatus.error;
1190
- }
1191
-
1192
- return statusMap[info.pm2_env.status];
1193
- };
1194
-
1195
- const getProcessInfo = (processId, { throwOnNotExist = true, timeout = 10_000 } = {}) =>
1196
- getPm2ProcessInfo(processId, { printError: logger.error.bind(logger), throwOnNotExist, timeout });
1197
-
1198
- const deleteProcess = (processId) => {
1199
- return new Promise((resolve, reject) => {
1200
- pm2.delete(processId, async (err) => {
1201
- if (isUsefulError(err)) {
1202
- logger.error('blocklet process delete failed', { processId, error: err });
1203
- return reject(err);
1204
- }
1205
- await dockerRemoveByName(processId);
1206
- return resolve(processId);
1207
- });
1208
- });
1209
- };
1210
-
1211
- const reloadProcess = (processId) =>
1212
- new Promise((resolve, reject) => {
1213
- pm2.reload(processId, (err) => {
1214
- if (err) {
1215
- if (isUsefulError(err)) {
1216
- logger.error('blocklet reload failed', { processId, error: err });
1217
- }
1218
- return reject(err);
1219
- }
1220
- return resolve(processId);
1221
- });
1222
- });
1223
-
1224
- const validateBlocklet = (blocklet) =>
1225
- forEachComponentV2(blocklet, (b) => {
1226
- isRequirementsSatisfied(b.meta.requirements);
1227
- validateEngine(getBlockletEngineNameByPlatform(b.meta));
1228
- });
1229
-
1230
- const validateBlockletChainInfo = (blocklet) => {
1231
- const chainInfo = getChainInfo({
1232
- CHAIN_TYPE: blocklet.configObj[BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_CHAIN_TYPE],
1233
- CHAIN_ID: blocklet.configObj[BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_CHAIN_ID],
1234
- CHAIN_HOST: blocklet.configObj[BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_CHAIN_HOST],
119
+ const checkBlockletProcessHealthy = (blocklet, options = {}) =>
120
+ _checkBlockletProcessHealthy(blocklet, {
121
+ ...options,
122
+ findInterfacePortByName,
1235
123
  });
1236
124
 
1237
- const { error } = chainInfoSchema.validate(chainInfo);
1238
- if (error) {
1239
- throw error;
1240
- }
1241
-
1242
- return chainInfo;
1243
- };
1244
-
1245
- const _checkProcessHealthy = async (
1246
- blocklet,
1247
- { minConsecutiveTime, timeout, logToTerminal, isGreen = false, appDid }
1248
- ) => {
1249
- const { meta, ports, greenPorts, env } = blocklet;
1250
- const { name } = meta;
1251
- const processId = isGreen ? `${env.processId}-green` : env.processId;
1252
-
1253
- const webInterface = (meta.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
1254
- const dockerInterface = (meta.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_DOCKER);
1255
-
1256
- if (!webInterface && !dockerInterface) {
1257
- // TODO: how do we check healthy for service interfaces
1258
- throw new Error(`Blocklet ${name} does not have any web interface`);
1259
- }
1260
-
1261
- try {
1262
- // ensure pm2 status is 'online'
1263
- const getStatus = async () => {
1264
- try {
1265
- const info = await getProcessInfo(processId, { timeout: 3_000 });
1266
- return { status: info.pm2_env.status, envPort: info.pm2_env.BLOCKLET_PORT };
1267
- } catch (err) {
1268
- logger.error('blocklet checkStart error', { appDid, error: err, processId, name });
1269
- return { status: '', envPort: null };
1270
- }
1271
- };
1272
-
1273
- // eslint-disable-next-line prefer-const
1274
- let { status, envPort } = await getStatus();
1275
-
1276
- for (let i = 0; i < 20 && status !== 'online'; i++) {
1277
- const t = process.env.NODE_ENV !== 'test' ? 500 : 30;
1278
- await sleep(t);
1279
- ({ status, envPort } = await getStatus());
1280
- }
1281
-
1282
- if (status !== 'online') {
1283
- throw new Error('process not start within 10s');
1284
- }
1285
-
1286
- // ⚠️ 关键修复:优先从进程实际监听的端口获取
1287
- // 对于 Docker 容器,从 Docker 端口映射获取实际端口
1288
- // 这样可以避免端口刷新后,健康检查使用错误的端口
1289
- const actualPort = await getActualListeningPort(processId, blocklet);
1290
-
1291
- // 端口优先级:实际端口 > pm2 环境变量端口 > 数据库中的端口
1292
- const port =
1293
- actualPort ||
1294
- envPort ||
1295
- findInterfacePortByName({ meta, ports: isGreen ? greenPorts : ports }, (webInterface || dockerInterface).name);
1296
-
1297
- if (logToTerminal) {
1298
- // eslint-disable-next-line no-console
1299
- logger.info(
1300
- // eslint-disable-next-line no-nested-ternary
1301
- `Checking endpoint healthy for ${meta.title}, port: ${port}${actualPort ? ' (actual)' : envPort ? ' (from pm2 env)' : ' (from db)'}, minConsecutiveTime: ${
1302
- minConsecutiveTime / 1000
1303
- }s, timeout: ${timeout / 1000}s`
1304
- );
1305
- }
1306
-
1307
- if (
1308
- actualPort &&
1309
- actualPort !== envPort &&
1310
- actualPort !==
1311
- (isGreen
1312
- ? greenPorts?.[webInterface?.port || dockerInterface?.port]
1313
- : ports?.[webInterface?.port || dockerInterface?.port])
1314
- ) {
1315
- logger.info('Port mismatch detected, using actual port for health check', {
1316
- processId,
1317
- appDid,
1318
- actualPort,
1319
- envPort,
1320
- dbPort: isGreen
1321
- ? greenPorts?.[webInterface?.port || dockerInterface?.port]
1322
- : ports?.[webInterface?.port || dockerInterface?.port],
1323
- });
1324
- }
1325
-
1326
- try {
1327
- await ensureEndpointHealthy({
1328
- port,
1329
- protocol: webInterface ? 'http' : 'tcp',
1330
- minConsecutiveTime,
1331
- timeout,
1332
- doConsecutiveCheck: blocklet.mode !== BLOCKLET_MODES.DEVELOPMENT,
1333
- waitTCP: !webInterface,
1334
- shouldAbort: async () => {
1335
- // Check if pm2 process exists and is online
1336
- try {
1337
- const info = await getProcessInfo(processId, { timeout: 3_000 });
1338
- const currentStatus = info.pm2_env.status;
1339
- if (currentStatus !== 'online') {
1340
- throw new Error(`pm2 process ${processId} status is ${currentStatus}, not online`);
1341
- }
1342
- } catch (err) {
1343
- // If process doesn't exist or has error, abort immediately
1344
- logger.error('pm2 process check failed in shouldAbort', { appDid, error: err, processId, name });
1345
- const isProcessNotExist =
1346
- err.message &&
1347
- (err.message.includes('not found') ||
1348
- err.message.includes('does not exist') ||
1349
- err.message.includes('not running'));
1350
- if (isProcessNotExist) {
1351
- throw new Error(`pm2 process ${processId} (${name}) died or does not exist: ${err.message}`);
1352
- }
1353
- throw new Error(`pm2 process ${processId} (${name}) check failed: ${err.message}`);
1354
- }
1355
- },
1356
- });
1357
- } catch (error) {
1358
- const isProcessDead =
1359
- error.message &&
1360
- (error.message.includes('pm2 process') ||
1361
- error.message.includes('died') ||
1362
- error.message.includes('does not exist'));
1363
- if (isProcessDead) {
1364
- logger.error('blocklet process died during health check', {
1365
- appDid,
1366
- processId,
1367
- name,
1368
- port,
1369
- error: error.message,
1370
- });
1371
- throw error;
1372
- }
1373
- logger.error('ensure endpoint healthy failed', {
1374
- appDid,
1375
- port,
1376
- minConsecutiveTime,
1377
- timeout,
1378
- error: error.message,
1379
- });
1380
- throw error;
1381
- }
1382
- } catch (error) {
1383
- logger.error('start blocklet failed', { processId, name });
1384
- throw error;
1385
- }
1386
- };
1387
-
1388
- const checkBlockletProcessHealthy = async (
1389
- blocklet,
1390
- { minConsecutiveTime, timeout, componentDids, setBlockletRunning, isGreen = false, appDid } = {}
1391
- ) => {
1392
- // if (process.env.NODE_ENV === 'test' && process.env.ABT_NODE_TEST_MIN_CONSECUTIVE_TIME) {
1393
- // // need bigger than minConsecutiveTime in test env
1394
- // // eslint-disable-next-line no-param-reassign
1395
- // timeout = Math.max(+process.env.ABT_NODE_TEST_MIN_CONSECUTIVE_TIME * 10, minConsecutiveTime + 3000);
1396
- // }
1397
- await forEachBlocklet(
1398
- blocklet,
1399
- async (b) => {
1400
- if (b.meta.group === BlockletGroup.gateway) {
1401
- return;
1402
- }
1403
-
1404
- // components that relies on another engine component should not be checked
1405
- const engine = getBlockletEngine(b.meta);
1406
- if (engine.interpreter === 'blocklet') {
1407
- return;
1408
- }
1409
-
1410
- if (!hasStartEngine(b.meta)) {
1411
- return;
1412
- }
1413
-
1414
- // eslint-disable-next-line no-use-before-define
1415
- if (shouldSkipComponent(b.meta.did, componentDids)) {
1416
- logger.info('skip check component healthy', { processId: b.env.processId });
1417
- return;
1418
- }
1419
-
1420
- const logToTerminal = [blocklet.mode, b.mode].includes(BLOCKLET_MODES.DEVELOPMENT);
1421
-
1422
- const startedAt = Date.now();
1423
-
1424
- await _checkProcessHealthy(b, { minConsecutiveTime, timeout, logToTerminal, isGreen, appDid });
1425
-
1426
- logger.info('done check component healthy', { processId: b.env.processId, time: Date.now() - startedAt });
1427
-
1428
- if (setBlockletRunning) {
1429
- try {
1430
- await setBlockletRunning(b.meta.did);
1431
- } catch (error) {
1432
- logger.error(`Failed to set blocklet as running for DID: ${b.meta.name || b.meta.did}`, { error });
1433
- }
1434
- }
1435
- },
1436
- { parallel: true }
1437
- );
1438
- };
1439
-
1440
- const shouldCheckHealthy = (blocklet) => {
1441
- if (blocklet.meta.group === BlockletGroup.gateway) {
1442
- return false;
1443
- }
1444
-
1445
- // components that relies on another engine component should not be checked
1446
- const engine = getBlockletEngine(blocklet.meta);
1447
- if (engine.interpreter === 'blocklet') {
1448
- return false;
1449
- }
1450
-
1451
- return hasStartEngine(blocklet.meta);
1452
- };
1453
-
1454
- const isBlockletPortHealthy = async (blocklet, { minConsecutiveTime = 3000, timeout = 10 * 1000 } = {}) => {
1455
- if (!blocklet) {
1456
- return;
1457
- }
1458
- const { environments } = blocklet;
1459
- const webInterface = (blocklet.meta?.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
1460
- const dockerInterface = (blocklet.meta?.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_DOCKER);
1461
- const key = webInterface?.port || dockerInterface?.port || 'BLOCKLET_PORT';
1462
-
1463
- let port = blocklet.greenStatus === BlockletStatus.running ? blocklet.greenPorts?.[key] : blocklet.ports?.[key];
1464
-
1465
- if (!port) {
1466
- const keyPort = webInterface?.port || dockerInterface?.port;
1467
- port = environments?.find((e) => e.key === keyPort)?.value;
1468
- }
1469
-
1470
- if (!port) {
1471
- return;
1472
- }
1473
-
1474
- await ensureEndpointHealthy({
1475
- port,
1476
- protocol: webInterface ? 'http' : 'tcp',
1477
- minConsecutiveTime,
1478
- timeout,
1479
- doConsecutiveCheck: false,
1480
- });
1481
- };
1482
-
1483
- const expandTarball = async ({ source, dest, strip = 1 }) => {
1484
- logger.info('expand blocklet', { source, dest });
1485
-
1486
- if (!fs.existsSync(source)) {
1487
- throw new Error(`Blocklet tarball ${source} does not exist`);
1488
- }
1489
-
1490
- fs.mkdirSync(dest, { recursive: true });
1491
-
1492
- await streamToPromise(
1493
- fs
1494
- .createReadStream(source)
1495
- .pipe(new Throttle({ rate: 1024 * 1024 * 20 })) // 20MB
1496
- .pipe(tar.x({ C: dest, strip }))
1497
- );
1498
-
1499
- return dest;
1500
- };
1501
-
1502
- const verifyIntegrity = async ({ file, integrity: expected }) => {
1503
- const stream = fs.createReadStream(file);
1504
- const result = await ssri.checkStream(stream, ssri.parse(expected));
1505
- logger.debug('verify integrity result', { result });
1506
- stream.destroy();
1507
- return true;
1508
- };
1509
-
1510
- /**
1511
- * @param {string} installDir
1512
- * @returns {Promise<Array<{ key: string, dir: string }>>} key is <[scope/]name/version>, dir is appDir
1513
- */
1514
- const getAppDirs = async (installDir) => {
1515
- const appDirs = [];
1516
-
1517
- const getNextLevel = (level, name) => {
1518
- if (level === 'root') {
1519
- if (name.startsWith('@')) {
1520
- return 'scope';
1521
- }
1522
- return 'name';
1523
- }
1524
- if (level === 'scope') {
1525
- return 'name';
1526
- }
1527
- if (level === 'name') {
1528
- return 'version';
1529
- }
1530
- throw new Error(`Invalid level ${level}`);
1531
- };
1532
-
1533
- const fillAppDirs = async (dir, level = 'root') => {
1534
- if (level === 'version') {
1535
- appDirs.push({
1536
- key: formatBackSlash(path.relative(installDir, dir)),
1537
- dir,
1538
- });
1539
-
1540
- return;
1541
- }
1542
-
1543
- const nextDirs = [];
1544
- for (const x of await fs.promises.readdir(dir)) {
1545
- if (!fs.lstatSync(path.join(dir, x)).isDirectory()) {
1546
- logger.error('pruneBlockletBundle: invalid file in bundle storage', { dir, file: x });
1547
- // eslint-disable-next-line no-continue
1548
- continue;
1549
- }
1550
- nextDirs.push(x);
1551
- }
1552
-
1553
- for (const x of nextDirs) {
1554
- await fillAppDirs(path.join(dir, x), getNextLevel(level, x));
1555
- }
1556
- };
1557
-
1558
- await fillAppDirs(installDir, 'root');
1559
-
1560
- return appDirs;
1561
- };
1562
-
1563
- const pruneBlockletBundle = async ({ blocklets, installDir, blockletSettings }) => {
1564
- for (const blocklet of blocklets) {
1565
- if (
1566
- [
1567
- BlockletStatus.waiting,
1568
- BlockletStatus.installing,
1569
- BlockletStatus.upgrading,
1570
- BlockletStatus.downloading,
1571
- ].includes(blocklet.status)
1572
- ) {
1573
- logger.info('There are blocklet activities in progress, abort pruning', {
1574
- bundleName: blocklet.meta.bundleName,
1575
- status: fromBlockletStatus(blocklet.status),
1576
- });
1577
- return;
1578
- }
1579
- }
1580
-
1581
- // blockletMap: { <[scope/]name/version>: true }
1582
- const blockletMap = {};
1583
- for (const blocklet of blocklets) {
1584
- forEachBlockletSync(blocklet, (component) => {
1585
- blockletMap[`${component.meta.bundleName}/${component.meta.version}`] = true;
1586
- blockletMap[`${component.meta.bundleName}/${getVersionScope(component.meta)}`] = true;
1587
- });
1588
- }
1589
- for (const setting of blockletSettings) {
1590
- for (const child of setting.children || []) {
1591
- if (child.status !== BlockletStatus.deleted) {
1592
- forEachBlockletSync(child, (component) => {
1593
- blockletMap[`${component.meta.bundleName}/${component.meta.version}`] = true;
1594
- blockletMap[`${component.meta.bundleName}/${getVersionScope(component.meta)}`] = true;
1595
- });
1596
- }
1597
- }
1598
- }
1599
-
1600
- // fill appDirs
1601
- let appDirs = [];
1602
- try {
1603
- appDirs = await getAppDirs(installDir);
1604
- } catch (error) {
1605
- logger.error('fill app dirs failed', { error });
1606
- }
1607
-
1608
- const ensureBundleDirRemoved = async (dir) => {
1609
- const relativeDir = path.relative(installDir, dir);
1610
- const arr = relativeDir.split(path.sep).filter(Boolean);
1611
- const { length } = arr;
1612
- const bundleName = arr[length - 2];
1613
- const scopeName = length > 2 ? arr[length - 3] : '';
1614
- const bundleDir = path.join(installDir, scopeName, bundleName);
1615
- const isDirEmpty = (await fs.promises.readdir(bundleDir)).length === 0;
1616
- if (isDirEmpty) {
1617
- logger.info('Remove bundle folder', { bundleDir });
1618
- await fs.remove(bundleDir);
1619
- }
1620
- if (scopeName) {
1621
- const scopeDir = path.join(installDir, scopeName);
1622
- const isScopeEmpty = (await fs.promises.readdir(scopeDir)).length === 0;
1623
- if (isScopeEmpty) {
1624
- logger.info('Remove scope folder', { scopeDir });
1625
- await fs.remove(scopeDir);
1626
- }
1627
- }
1628
- };
1629
-
1630
- // remove trash
1631
- for (const app of appDirs) {
1632
- if (!blockletMap[app.key]) {
1633
- logger.info('Remove app folder', { dir: app.dir });
1634
- await fs.remove(app.dir);
1635
- await ensureBundleDirRemoved(app.dir);
1636
- }
1637
- }
1638
-
1639
- logger.info('Blocklet source folder has been pruned');
1640
- };
1641
-
1642
- const _diskInfoTasks = {};
1643
- const _getDiskInfo = async (blocklet) => {
1644
- try {
1645
- const { env } = blocklet;
1646
- const [app, cache, log, data] = await Promise.all([
1647
- getFolderSize(env.appDir),
1648
- getFolderSize(env.cacheDir),
1649
- getFolderSize(env.logsDir),
1650
- getFolderSize(env.dataDir),
1651
- ]);
1652
- return { app, cache, log, data };
1653
- } catch (error) {
1654
- logger.error('Get disk info failed', { name: getDisplayName(blocklet), error });
1655
- return { app: 0, cache: 0, log: 0, data: 0 };
1656
- }
1657
- };
1658
-
1659
- const getDiskInfo = (blocklet, { useFakeDiskInfo } = {}) => {
1660
- if (useFakeDiskInfo) {
1661
- return { app: 0, cache: 0, log: 0, data: 0 };
1662
- }
1663
-
1664
- const { appDid } = blocklet;
1665
-
1666
- // Cache disk info results for 5 minutes
1667
- _diskInfoTasks[appDid] ??= _getDiskInfo(blocklet).finally(() => {
1668
- setTimeout(
1669
- () => {
1670
- delete _diskInfoTasks[appDid];
1671
- },
1672
- 5 * 60 * 1000
1673
- );
1674
- });
1675
-
1676
- return new Promise((resolve) => {
1677
- _diskInfoTasks[appDid].then(resolve).catch(() => {
1678
- resolve({ app: 0, cache: 0, log: 0, data: 0 });
1679
- });
1680
- });
1681
- };
1682
-
1683
- const getRuntimeInfo = async (processId) => {
1684
- const proc = await getProcessInfo(processId);
1685
- const dockerName = proc.pm2_env?.env?.dockerName;
1686
- if (dockerName) {
1687
- const dockerInfo = await getDockerRuntimeInfo(dockerName);
1688
- return {
1689
- ...dockerInfo,
1690
- pid: proc.pid,
1691
- uptime: proc.pm2_env ? Date.now() - Number(proc.pm2_env.pm_uptime) : 0,
1692
- port: proc.pm2_env ? proc.pm2_env.BLOCKLET_PORT : null,
1693
- status: proc.pm2_env ? proc.pm2_env.status : null,
1694
- runningDocker: !!dockerName,
1695
- };
1696
- }
1697
- return {
1698
- pid: proc.pid,
1699
- uptime: proc.pm2_env ? Date.now() - Number(proc.pm2_env.pm_uptime) : 0,
1700
- memoryUsage: proc.monit.memory,
1701
- cpuUsage: proc.monit.cpu,
1702
- status: proc.pm2_env ? proc.pm2_env.status : null,
1703
- port: proc.pm2_env ? proc.pm2_env.BLOCKLET_PORT : null,
1704
- runningDocker: false,
1705
- };
1706
- };
1707
-
1708
- /**
1709
- * merge services
1710
- * from meta.children[].mountPoints[].services, meta.children[].services
1711
- * to childrenMeta[].interfaces[].services
1712
- *
1713
- * @param {array<child>|object{children:array}} source e.g. [<config>] or { children: [<config>] }
1714
- * @param {array<meta|{meta}>} childrenMeta e.g. [<meta>] or [{ meta: <meta> }]
1715
- */
1716
-
1717
- const mergeMeta = (source, childrenMeta = []) => {
1718
- // configMap
1719
- const configMap = {};
1720
- (Array.isArray(source) ? source : getComponentConfig(source) || []).forEach((x) => {
1721
- configMap[x.name] = x;
1722
- });
1723
-
1724
- // merge service from config to child meta
1725
- childrenMeta.forEach((child) => {
1726
- const childMeta = child.meta || child;
1727
- const config = configMap[childMeta.name];
1728
- if (!config) {
1729
- return;
1730
- }
1731
-
1732
- (config.mountPoints || []).forEach((mountPoint) => {
1733
- if (!mountPoint.services) {
1734
- return;
1735
- }
1736
-
1737
- const childInterface = childMeta.interfaces.find((y) => y.name === mountPoint.child.interfaceName);
1738
- if (childInterface) {
1739
- // merge
1740
- const services = childInterface.services || [];
1741
- mountPoint.services.forEach((x) => {
1742
- const index = services.findIndex((y) => y.name === x.name);
1743
- if (index >= 0) {
1744
- services.splice(index, 1, x);
1745
- } else {
1746
- services.push(x);
1747
- }
1748
- });
1749
- childInterface.services = services;
1750
- }
1751
- });
1752
-
1753
- if (config.services) {
1754
- const childInterface = findWebInterface(childMeta);
1755
- if (childInterface) {
1756
- // merge
1757
- const services = childInterface.services || [];
1758
- config.services.forEach((x) => {
1759
- const index = services.findIndex((y) => y.name === x.name);
1760
- if (index >= 0) {
1761
- services.splice(index, 1, x);
1762
- } else {
1763
- services.push(x);
1764
- }
1765
- });
1766
- childInterface.services = services;
1767
- }
1768
- }
1769
- });
1770
- };
1771
-
1772
- const getUpdateMetaList = (oldBlocklet = {}, newBlocklet = {}) => {
1773
- const oldMap = {};
1774
- forEachChildSync(oldBlocklet, (b, { id }) => {
1775
- if (b.bundleSource) {
1776
- oldMap[id] = b.meta.version;
1777
- }
1778
- });
1779
-
1780
- const res = [];
1781
-
1782
- forEachChildSync(newBlocklet, (b, { id }) => {
1783
- if ((b.bundleSource && semver.gt(b.meta.version, oldMap[id])) || process.env.TEST_UPDATE_ALL_BLOCKLET === 'true') {
1784
- res.push({ id, meta: b.meta });
1785
- }
1786
- });
1787
-
1788
- return res;
1789
- };
1790
-
1791
- /**
1792
- * @returns BLOCKLET_INSTALL_TYPE
1793
- */
1794
- const getTypeFromInstallParams = (params) => {
1795
- if (params.type) {
1796
- if (!Object.values(BLOCKLET_INSTALL_TYPE).includes(params.type)) {
1797
- throw new Error(`Can only install blocklet from ${Object.values(BLOCKLET_INSTALL_TYPE).join('/')}`);
1798
- }
1799
- return params.type;
1800
- }
1801
-
1802
- if (params.url) {
1803
- return BLOCKLET_INSTALL_TYPE.URL;
1804
- }
1805
-
1806
- if (params.file) {
1807
- throw new Error('install from upload is not supported');
1808
- }
1809
-
1810
- if (params.did) {
1811
- return BLOCKLET_INSTALL_TYPE.STORE;
1812
- }
1813
-
1814
- if (params.title && params.description) {
1815
- return BLOCKLET_INSTALL_TYPE.CREATE;
1816
- }
1817
-
1818
- throw new Error(`Can only install blocklet from ${Object.values(BLOCKLET_INSTALL_TYPE).join('/')}`);
1819
- };
1820
-
1821
- const checkDuplicateComponents = (components = []) => {
1822
- const duplicates = components.filter(
1823
- (item, index) => components.findIndex((x) => x.meta.did === item.meta.did) !== index
1824
- );
1825
- if (duplicates.length) {
1826
- throw new Error(
1827
- `Cannot add duplicate component${duplicates.length > 1 ? 's' : ''}: ${duplicates
1828
- .map((x) => getDisplayName(x, true))
1829
- .join(', ')}`
1830
- );
1831
- }
1832
- };
1833
-
1834
- const getDiffFiles = async (inputFiles, sourceDir) => {
1835
- if (!fs.existsSync(sourceDir)) {
1836
- throw new Error(`${sourceDir} does not exist`);
1837
- }
1838
-
1839
- const files = inputFiles.reduce((obj, item) => {
1840
- obj[item.file] = item.hash;
1841
- return obj;
1842
- }, {});
1843
-
1844
- const { files: sourceFiles } = await hashFiles(sourceDir, {
1845
- filter: (x) => x.indexOf('node_modules') === -1,
1846
- concurrentHash: 1,
1847
- });
1848
-
1849
- const addSet = [];
1850
- const changeSet = [];
1851
- const deleteSet = [];
1852
-
1853
- const diffFiles = diff(sourceFiles, files);
1854
- if (diffFiles) {
1855
- diffFiles.forEach((item) => {
1856
- if (item.kind === 'D') {
1857
- deleteSet.push(item.path[0]);
1858
- }
1859
- if (item.kind === 'E') {
1860
- changeSet.push(item.path[0]);
1861
- }
1862
- if (item.kind === 'N') {
1863
- addSet.push(item.path[0]);
1864
- }
1865
- });
1866
- }
1867
-
1868
- return {
1869
- addSet,
1870
- changeSet,
1871
- deleteSet,
1872
- };
1873
- };
1874
-
1875
- const checkCompatibleOnce = {};
1876
-
1877
- // TODO: 梁柱, 这里为了兼容旧版的 blocklet,需要暂时保留,未来所有 blocklet 都使用新路径了可以删除
1878
- const compatibleWithOldBlocklets = (dir) => {
1879
- if (checkCompatibleOnce[dir] !== undefined) {
1880
- return checkCompatibleOnce[dir];
1881
- }
1882
-
1883
- checkCompatibleOnce[dir] = !!fs.existsSync(path.join(dir, 'blocklet.yml'));
1884
-
1885
- return checkCompatibleOnce[dir];
1886
- };
1887
-
1888
- const getBundleDir = (installDir, meta) => {
1889
- const oldDir = path.join(installDir, meta.bundleName || meta.name, meta.version);
1890
- if (compatibleWithOldBlocklets(oldDir)) {
1891
- return oldDir;
1892
- }
1893
-
1894
- return path.join(installDir, meta.bundleName || meta.name, getVersionScope(meta));
1895
- };
1896
-
1897
- const needBlockletDownload = (blocklet, oldBlocklet) => {
1898
- if ([BlockletSource.upload, BlockletSource.local, BlockletSource.custom].includes(blocklet.source)) {
1899
- return false;
1900
- }
1901
-
1902
- if (!get(oldBlocklet, 'meta.dist.integrity')) {
1903
- return true;
1904
- }
1905
-
1906
- return get(oldBlocklet, 'meta.dist.integrity') !== get(blocklet, 'meta.dist.integrity');
1907
- };
1908
-
1909
- const formatBlockletTheme = (rawTheme) => {
1910
- let themeConfig = {};
1911
-
1912
- if (rawTheme) {
1913
- if (Array.isArray(rawTheme.concepts) && rawTheme.currentConceptId) {
1914
- const concept = rawTheme.concepts.find((x) => x.id === rawTheme.currentConceptId);
1915
- themeConfig = {
1916
- ...concept.themeConfig,
1917
- prefer: concept.prefer,
1918
- name: concept.name,
1919
- };
1920
- } else {
1921
- // 兼容旧数据
1922
- themeConfig = {
1923
- light: rawTheme.light || {},
1924
- dark: rawTheme.dark || {},
1925
- common: rawTheme.common || {},
1926
- prefer: rawTheme.prefer || 'system',
1927
- name: rawTheme.name || 'Default',
1928
- };
1929
- }
1930
- }
1931
-
1932
- const result = mergeWith(
1933
- // 至少提供 palette 色板值(客户端会使用)
1934
- cloneDeep({
1935
- light: { palette: BLOCKLET_THEME_LIGHT.palette },
1936
- dark: { palette: BLOCKLET_THEME_DARK.palette },
1937
- prefer: 'system',
1938
- }),
1939
- themeConfig,
1940
- // 数组值直接替换
1941
- (_, srcValue) => {
1942
- if (Array.isArray(srcValue)) {
1943
- return srcValue;
1944
- }
1945
- return undefined;
1946
- }
1947
- );
1948
-
1949
- // 保留原始数据,用于 settings 保存
1950
- Object.defineProperty(result, 'raw', {
1951
- value: rawTheme,
1952
- enumerable: false,
1953
- writable: false,
1954
- });
1955
-
1956
- return result;
1957
- };
1958
-
1959
- const _getBlocklet = async ({
1960
- did,
1961
- dataDirs,
1962
- states,
1963
- e2eMode = false,
1964
- throwOnNotExist = true,
1965
- ensureIntegrity = false,
1966
- getOptionalComponents = false,
1967
- } = {}) => {
1968
- if (!did) {
1969
- throw new Error('Blocklet did does not exist');
1970
- }
1971
- if (!isValidDid(did)) {
1972
- logger.error('Blocklet did is invalid', { did });
1973
- throw new Error('Blocklet did is invalid');
1974
- }
1975
-
1976
- if (!dataDirs) {
1977
- throw new Error('dataDirs does not exist');
1978
- }
1979
-
1980
- if (!states) {
1981
- throw new Error('states does not exist');
1982
- }
1983
-
1984
- const blocklet = await states.blocklet.getBlocklet(did);
1985
- if (!blocklet) {
1986
- if (throwOnNotExist || ensureIntegrity) {
1987
- logger.error('can not find blocklet in database by did', { did });
1988
- throw new Error('can not find blocklet in database by did');
1989
- }
1990
- return null;
1991
- }
1992
-
1993
- // 优化:并行查询独立数据(只查询一次 extraDoc,然后从内存中同步提取)
1994
- const [extraDoc, nodeInfo, site] = await Promise.all([
1995
- states.blockletExtras.getExtraByDid(blocklet.meta.did),
1996
- states.node.read(),
1997
- states.site.findOneByBlocklet(blocklet.meta.did),
1998
- ]);
1999
-
2000
- // 从 extraDoc 中同步提取 settings(不需要再次查询数据库)
2001
- const extrasMeta = extraDoc ? pick(extraDoc, ['did', 'meta', 'controller']) : null;
2002
- const settings = states.blockletExtras.getFromDoc({ doc: extraDoc, dids: [blocklet.meta.did], name: 'settings' });
2003
-
2004
- // app settings
2005
- // FIXME: @zhanghan 在 server 开发模式下,使用 `node /workspace/arcblock/blocklet-server/core/cli/tools/dev.js` 运行的 blocklet,blocklet.meta.did 和 blocklet.appPid 是不一致的
2006
- blocklet.trustedPassports = get(settings, 'trustedPassports') || [];
2007
- blocklet.trustedFactories = (get(settings, 'trustedFactories') || []).map((x) => {
2008
- if (!x.passport.ttlPolicy) {
2009
- x.passport.ttlPolicy = 'never';
2010
- x.passport.ttl = 0;
2011
- }
2012
- if (x.factoryAddress) {
2013
- x.factoryAddress = toAddress(x.factoryAddress);
2014
- }
2015
- return x;
2016
- });
2017
- blocklet.enablePassportIssuance = get(settings, 'enablePassportIssuance', true);
2018
- blocklet.settings = settings || {};
2019
-
2020
- if (extrasMeta) {
2021
- blocklet.controller = extrasMeta.controller;
2022
- }
2023
-
2024
- blocklet.settings.storeList = blocklet.settings.storeList || [];
2025
- blocklet.settings.theme = formatBlockletTheme(blocklet.settings.theme);
2026
- blocklet.settings.languages = blocklet.settings.languages || [];
2027
-
2028
- // 移除第一个版本中 from 为 tmpl 的导航
2029
- if (blocklet?.settings?.navigations && Array.isArray(blocklet.settings.navigations)) {
2030
- blocklet.settings.navigations = (blocklet.settings.navigations || []).filter(
2031
- (item) => !(item?.parent === '/team' && ['tmpl'].includes(item.from))
2032
- );
2033
- }
2034
-
2035
- (nodeInfo?.blockletRegistryList || []).forEach((store) => {
2036
- if (!blocklet.settings.storeList.find((x) => x.url === store.url)) {
2037
- blocklet.settings.storeList.push({
2038
- ...store,
2039
- protected: true,
2040
- });
2041
- }
2042
- });
2043
-
2044
- blocklet.site = site;
2045
- blocklet.enableDocker = nodeInfo.enableDocker;
2046
- blocklet.enableDockerNetwork = nodeInfo.enableDockerNetwork;
2047
-
2048
- // 第一次 forEachBlockletSync:收集所有组件的 dids
2049
- const componentConfigRequests = [];
2050
- forEachBlockletSync(blocklet, (component, { ancestors }) => {
2051
- const dids = [...ancestors.map((x) => x.meta.did), component.meta.did];
2052
- componentConfigRequests.push({
2053
- componentDid: component.meta.did,
2054
- dids,
2055
- });
2056
- });
2057
-
2058
- // 基于缓存文档,为每个组件提取 configs(同步操作,不需要再次查询数据库)
2059
- const configsMap = new Map();
2060
- componentConfigRequests.forEach(({ componentDid, dids }) => {
2061
- const configs = states.blockletExtras.getFromDoc({ doc: extraDoc, dids, name: 'configs' });
2062
- configsMap.set(componentDid, configs);
2063
- });
2064
-
2065
- // 第二次 forEachBlockletSync:填充组件
2066
- forEachBlockletSync(blocklet, (component, { id, level, ancestors }) => {
2067
- // component env
2068
- try {
2069
- // Validate component has required meta fields for getComponentDirs
2070
- if (!component.meta) {
2071
- throw new Error(`Component missing meta field: ${component.meta?.did || id}`);
2072
- }
2073
- if (!component.meta.name && !component.meta.bundleName) {
2074
- throw new Error(
2075
- `Component missing meta.name and meta.bundleName: ${component.meta.did || id}. ` +
2076
- 'This may indicate a migration issue with blocklet_children table.'
2077
- );
2078
- }
2079
-
2080
- component.env = {
2081
- id,
2082
- name: getComponentName(component, ancestors),
2083
- processId: getComponentProcessId(component, ancestors),
2084
- ...getComponentDirs(component, {
2085
- dataDirs,
2086
- ensure: ensureIntegrity,
2087
- ancestors,
2088
- e2eMode: level === 0 ? e2eMode : false,
2089
- }),
2090
- };
2091
- } catch (error) {
2092
- logger.error('Failed to set component env in _getBlocklet', {
2093
- componentDid: component.meta?.did,
2094
- componentName: component.meta?.name,
2095
- componentBundleName: component.meta?.bundleName,
2096
- error: error.message,
2097
- stack: error.stack,
2098
- });
2099
- throw error;
2100
- }
2101
-
2102
- // component config - 从预取的 configsMap 中获取
2103
- const configs = configsMap.get(component.meta.did) || [];
2104
- const rootBlocklet = ancestors.length > 0 ? ancestors[0] : blocklet;
2105
- fillBlockletConfigs(component, configs, { rootBlocklet, nodeInfo, dataDirs });
2106
- });
2107
-
2108
- if (getOptionalComponents) {
2109
- const optionalComponents = await parseOptionalComponents(blocklet);
2110
- blocklet.optionalComponents = optionalComponents;
2111
- } else {
2112
- blocklet.optionalComponents = [];
2113
- }
2114
-
2115
- return blocklet;
2116
- };
2117
-
2118
- const getBlocklet = ({
2119
- did,
2120
- ensureIntegrity = false,
2121
- getOptionalComponents = false,
2122
- useCache = false,
2123
- ...rest
2124
- } = {}) => {
2125
- let cacheKey = '';
2126
-
2127
- if (useCache) {
2128
- cacheKey = JSON.stringify({
2129
- ensureIntegrity,
2130
- getOptionalComponents,
2131
- });
2132
- }
2133
-
2134
- return blockletCache.autoCacheGroup(did, cacheKey, () => {
2135
- return _getBlocklet({ did, ensureIntegrity, getOptionalComponents, ...rest });
2136
- });
2137
- };
2138
-
2139
- const fromProperty2Config = (properties = {}, result) => {
2140
- Object.keys(properties).forEach((key) => {
2141
- const prop = properties[key];
2142
- if (prop.properties && ['ArrayTable', 'ArrayCards'].includes(prop['x-component']) === false) {
2143
- fromProperty2Config(prop.properties, result);
2144
- } else if (prop['x-decorator'] === 'FormItem') {
2145
- const secure = prop['x-component'] === 'Password';
2146
- result.push({
2147
- default: prop.default || '',
2148
- description: prop.title || key,
2149
- name: `${BLOCKLET_PREFERENCE_PREFIX}${key}`,
2150
- required: prop.required || false,
2151
- secure,
2152
- // eslint-disable-next-line no-nested-ternary
2153
- shared: secure ? false : typeof prop.shared === 'undefined' ? true : prop.shared,
2154
- });
2155
- }
2156
- });
2157
- };
2158
- const getConfigFromPreferences = (blocklet) => {
2159
- const result = [];
2160
- const schemaFile = path.join(blocklet.env.appDir, BLOCKLET_PREFERENCE_FILE);
2161
- if (fs.existsSync(schemaFile)) {
2162
- try {
2163
- const schema = JSON.parse(fs.readFileSync(schemaFile, 'utf8'));
2164
- fromProperty2Config(schema.schema?.properties, result);
2165
- } catch {
2166
- // do nothing
2167
- }
2168
- }
2169
-
2170
- return result;
2171
- };
2172
-
2173
- const shouldEnableSlpDomain = (mode) => {
2174
- if (process.env.ABT_NODE_ENABLE_SLP_DOMAIN === 'true') {
2175
- return true;
2176
- }
2177
-
2178
- if (process.env.ABT_NODE_ENABLE_SLP_DOMAIN === 'false') {
2179
- return false;
2180
- }
2181
-
2182
- return isInServerlessMode({ mode });
2183
- };
2184
-
2185
- const getBlockletURLForLauncher = ({ blocklet, nodeInfo }) => {
2186
- const enableSlpDomain = shouldEnableSlpDomain(nodeInfo.mode);
2187
- let didDomain = '';
2188
- if (enableSlpDomain) {
2189
- didDomain = getDidDomainForBlocklet({
2190
- // eslint-disable-next-line no-use-before-define
2191
- did: getSlpDid(nodeInfo.did, blocklet.appPid),
2192
- didDomain: nodeInfo.slpDomain,
2193
- });
2194
- } else {
2195
- didDomain = getDidDomainForBlocklet({
2196
- did: blocklet.appPid,
2197
- didDomain: nodeInfo.didDomain,
2198
- });
2199
- }
2200
-
2201
- return `https://${didDomain}`;
2202
- };
2203
- const createDataArchive = (dataDir, fileName) => {
2204
- const zipPath = path.join(os.tmpdir(), fileName);
2205
- if (fs.existsSync(zipPath)) {
2206
- fs.removeSync(zipPath);
2207
- }
2208
-
2209
- const archive = createArchive('zip', { zlib: { level: 9 } });
2210
- const stream = fs.createWriteStream(zipPath);
2211
-
2212
- return new Promise((resolve, reject) => {
2213
- archive
2214
- .directory(dataDir, false)
2215
- .on('error', (err) => reject(err))
2216
- .pipe(stream);
2217
-
2218
- stream.on('close', () => resolve(zipPath));
2219
- archive.finalize();
2220
- });
2221
- };
2222
-
2223
- const isBlockletAppSkUsed = ({ environments, migratedFrom = [] }, appSk) => {
2224
- const isUsedInEnv = environments.find((e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK)?.value === appSk;
2225
- const isUsedInHistory = migratedFrom.some((x) => x.appSk === appSk);
2226
- return isUsedInEnv || isUsedInHistory;
2227
- };
2228
-
2229
- const isRotatingAppSk = (newConfigs, oldConfigs, externalSk) => {
2230
- const newSk = newConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK === x.key);
2231
- if (!newSk) {
2232
- // If no newSk found, we are not rotating the appSk
2233
- return false;
2234
- }
2235
-
2236
- const oldSk = oldConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK === x.key);
2237
- if (!oldSk) {
2238
- // If we have no oldSk, we are setting the initial appSk for external managed apps
2239
- // If we have no oldSk, but we are not external managed apps, we are rotating the appSk
2240
- return !externalSk;
2241
- }
2242
-
2243
- // Otherwise, we must be rotating the appSk
2244
- // eslint-disable-next-line sonarjs/prefer-single-boolean-return
2245
- if (oldSk.value !== newSk.value) {
2246
- return true;
2247
- }
2248
-
2249
- return false;
2250
- };
2251
-
2252
- /**
2253
- * this function has side effect on config.value
2254
- * @param {{ key: string, value?: string }} config
2255
- */
2256
- const validateAppConfig = async (config, states) => {
2257
- const x = config;
2258
-
2259
- // sk should be force secured while other app prop should not be secured
2260
- config.secure = x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK;
2261
-
2262
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
2263
- if (x.value) {
2264
- let wallet;
2265
- try {
2266
- wallet = fromSecretKey(x.value, { role: types.RoleType.ROLE_APPLICATION });
2267
- } catch {
2268
- try {
2269
- wallet = fromSecretKey(x.value, 'eth');
2270
- } catch {
2271
- throw new Error('Invalid custom blocklet secret key');
2272
- }
2273
- }
2274
-
2275
- // Ensure sk is not used by existing blocklets, otherwise we may encounter appDid collision
2276
- const exist = await states.blocklet.hasBlocklet(wallet.address);
2277
- if (exist) {
2278
- throw new Error('Invalid custom blocklet secret key: already used by existing blocklet');
2279
- }
2280
- } else {
2281
- delete x.value;
2282
- }
2283
- }
2284
-
2285
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_URL) {
2286
- if (isEmpty(x.value)) {
2287
- throw new Error(`${x.key} can not be empty`);
2288
- }
2289
-
2290
- if (!isUrl(x.value)) {
2291
- throw new Error(`${x.key}(${x.value}) is not a valid URL`);
2292
- }
2293
- }
2294
-
2295
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_NAME) {
2296
- x.value = await titleSchema.validateAsync(x.value);
2297
- }
2298
-
2299
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_DESCRIPTION) {
2300
- x.value = await descriptionSchema.validateAsync(x.value);
2301
- }
2302
-
2303
- if (
2304
- [
2305
- BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO,
2306
- BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_RECT,
2307
- BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_RECT_DARK,
2308
- BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_SQUARE,
2309
- BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_SQUARE_DARK,
2310
- BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_FAVICON,
2311
- BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPLASH_PORTRAIT,
2312
- BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPLASH_LANDSCAPE,
2313
- ].includes(x.key)
2314
- ) {
2315
- x.value = await logoSchema.validateAsync(x.value);
2316
- }
2317
-
2318
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_CHAIN_TYPE) {
2319
- if (['arcblock', 'ethereum'].includes(x.value) === false) {
2320
- throw new Error('Invalid blocklet wallet type, only "default" and "eth" are supported');
2321
- }
2322
- }
2323
-
2324
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_DELETABLE) {
2325
- if (['yes', 'no'].includes(x.value) === false) {
2326
- throw new Error('BLOCKLET_DELETABLE must be either "yes" or "no"');
2327
- }
2328
- }
2329
-
2330
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_PASSPORT_COLOR) {
2331
- if (x.value && x.value !== 'auto') {
2332
- if (x.value.length !== 7 || !isHex(x.value.slice(-6))) {
2333
- throw new Error('BLOCKLET_PASSPORT_COLOR must be a hex encoded color, eg. #ffeeaa');
2334
- }
2335
- }
2336
- }
2337
-
2338
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACE_ENDPOINT) {
2339
- // @note: value 置空以表删除
2340
- if (x.value && !isUrl(x.value)) {
2341
- throw new Error(`${x.key}(${x.value}) is not a valid URL`);
2342
- }
2343
- }
2344
-
2345
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACES_URL) {
2346
- if (isEmpty(x.value)) {
2347
- throw new Error(`${x.key} can not be empty`);
2348
- }
2349
-
2350
- if (!isUrl(x.value)) {
2351
- throw new Error(`${x.key}(${x.value}) is not a valid URL`);
2352
- }
2353
- }
2354
-
2355
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_BACKUP_ENDPOINT) {
2356
- // @note: value 置空以表删除
2357
- if (isEmpty(x.value)) {
2358
- x.value = '';
2359
- }
2360
-
2361
- if (!isEmpty(x.value) && !isUrl(x.value)) {
2362
- throw new Error(`${x.key}(${x.value}) is not a valid URL`);
2363
- }
2364
- }
2365
-
2366
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_CLUSTER_SIZE) {
2367
- if (isEmpty(x.value)) {
2368
- x.value = '';
2369
- }
2370
-
2371
- const v = Number(x.value);
2372
- if (Number.isNaN(v)) {
2373
- throw new Error(`${x.key} must be number`);
2374
- }
2375
- if (!Number.isInteger(v)) {
2376
- throw new Error(`${x.key} must be integer`);
2377
- }
2378
- }
2379
- };
2380
-
2381
- const checkDuplicateAppSk = async ({ sk, did, states }) => {
2382
- if (!sk && !did) {
2383
- throw new Error('sk and did is empty');
2384
- }
2385
-
2386
- let appSk = sk;
2387
- if (!sk) {
2388
- const nodeInfo = await states.node.read();
2389
- const blocklet = await states.blocklet.getBlocklet(did);
2390
- const configs = await states.blockletExtras.getConfigs([did]);
2391
- const { wallet } = getBlockletInfo(
2392
- {
2393
- meta: blocklet.meta,
2394
- environments: (configs || []).filter((x) => x.value),
2395
- },
2396
- nodeInfo.sk
2397
- );
2398
- appSk = wallet.secretKey;
2399
- }
2400
-
2401
- let wallet;
2402
- try {
2403
- wallet = fromSecretKey(appSk, { role: types.RoleType.ROLE_APPLICATION });
2404
- } catch {
2405
- try {
2406
- wallet = fromSecretKey(appSk, 'eth');
2407
- } catch {
2408
- throw new Error('Invalid custom blocklet secret key');
2409
- }
2410
- }
2411
-
2412
- const exist = await states.blocklet.hasBlocklet(wallet.address);
2413
- if (exist) {
2414
- throw new Error(`blocklet secret key already used by ${exist.meta.title || exist.meta.name}`);
2415
- }
2416
- };
2417
-
2418
- const checkDuplicateMountPoint = (app, mountPoint) => {
2419
- const err = new Error(`cannot add duplicate mount point, ${mountPoint || '/'} already exist`);
2420
-
2421
- for (const component of app.children || []) {
2422
- if (
2423
- hasStartEngine(component.meta) &&
2424
- normalizePathPrefix(component.mountPoint) === normalizePathPrefix(mountPoint)
2425
- ) {
2426
- throw err;
2427
- }
2428
- }
2429
- };
2430
-
2431
- const resolveMountPointConflict = (comp, blocklet) => {
2432
- try {
2433
- if (!comp?.mountPoint) return comp;
2434
-
2435
- const children = (blocklet?.children || []).filter((x) => x?.meta && hasMountPoint(x.meta));
2436
-
2437
- const existingComponent = children.find((x) => x.mountPoint === comp.mountPoint && x.meta?.did !== comp.meta?.did);
2438
- if (!existingComponent) return comp;
2439
-
2440
- const baseName = formatName(comp?.meta?.name) || formatName(comp?.meta?.title);
2441
- comp.mountPoint = children.some((x) => x.mountPoint === `/${baseName}`)
2442
- ? comp.meta.did
2443
- : baseName.toLocaleLowerCase();
2444
-
2445
- return comp;
2446
- } catch (error) {
2447
- logger.error('Failed to resolve mount point:', error);
2448
- comp.mountPoint = comp.meta.did;
2449
- return comp;
2450
- }
2451
- };
2452
-
2453
- const validateStore = (nodeInfo, storeUrl) => {
2454
- if (nodeInfo.mode !== 'serverless') {
2455
- return;
2456
- }
2457
-
2458
- const storeUrlObj = new URL(storeUrl);
2459
-
2460
- // Check trusted blocklet sources from environment variable first
2461
- const trustedSources = process.env.ABT_NODE_TRUSTED_SOURCES;
2462
- if (trustedSources) {
2463
- const trustedHosts = trustedSources
2464
- .split(',')
2465
- .map((url) => url.trim())
2466
- .filter(Boolean)
2467
- .map((url) => new URL(url).host);
2468
-
2469
- if (trustedHosts.includes(storeUrlObj.host)) {
2470
- return;
2471
- }
2472
- }
2473
-
2474
- const registerUrlObj = new URL(nodeInfo.registerUrl);
2475
-
2476
- // 信任 Launcher 打包的应用
2477
- if (registerUrlObj.host === storeUrlObj.host) {
2478
- return;
2479
- }
2480
-
2481
- const inStoreList = nodeInfo.blockletRegistryList.find((item) => {
2482
- const itemURLObj = new URL(item.url);
2483
-
2484
- return itemURLObj.host === storeUrlObj.host;
2485
- });
2486
-
2487
- if (!inStoreList) {
2488
- throw new Error('Must be installed from the compliant blocklet store list');
2489
- }
2490
- };
2491
-
2492
- const validateInServerless = ({ blockletMeta }) => {
2493
- const { interfaces } = blockletMeta;
2494
- const externalPortInterfaces = (interfaces || []).filter((item) => !!item.port?.external);
2495
-
2496
- if (externalPortInterfaces.length > 0) {
2497
- throw new Error('Blocklets with exposed ports cannot be installed');
2498
- }
2499
- };
2500
-
2501
- const checkStructVersion = (blocklet) => {
2502
- if (blocklet.structVersion !== APP_STRUCT_VERSION) {
2503
- throw new Error('You should migrate the application first');
2504
- }
2505
- };
2506
-
2507
- const isVersionCompatible = (actualVersion, expectedRange) =>
2508
- !expectedRange || expectedRange === 'latest' || semver.satisfies(actualVersion, expectedRange);
2509
-
2510
- const checkVersionCompatibility = (components) => {
2511
- for (const component of components) {
2512
- // eslint-disable-next-line no-loop-func
2513
- forEachBlockletSync(component, (x) => {
2514
- const dependencies = x.dependencies || [];
2515
- dependencies.forEach((dep) => {
2516
- const { did, version: expectedRange } = dep;
2517
- const exist = components.find((y) => y.meta.did === did);
2518
- if (exist && !isVersionCompatible(exist.meta.version, expectedRange)) {
2519
- throw new Error(
2520
- `Check version compatible failed: ${component.meta.title || component.meta.did} expects ${
2521
- exist.meta.title || exist.meta.did
2522
- }'s version to be ${expectedRange}, but actual is ${exist.meta.version}`
2523
- );
2524
- }
2525
- });
2526
- });
2527
- }
2528
- };
2529
-
2530
- const getBlockletKnownAs = (blocklet) => {
2531
- const alsoKnownAs = [blocklet.appDid];
2532
- if (Array.isArray(blocklet.migratedFrom)) {
2533
- blocklet.migratedFrom.filter((x) => x.appDid !== blocklet.appPid).forEach((x) => alsoKnownAs.push(x.appDid));
2534
- }
2535
-
2536
- return alsoKnownAs.filter(Boolean).map(toDid);
2537
- };
2538
-
2539
- const getFixedBundleSource = (component) => {
2540
- if (!component) {
2541
- return null;
2542
- }
2543
-
2544
- if (component.bundleSource) {
2545
- return component.bundleSource;
2546
- }
2547
-
2548
- const { source, deployedFrom, meta: { bundleName } = {} } = component;
2549
-
2550
- if (!deployedFrom) {
2551
- return null;
2552
- }
2553
-
2554
- if (source === BlockletSource.registry && bundleName) {
2555
- return {
2556
- store: deployedFrom,
2557
- name: bundleName,
2558
- version: 'latest',
2559
- };
2560
- }
2561
-
2562
- if (source === BlockletSource.url) {
2563
- return {
2564
- url: deployedFrom,
2565
- };
2566
- }
2567
-
2568
- return null;
2569
- };
2570
-
2571
- const updateBlockletFallbackLogo = async (blocklet) => {
2572
- if (isEthereumDid(blocklet.meta.did)) {
2573
- await fs.writeFile(path.join(blocklet.env.dataDir, 'logo.svg'), createBlockiesSvg(blocklet.meta.did));
2574
- } else {
2575
- await fs.writeFile(path.join(blocklet.env.dataDir, 'logo.svg'), createDidLogo(blocklet.meta.did));
2576
- }
2577
- };
2578
-
2579
- const ensureAppLogo = async (blocklet, blockletsDir) => {
2580
- if (!blocklet) {
2581
- return;
2582
- }
2583
-
2584
- if (
2585
- blocklet.source === BlockletSource.custom &&
2586
- (blocklet.children || [])[0]?.meta?.logo &&
2587
- blocklet.children[0].env.appDir
2588
- ) {
2589
- const fileName = blocklet.children[0].meta.logo;
2590
- const src = path.join(blocklet.children[0].env.appDir, fileName);
2591
- const dist = path.join(getBundleDir(blockletsDir, blocklet.meta), fileName);
2592
-
2593
- if (fs.existsSync(src)) {
2594
- await fs.copy(src, dist);
2595
- }
2596
- }
2597
- };
2598
-
2599
- const getBlockletDidDomainList = (blocklet, nodeInfo) => {
2600
- const domainAliases = [];
2601
- const alsoKnownAs = getBlockletKnownAs(blocklet);
2602
-
2603
- const dids = [blocklet.appPid, blocklet.appDid, ...alsoKnownAs].filter(Boolean).map((did) => toAddress(did));
2604
-
2605
- uniq(dids).forEach((did) => {
2606
- const domain = getDidDomainForBlocklet({ did, didDomain: nodeInfo.didDomain });
2607
-
2608
- domainAliases.push({ value: domain, isProtected: true });
2609
- });
2610
-
2611
- // eslint-disable-next-line no-use-before-define
2612
- const enableSlpDomain = shouldEnableSlpDomain(nodeInfo.mode);
2613
- if (enableSlpDomain) {
2614
- // eslint-disable-next-line no-use-before-define
2615
- const slpDid = getSlpDid(nodeInfo.did, blocklet.appPid);
2616
- const domain = getDidDomainForBlocklet({ did: slpDid, didDomain: nodeInfo.slpDomain });
2617
-
2618
- domainAliases.push({ value: domain, isProtected: true });
2619
- }
2620
-
2621
- return domainAliases;
2622
- };
2623
-
2624
- const getBlockletStatus = (blocklet) => {
2625
- const fallbackStatus = BlockletStatus.stopped;
2626
-
2627
- if (!blocklet) {
2628
- return fallbackStatus;
2629
- }
2630
-
2631
- if (!blocklet.children?.length) {
2632
- if (blocklet.meta?.group === BlockletGroup.gateway) {
2633
- return blocklet.status;
2634
- }
2635
-
2636
- if (blocklet.status === BlockletStatus.added) {
2637
- return BlockletStatus.added;
2638
- }
2639
-
2640
- // for backward compatibility
2641
- if (!blocklet.structVersion) {
2642
- return blocklet.status;
2643
- }
2644
-
2645
- return fallbackStatus;
2646
- }
2647
-
2648
- let inProgressStatus;
2649
- let runningStatus;
2650
- let status;
2651
-
2652
- forEachComponentV2Sync(blocklet, (component) => {
2653
- if (component.meta?.group === BlockletGroup.gateway) {
2654
- return;
2655
- }
2656
-
2657
- if (isInProgress(component.status)) {
2658
- if (!inProgressStatus) {
2659
- inProgressStatus = component.status;
2660
- }
2661
- return;
2662
- }
2663
-
2664
- if (isRunning(component.status) || isRunning(component.greenStatus)) {
2665
- runningStatus = BlockletStatus.running;
2666
- return;
2667
- }
2668
-
2669
- if (status === BlockletStatus.stopped) {
2670
- return;
2671
- }
2672
-
2673
- status = component.status;
2674
- });
2675
-
2676
- return inProgressStatus || runningStatus || status;
2677
- };
2678
-
2679
- const shouldSkipComponent = (componentDid, whiteList) => {
2680
- if (!whiteList || !Array.isArray(whiteList)) {
2681
- return false;
2682
- }
2683
-
2684
- const arr = whiteList.filter(Boolean);
2685
-
2686
- if (!arr.length) {
2687
- return false;
2688
- }
2689
-
2690
- return !arr.includes(componentDid);
2691
- };
2692
-
2693
- const ensurePortsShape = (_states, portsA, portsB) => {
2694
- if (!portsA || Object.keys(portsA).length === 0) {
2695
- return;
2696
- }
2697
- if (Object.keys(portsB).length === 0) {
2698
- for (const key of Object.keys(portsA)) {
2699
- portsB[key] = portsA[key];
2700
- }
2701
- }
2702
- };
2703
-
2704
- const ensureAppPortsNotOccupied = async ({ blocklet, componentDids: inputDids, states, manager, isGreen = false }) => {
2705
- const { did } = blocklet.meta;
2706
- const lockName = `port-check-${did}`;
2707
-
2708
- // ⚠️ 关键修复:使用 DBCache 锁确保端口分配的原子性
2709
- // 在多进程环境下,防止多个进程同时检查同一个端口
2710
- await portAssignLock.acquire(lockName);
2711
-
2712
- try {
2713
- const occupiedDids = new Set();
2714
-
2715
- await forEachComponentV2(blocklet, async (b) => {
2716
- try {
2717
- if (shouldSkipComponent(b.meta.did, inputDids)) return;
2718
-
2719
- if (!b.greenPorts) {
2720
- occupiedDids.add(b.meta.did);
2721
- b.greenPorts = {};
2722
- }
2723
- const { ports = {}, greenPorts } = b;
2724
- ensurePortsShape(states, ports, greenPorts);
2725
-
2726
- const targetPorts = isGreen ? greenPorts : ports;
2727
-
2728
- let currentOccupied = false;
2729
- for (const port of Object.values(targetPorts)) {
2730
- currentOccupied = await isPortTaken(port);
2731
- if (currentOccupied) {
2732
- break;
2733
- }
2734
- }
2735
-
2736
- if (currentOccupied) {
2737
- occupiedDids.add(b.meta.did);
2738
- }
2739
- } catch (error) {
2740
- logger.error('Failed to check ports occupied', { error, blockletDid: b.meta.did, isGreen });
2741
- }
2742
- });
2743
-
2744
- if (occupiedDids.size === 0) {
2745
- logger.info('No occupied ports detected, no refresh needed', { did, isGreen });
2746
- return blocklet;
2747
- }
2748
-
2749
- const componentDids = Array.from(occupiedDids);
2750
- const {
2751
- refreshed,
2752
- componentDids: actuallyRefreshedDids,
2753
- isInitialAssignment,
2754
- } = await states.blocklet.refreshBlockletPorts(did, componentDids, isGreen);
2755
-
2756
- // 只有真正刷新了端口才打印日志和更新环境
2757
- if (refreshed && actuallyRefreshedDids.length > 0) {
2758
- // 区分首次分配和冲突刷新,使用不同的日志信息
2759
- if (isInitialAssignment) {
2760
- logger.info('Assigned green ports for blue-green deployment', {
2761
- did,
2762
- componentDids: actuallyRefreshedDids,
2763
- isGreen,
2764
- });
2765
- } else {
2766
- logger.info('Refreshed component ports due to conflict', {
2767
- did,
2768
- componentDids: actuallyRefreshedDids,
2769
- isGreen,
2770
- });
2771
- }
2772
-
2773
- await manager._updateBlockletEnvironment(did);
2774
- const newBlocklet = await manager.ensureBlocklet(did);
2775
-
2776
- return newBlocklet;
2777
- }
2778
-
2779
- logger.info('Ports were detected as occupied but not actually occupied during refresh, no refresh needed', {
2780
- did,
2781
- componentDids,
2782
- isGreen,
2783
- });
2784
-
2785
- return blocklet;
2786
- } finally {
2787
- await portAssignLock.releaseLock(lockName);
2788
- }
2789
- };
2790
-
2791
- const getComponentNamesWithVersion = (app = {}, componentDids = []) => {
2792
- const str = uniq(componentDids)
2793
- .map((x) => {
2794
- const component = (app.children || []).find((y) => y.meta.did === x);
2795
- return `${component.meta.title}@${component.meta.version}`;
2796
- })
2797
- .join(', ');
2798
- return str;
2799
- };
2800
-
2801
- const getSlpDid = (serverDid, appPid) => {
2802
- if (!serverDid || !appPid) {
2803
- throw new Error('serverDid and appPid is required');
2804
- }
2805
-
2806
- const buffer = Buffer.concat([toBuffer(serverDid), toBuffer(appPid)]);
2807
- const md5Str = md5(buffer);
2808
-
2809
- const wallet = fromPublicKey(md5Str);
2810
- return wallet.address;
2811
- };
2812
-
2813
- // eslint-disable-next-line require-await
2814
- const publishDidDocument = async ({ blocklet, ownerInfo, nodeInfo }) => {
2815
- const alsoKnownAs = getBlockletKnownAs(blocklet);
2816
- logger.debug('updateDidDocument blocklet info', { blocklet });
2817
-
2818
- const { wallet } = getBlockletInfo(blocklet, nodeInfo.sk);
2819
- const { mode, did: serverDid } = nodeInfo;
2820
-
2821
- let slpDid = null;
2822
- const enableSlpDomain = shouldEnableSlpDomain(mode);
2823
- if (enableSlpDomain) {
2824
- slpDid = getSlpDid(serverDid, blocklet.appPid);
2825
-
2826
- if (alsoKnownAs.indexOf(slpDid) === -1) {
2827
- alsoKnownAs.push(toDid(slpDid));
2828
- }
2829
- }
2830
-
2831
- logger.info('update did document', {
2832
- blockletDid: blocklet.meta.did,
2833
- alsoKnownAs,
2834
- slpDid,
2835
- daemonDidDomain: getServerDidDomain(nodeInfo),
2836
- didRegistryUrl: nodeInfo.didRegistry,
2837
- domain: nodeInfo.didDomain,
2838
- slpDomain: nodeInfo.slpDomain,
2839
- });
2840
-
2841
- const name = blocklet.meta?.title || blocklet.meta?.name;
2842
- const state = fromBlockletStatus(blocklet.status);
2843
-
2844
- let launcher;
2845
- if (!isEmpty(blocklet.controller)) {
2846
- launcher = {
2847
- did: toDid(blocklet.controller.did || nodeInfo.registerInfo.appPid), // 目前 controller 没有 launcher 的元信息, 默认在 nodeInfo 中存储
2848
- name: blocklet.controller.launcherName || nodeInfo.registerInfo.appName || '',
2849
- url: blocklet.controller.launcherUrl || nodeInfo.registerInfo.appUrl || '',
2850
- userDid: toDid(blocklet.controller.nftOwner),
2851
- };
2852
- }
2853
-
2854
- const isPrimaryDomain = (d) => {
2855
- const appUrl = blocklet.environments.find((x) => x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_URL)?.value;
2856
- try {
2857
- const url = new URL(appUrl);
2858
- return url.hostname === d;
2859
- } catch (error) {
2860
- logger.error('failed to get primary domain', { error, domain: d, appUrl });
2861
- return false;
2862
- }
2863
- };
2864
-
2865
- const domains = await Promise.all(
2866
- (blocklet.site?.domainAliases || []).map(async (item) => {
2867
- let type = isCustomDomain(item.value) ? 'custom' : 'internal';
2868
- // 如果域名是 appUrl,则设置为 primary
2869
- if (isPrimaryDomain(item.value)) {
2870
- type = 'primary';
2871
- }
2872
-
2873
- if (item.value.includes(SLOT_FOR_IP_DNS_SITE)) {
2874
- const nodeIp = await getAccessibleExternalNodeIp();
2875
- item.value = replaceDomainSlot({ domain: item.value, nodeIp });
2876
- }
2877
-
2878
- return {
2879
- type,
2880
- host: item.value,
2881
- url: `https://${item.value}`,
2882
- source: 'dnsRecords', // 固定为 dnsRecords
2883
- };
2884
- })
2885
- );
2886
-
2887
- let owner;
2888
- if (ownerInfo) {
2889
- owner = {
2890
- did: toDid(ownerInfo.did),
2891
- name: ownerInfo.fullName,
2892
- avatar: fixAvatar(ownerInfo.avatar),
2893
- };
2894
- }
2895
-
2896
- return didDocument.updateBlockletDocument({
2897
- blocklet,
2898
- wallet,
2899
- alsoKnownAs,
2900
- slpDid,
2901
- daemonDidDomain: getServerDidDomain(nodeInfo),
2902
- didRegistryUrl: nodeInfo.didRegistry,
2903
- domain: nodeInfo.didDomain,
2904
- slpDomain: nodeInfo.slpDomain,
2905
- serverDid,
2906
- blockletServerVersion: nodeInfo.version,
2907
- name,
2908
- state,
2909
- owner,
2910
- launcher,
2911
- domains,
2912
- });
2913
- };
2914
-
2915
- const updateDidDocument = async ({ did, nodeInfo, teamManager, states }) => {
2916
- const blocklet = await states.blocklet.getBlocklet(did);
2917
- const blockletExtra = await states.blockletExtras.findOne({ did });
2918
-
2919
- blocklet.site = await states.site.findOneByBlocklet(did);
2920
- blocklet.settings = await states.blockletExtras.getSettings(did);
2921
- blocklet.controller = blockletExtra?.controller;
2922
-
2923
- const ownerDid = blocklet.settings?.owner?.did;
2924
- let ownerInfo;
2925
- if (ownerDid) {
2926
- logger.info('get owner info', { ownerDid, teamDid: blocklet.appPid });
2927
- const userState = await teamManager.getUserState(blocklet.appPid);
2928
- ownerInfo = await userState.getUser(ownerDid);
2929
- }
2930
-
2931
- return publishDidDocument({ blocklet, ownerInfo, nodeInfo });
2932
- };
2933
-
2934
- // Update DID document state only (e.g., to 'deleted') without fetching from database
2935
- // Used when blocklet is being removed and database data may not be available
2936
- const updateDidDocumentStateOnly = ({ did, blocklet, state, nodeInfo }) => {
2937
- logger.debug('update did document state only', { did, state });
2938
-
2939
- const { wallet } = getBlockletInfo(blocklet, nodeInfo.sk);
2940
-
2941
- return didDocument.updateBlockletStateOnly({
2942
- did,
2943
- state,
2944
- didRegistryUrl: nodeInfo.didRegistry,
2945
- wallet,
2946
- blockletServerVersion: nodeInfo.version,
2947
- });
2948
- };
2949
-
2950
- const getAppConfigsFromComponent = (meta, configsInApp = [], configsInComponent = []) => {
2951
- const configs = [];
2952
- for (const configInMeta of meta?.environments || []) {
2953
- if (isEnvShareable(configInMeta)) {
2954
- const configInApp = (configsInApp || []).find((x) => x.key === configInMeta.name);
2955
- if (!configInApp) {
2956
- const configInComponent = configsInComponent.find((y) => y.key === configInMeta.name);
2957
- if (configInComponent && isEnvShareable(configInComponent)) {
2958
- configs.push(configInComponent);
2959
- }
2960
- }
2961
- }
2962
- }
2963
- return configs;
2964
- };
2965
-
2966
- const getConfigsFromInput = (configs = [], oldConfigs = []) => {
2967
- const sharedConfigs = [];
2968
- const selfConfigs = [];
2969
-
2970
- configs.forEach((config) => {
2971
- const oldConfig = oldConfigs.find((y) => y.key === config.key);
2972
- if (!config.key.startsWith(BLOCKLET_PREFERENCE_PREFIX) && (isEnvShareable(config) || isEnvShareable(oldConfig))) {
2973
- sharedConfigs.push(config);
2974
- } else {
2975
- selfConfigs.push(config);
2976
- }
2977
- });
2978
-
2979
- return { sharedConfigs, selfConfigs };
2980
- };
2981
-
2982
- // remove app configs if no component use it
2983
- const removeAppConfigsFromComponent = async (componentConfigs, app, blockletExtraState) => {
2984
- const appConfigs = app.configs || [];
2985
- const remainedConfigs = [].concat(...(app.children || []).map((x) => x.configs || []));
2986
- const removedAppConfigs = [];
2987
-
2988
- componentConfigs.forEach((config) => {
2989
- const appConfig = appConfigs.find((x) => x.key === config.key);
2990
- if (
2991
- appConfig &&
2992
- !appConfig.custom &&
2993
- !(app.meta.environments || []).find((x) => x.name === config.key) &&
2994
- !remainedConfigs.find((x) => x.key === config.key && isEnvShareable(x))
2995
- ) {
2996
- removedAppConfigs.push({ key: appConfig.key, value: undefined });
2997
- }
2998
- });
2999
-
3000
- if (removedAppConfigs.length) {
3001
- await blockletExtraState.setConfigs(app.meta.did, removedAppConfigs);
3002
- }
3003
- };
3004
-
3005
- const getPackComponent = (app) => {
3006
- return (app?.children || []).find((x) => x.meta.group === BlockletGroup.pack);
3007
- };
3008
-
3009
- const getPackConfig = (app) => {
3010
- const packComponent = getPackComponent(app);
3011
- if (!packComponent) {
3012
- return null;
3013
- }
3014
-
3015
- const resource = (packComponent.meta.resource?.bundles || []).find(
3016
- (x) => x.did === packComponent.meta.did && x.type === 'config'
3017
- );
3018
-
3019
- if (!resource) {
3020
- return null;
3021
- }
3022
-
3023
- const { appDir } = packComponent.env;
3024
- const configFile = path.join(appDir, BLOCKLET_RESOURCE_DIR, resource.did, resource.type, 'config.json');
3025
-
3026
- if (!fs.existsSync(configFile)) {
3027
- return null;
3028
- }
3029
-
3030
- return fs.readJSON(configFile);
3031
- };
3032
-
3033
- /** 复制打包文件中的图片
3034
- * @param {string} appDataDir
3035
- * @param {string} packDir
3036
- * @param {{ navigations: Array<{ section: string | string[], icon: string }>, configObj: Record<string, string> }} packConfig
3037
- */
3038
- const copyPackImages = async ({ appDataDir, packDir, packConfig = {} }) => {
3039
- const mediaDir = path.join(appDataDir, 'media', 'blocklet-service');
3040
- const { navigations = [], configObj = {} } = packConfig;
3041
- await fs.ensureDir(mediaDir);
3042
-
3043
- // 过滤出 bottomNavigation 的图标
3044
- const bottomNavItems = navigations.filter((item) => {
3045
- // 处理 section 字段为数组的情况
3046
- const sections = Array.isArray(item.section) ? item.section : [item.section];
3047
- return sections.includes('bottomNavigation') && item.icon;
3048
- });
3049
-
3050
- // 复制 tabbar 导航图标
3051
- if (bottomNavItems.length > 0) {
3052
- await Promise.all(
3053
- bottomNavItems.map(async (item) => {
3054
- const iconFileName = path.basename(item.icon);
3055
- const iconInImages = path.join(packDir, 'images', iconFileName);
3056
-
3057
- if (fs.existsSync(iconInImages)) {
3058
- await fs.copy(iconInImages, path.join(mediaDir, iconFileName));
3059
- }
3060
- })
3061
- );
3062
- }
3063
-
3064
- // 复制品牌相关图片
3065
- await Promise.all(
3066
- APP_CONFIG_IMAGE_KEYS.map(async (key) => {
3067
- const value = configObj[key];
3068
- if (value) {
3069
- const imgFile = path.join(packDir, 'images', value);
3070
- if (fs.existsSync(imgFile)) {
3071
- await fs.copy(imgFile, path.join(appDataDir, value));
3072
- }
3073
- }
3074
- })
3075
- );
3076
- };
3077
-
3078
- /**
3079
- * @param {import('@blocklet/server-js').BlockletState} blocklet
3080
- * @returns {boolean}
3081
- */
3082
- const isDevelopmentMode = (blocklet) => blocklet?.mode === BLOCKLET_MODES.DEVELOPMENT;
3083
-
3084
- const getHookArgs = (blocklet) => ({
3085
- output: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? '' : path.join(blocklet.env.logsDir, 'output.log'),
3086
- error: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? '' : path.join(blocklet.env.logsDir, 'error.log'),
3087
- timeout:
3088
- Math.max(
3089
- get(blocklet, 'meta.timeout.script', 120),
3090
- ...(blocklet?.children || []).map((child) => child.meta?.timeout?.script || 0)
3091
- ) * 1000,
3092
- });
3093
-
3094
125
  module.exports = {
3095
126
  updateBlockletFallbackLogo,
3096
127
  forEachBlocklet,
128
+ getDisplayName,
129
+ isExternalBlocklet,
3097
130
  getBlockletMetaFromUrl: (url) => getBlockletMetaFromUrl(url, { logger }),
3098
131
  parseComponents,
3099
132
  filterRequiredComponents,
@@ -3107,10 +140,10 @@ module.exports = {
3107
140
  validateBlockletChainInfo,
3108
141
  fillBlockletConfigs,
3109
142
  ensureBlockletExpanded,
3110
- startBlockletProcess,
3111
- stopBlockletProcess,
3112
- deleteBlockletProcess,
3113
- reloadBlockletProcess,
143
+ startBlockletProcess: _startBlockletProcess,
144
+ stopBlockletProcess: _stopBlockletProcess,
145
+ deleteBlockletProcess: _deleteBlockletProcess,
146
+ reloadBlockletProcess: _reloadBlockletProcess,
3114
147
  checkBlockletProcessHealthy,
3115
148
  isBlockletPortHealthy,
3116
149
  shouldCheckHealthy,
@@ -3169,5 +202,4 @@ module.exports = {
3169
202
  getBlockletConfigObj,
3170
203
  isDevelopmentMode,
3171
204
  resolveMountPointConflict,
3172
- deleteBlockletCache,
3173
205
  };