@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.
- package/lib/api/team/access-key-manager.js +104 -0
- package/lib/api/team/invitation-manager.js +461 -0
- package/lib/api/team/notification-manager.js +189 -0
- package/lib/api/team/oauth-manager.js +60 -0
- package/lib/api/team/org-crud-manager.js +202 -0
- package/lib/api/team/org-manager.js +56 -0
- package/lib/api/team/org-member-manager.js +403 -0
- package/lib/api/team/org-query-manager.js +126 -0
- package/lib/api/team/org-resource-manager.js +186 -0
- package/lib/api/team/passport-manager.js +670 -0
- package/lib/api/team/rbac-manager.js +335 -0
- package/lib/api/team/session-manager.js +540 -0
- package/lib/api/team/store-manager.js +198 -0
- package/lib/api/team/tag-manager.js +230 -0
- package/lib/api/team/user-auth-manager.js +132 -0
- package/lib/api/team/user-manager.js +78 -0
- package/lib/api/team/user-query-manager.js +299 -0
- package/lib/api/team/user-social-manager.js +354 -0
- package/lib/api/team/user-update-manager.js +224 -0
- package/lib/api/team/verify-code-manager.js +161 -0
- package/lib/api/team.js +439 -3287
- package/lib/blocklet/manager/disk/auth-manager.js +68 -0
- package/lib/blocklet/manager/disk/backup-manager.js +288 -0
- package/lib/blocklet/manager/disk/cleanup-manager.js +157 -0
- package/lib/blocklet/manager/disk/component-manager.js +83 -0
- package/lib/blocklet/manager/disk/config-manager.js +191 -0
- package/lib/blocklet/manager/disk/controller-manager.js +64 -0
- package/lib/blocklet/manager/disk/delete-reset-manager.js +328 -0
- package/lib/blocklet/manager/disk/download-manager.js +96 -0
- package/lib/blocklet/manager/disk/env-config-manager.js +311 -0
- package/lib/blocklet/manager/disk/federated-manager.js +651 -0
- package/lib/blocklet/manager/disk/hook-manager.js +124 -0
- package/lib/blocklet/manager/disk/install-component-manager.js +95 -0
- package/lib/blocklet/manager/disk/install-core-manager.js +448 -0
- package/lib/blocklet/manager/disk/install-download-manager.js +313 -0
- package/lib/blocklet/manager/disk/install-manager.js +36 -0
- package/lib/blocklet/manager/disk/install-upgrade-manager.js +340 -0
- package/lib/blocklet/manager/disk/job-manager.js +467 -0
- package/lib/blocklet/manager/disk/lifecycle-manager.js +26 -0
- package/lib/blocklet/manager/disk/notification-manager.js +343 -0
- package/lib/blocklet/manager/disk/query-manager.js +562 -0
- package/lib/blocklet/manager/disk/settings-manager.js +507 -0
- package/lib/blocklet/manager/disk/start-manager.js +611 -0
- package/lib/blocklet/manager/disk/stop-restart-manager.js +292 -0
- package/lib/blocklet/manager/disk/update-manager.js +153 -0
- package/lib/blocklet/manager/disk.js +669 -5796
- package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +5 -0
- package/lib/blocklet/manager/lock.js +18 -0
- package/lib/event/index.js +28 -24
- package/lib/util/blocklet/app-utils.js +192 -0
- package/lib/util/blocklet/blocklet-loader.js +258 -0
- package/lib/util/blocklet/config-manager.js +232 -0
- package/lib/util/blocklet/did-document.js +240 -0
- package/lib/util/blocklet/environment.js +555 -0
- package/lib/util/blocklet/health-check.js +449 -0
- package/lib/util/blocklet/install-utils.js +365 -0
- package/lib/util/blocklet/logo.js +57 -0
- package/lib/util/blocklet/meta-utils.js +269 -0
- package/lib/util/blocklet/port-manager.js +141 -0
- package/lib/util/blocklet/process-manager.js +504 -0
- package/lib/util/blocklet/runtime-info.js +105 -0
- package/lib/util/blocklet/validation.js +418 -0
- package/lib/util/blocklet.js +98 -3066
- package/lib/util/wallet-app-notification.js +40 -0
- package/package.json +22 -22
package/lib/util/blocklet.js
CHANGED
|
@@ -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
|
|
64
|
-
const {
|
|
65
|
-
const {
|
|
66
|
-
const {
|
|
67
|
-
const {
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
*
|
|
1071
|
-
* @param {*} blocklet should contain env props
|
|
96
|
+
* Wrapper for startBlockletProcess - injects local dependencies
|
|
1072
97
|
*/
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1179
|
-
* @returns {BlockletStatus}
|
|
117
|
+
* Wrapper for checkBlockletProcessHealthy - injects local dependency findInterfacePortByName
|
|
1180
118
|
*/
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
};
|