@abtnode/core 1.15.17 → 1.16.0-beta-b16cb035

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/lib/api/node.js +67 -69
  2. package/lib/api/team.js +386 -55
  3. package/lib/blocklet/downloader/blocklet-downloader.js +226 -0
  4. package/lib/blocklet/downloader/bundle-downloader.js +272 -0
  5. package/lib/blocklet/downloader/constants.js +3 -0
  6. package/lib/blocklet/downloader/resolve-download.js +199 -0
  7. package/lib/blocklet/extras.js +83 -26
  8. package/lib/blocklet/hooks.js +18 -65
  9. package/lib/blocklet/manager/base.js +10 -16
  10. package/lib/blocklet/manager/disk.js +1679 -1566
  11. package/lib/blocklet/manager/helper/install-application-from-backup.js +177 -0
  12. package/lib/blocklet/manager/helper/install-application-from-dev.js +94 -0
  13. package/lib/blocklet/manager/helper/install-application-from-general.js +188 -0
  14. package/lib/blocklet/manager/helper/install-component-from-dev.js +84 -0
  15. package/lib/blocklet/manager/helper/install-component-from-upload.js +181 -0
  16. package/lib/blocklet/manager/helper/install-component-from-url.js +173 -0
  17. package/lib/blocklet/manager/helper/migrate-application-to-struct-v2.js +450 -0
  18. package/lib/blocklet/manager/helper/rollback-cache.js +41 -0
  19. package/lib/blocklet/manager/helper/upgrade-components.js +152 -0
  20. package/lib/blocklet/migration.js +30 -52
  21. package/lib/blocklet/storage/backup/audit-log.js +27 -0
  22. package/lib/blocklet/storage/backup/base.js +62 -0
  23. package/lib/blocklet/storage/backup/blocklet-extras.js +92 -0
  24. package/lib/blocklet/storage/backup/blocklet.js +70 -0
  25. package/lib/blocklet/storage/backup/blocklets.js +74 -0
  26. package/lib/blocklet/storage/backup/data.js +19 -0
  27. package/lib/blocklet/storage/backup/logs.js +24 -0
  28. package/lib/blocklet/storage/backup/routing-rule.js +19 -0
  29. package/lib/blocklet/storage/backup/spaces.js +240 -0
  30. package/lib/blocklet/storage/restore/base.js +67 -0
  31. package/lib/blocklet/storage/restore/blocklet-extras.js +86 -0
  32. package/lib/blocklet/storage/restore/blocklet.js +56 -0
  33. package/lib/blocklet/storage/restore/blocklets.js +43 -0
  34. package/lib/blocklet/storage/restore/logs.js +21 -0
  35. package/lib/blocklet/storage/restore/spaces.js +156 -0
  36. package/lib/blocklet/storage/utils/hash.js +51 -0
  37. package/lib/blocklet/storage/utils/zip.js +43 -0
  38. package/lib/cert.js +206 -0
  39. package/lib/event.js +237 -64
  40. package/lib/index.js +191 -83
  41. package/lib/migrations/1.0.21-update-config.js +1 -1
  42. package/lib/migrations/1.0.22-max-memory.js +1 -1
  43. package/lib/migrations/1.0.25.js +1 -1
  44. package/lib/migrations/1.0.32-update-config.js +1 -1
  45. package/lib/migrations/1.0.33-blocklets.js +1 -1
  46. package/lib/migrations/1.5.20-registry.js +15 -0
  47. package/lib/migrations/1.6.17-blocklet-children.js +48 -0
  48. package/lib/migrations/1.6.21-rename-ip-echo-domain.js +35 -0
  49. package/lib/migrations/1.6.4-security.js +59 -0
  50. package/lib/migrations/1.6.5-security.js +60 -0
  51. package/lib/migrations/1.6.9-update-node-info-and-certificate.js +38 -0
  52. package/lib/migrations/1.7.1-blocklet-setup.js +18 -0
  53. package/lib/migrations/1.7.12-blocklet-meta.js +51 -0
  54. package/lib/migrations/1.7.15-blocklet-bundle-source.js +42 -0
  55. package/lib/migrations/1.7.20-blocklet-component.js +41 -0
  56. package/lib/migrations/1.8.33-blocklet-mem-limit.js +20 -0
  57. package/lib/migrations/README.md +1 -1
  58. package/lib/migrations/index.js +6 -2
  59. package/lib/monitor/blocklet-runtime-monitor.js +200 -0
  60. package/lib/monitor/get-history-list.js +37 -0
  61. package/lib/monitor/node-runtime-monitor.js +228 -0
  62. package/lib/router/helper.js +572 -497
  63. package/lib/router/index.js +85 -21
  64. package/lib/router/manager.js +146 -187
  65. package/lib/states/README.md +36 -1
  66. package/lib/states/access-key.js +39 -17
  67. package/lib/states/audit-log.js +462 -0
  68. package/lib/states/base.js +4 -213
  69. package/lib/states/blocklet-extras.js +194 -138
  70. package/lib/states/blocklet.js +361 -104
  71. package/lib/states/cache.js +8 -6
  72. package/lib/states/challenge.js +5 -5
  73. package/lib/states/index.js +19 -36
  74. package/lib/states/migration.js +4 -4
  75. package/lib/states/node.js +135 -46
  76. package/lib/states/notification.js +22 -35
  77. package/lib/states/session.js +17 -9
  78. package/lib/states/site.js +50 -25
  79. package/lib/states/user.js +74 -20
  80. package/lib/states/webhook.js +10 -6
  81. package/lib/team/manager.js +124 -7
  82. package/lib/util/blocklet.js +1223 -246
  83. package/lib/util/chain.js +1 -1
  84. package/lib/util/default-node-config.js +5 -23
  85. package/lib/util/disk-monitor.js +13 -10
  86. package/lib/util/domain-status.js +84 -15
  87. package/lib/util/get-accessible-external-node-ip.js +2 -2
  88. package/lib/util/get-domain-for-blocklet.js +13 -0
  89. package/lib/util/get-meta-from-url.js +33 -0
  90. package/lib/util/index.js +207 -272
  91. package/lib/util/ip.js +6 -0
  92. package/lib/util/maintain.js +233 -0
  93. package/lib/util/public-to-store.js +85 -0
  94. package/lib/util/ready.js +1 -1
  95. package/lib/util/requirement.js +28 -9
  96. package/lib/util/reset-node.js +22 -7
  97. package/lib/util/router.js +13 -0
  98. package/lib/util/rpc.js +16 -0
  99. package/lib/util/store.js +179 -0
  100. package/lib/util/sysinfo.js +44 -0
  101. package/lib/util/ua.js +54 -0
  102. package/lib/validators/blocklet-extra.js +24 -0
  103. package/lib/validators/node.js +25 -12
  104. package/lib/validators/permission.js +16 -1
  105. package/lib/validators/role.js +17 -3
  106. package/lib/validators/router.js +40 -20
  107. package/lib/validators/trusted-passport.js +1 -0
  108. package/lib/validators/util.js +22 -5
  109. package/lib/webhook/index.js +45 -35
  110. package/lib/webhook/sender/index.js +5 -0
  111. package/lib/webhook/sender/slack/index.js +1 -1
  112. package/lib/webhook/sender/wallet/index.js +48 -0
  113. package/package.json +54 -36
  114. package/lib/blocklet/registry.js +0 -205
  115. package/lib/states/https-cert.js +0 -67
  116. package/lib/util/get-ip-dns-domain-for-blocklet.js +0 -19
  117. package/lib/util/service.js +0 -66
  118. package/lib/util/upgrade.js +0 -178
  119. /package/lib/{queue.js → util/queue.js} +0 -0
@@ -2,55 +2,73 @@
2
2
  /* eslint-disable no-await-in-loop */
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
+ const flat = require('flat');
5
6
  const get = require('lodash/get');
7
+ const merge = require('lodash/merge');
8
+ const pick = require('lodash/pick');
6
9
  const cloneDeep = require('lodash/cloneDeep');
7
- const semver = require('semver');
8
- const capitalize = require('lodash/capitalize');
9
- const diff = require('deep-diff');
10
- const { Throttle } = require('stream-throttle');
11
- const LRU = require('lru-cache');
12
-
13
- const { isValid: isValidDid } = require('@arcblock/did');
14
- const { verifyPresentation } = require('@arcblock/vc');
15
- const { toBase58 } = require('@ocap/util');
16
- const { fromSecretKey } = require('@ocap/wallet');
10
+ const { isNFTExpired, getNftExpirationDate } = require('@abtnode/util/lib/nft');
11
+ const didDocument = require('@abtnode/util/lib/did-document');
12
+ const { sign } = require('@arcblock/jwt');
13
+ const { toSvg: createDidLogo } =
14
+ process.env.NODE_ENV !== 'test' ? require('@arcblock/did-motif') : require('@arcblock/did-motif/dist/did-motif.cjs');
15
+ const getBlockletInfo = require('@blocklet/meta/lib/info');
16
+ const sleep = require('@abtnode/util/lib/sleep');
17
17
 
18
18
  const logger = require('@abtnode/logger')('@abtnode/core:blocklet:manager');
19
- const hashFiles = require('@abtnode/util/lib/hash-files');
20
- const downloadFile = require('@abtnode/util/lib/download-file');
21
- const Lock = require('@abtnode/util/lib/lock');
22
- const { getVcFromPresentation } = require('@abtnode/util/lib/vc');
23
- const { BLOCKLET_PURCHASE_NFT_TYPE } = require('@abtnode/constant');
19
+ const {
20
+ WHO_CAN_ACCESS,
21
+ WHO_CAN_ACCESS_PREFIX_ROLES,
22
+ BLOCKLET_INSTALL_TYPE,
23
+ NODE_MODES,
24
+ APP_STRUCT_VERSION,
25
+ } = require('@abtnode/constant');
24
26
 
25
27
  const getBlockletEngine = require('@blocklet/meta/lib/engine');
26
- const { isFreeBlocklet } = require('@blocklet/meta/lib/payment');
27
- const validateBlockletEntry = require('@blocklet/meta/lib/entry');
28
- const { getRequiredMissingConfigs } = require('@blocklet/meta/lib/util');
28
+ const {
29
+ isDeletableBlocklet,
30
+ getAppMissingConfigs,
31
+ hasRunnableComponent,
32
+ forEachBlockletSync,
33
+ forEachChildSync,
34
+ forEachBlocklet,
35
+ getComponentId,
36
+ isPreferenceKey,
37
+ getRolesFromAuthConfig,
38
+ } = require('@blocklet/meta/lib/util');
39
+ const getComponentProcessId = require('@blocklet/meta/lib/get-component-process-id');
40
+ const { update: updateMetaFile } = require('@blocklet/meta/lib/file');
41
+ const { titleSchema, updateMountPointSchema, environmentNameSchema } = require('@blocklet/meta/lib/schema');
42
+ const Lock = require('@abtnode/util/lib/lock');
29
43
 
30
44
  const {
31
45
  BlockletStatus,
32
46
  BlockletSource,
33
47
  BlockletEvents,
34
- BLOCKLET_BUNDLE_FOLDER,
35
48
  BLOCKLET_MODES,
36
49
  BlockletGroup,
37
50
  fromBlockletStatus,
38
51
  fromBlockletSource,
39
- } = require('@blocklet/meta/lib/constants');
52
+ BLOCKLET_DEFAULT_PORT_NAME,
53
+ BLOCKLET_INTERFACE_TYPE_WEB,
54
+ BLOCKLET_INTERFACE_PUBLIC,
55
+ BLOCKLET_DYNAMIC_PATH_PREFIX,
56
+ BLOCKLET_INTERFACE_PROTOCOL_HTTP,
57
+ BLOCKLET_DEFAULT_PATH_REWRITE,
58
+ BLOCKLET_DEFAULT_VERSION,
59
+ BLOCKLET_LATEST_SPEC_VERSION,
60
+ BLOCKLET_META_FILE,
61
+ BLOCKLET_CONFIGURABLE_KEY,
62
+ } = require('@blocklet/constant');
40
63
  const util = require('../../util');
41
64
  const {
42
65
  refresh: refreshAccessibleExternalNodeIp,
43
66
  getFromCache: getAccessibleExternalNodeIp,
44
67
  } = require('../../util/get-accessible-external-node-ip');
45
68
  const {
46
- forEachBlocklet,
47
- getBlockletDirs,
48
- getBlockletMetaFromUrl,
49
- fillBlockletConfigs,
50
- ensureBlockletExpanded,
51
- getRootSystemEnvironments,
52
- getSystemEnvironments,
53
- getOverwrittenEnvironments,
69
+ getAppSystemEnvironments,
70
+ getComponentSystemEnvironments,
71
+ getAppOverwrittenEnvironments,
54
72
  getHealthyCheckTimeout,
55
73
  startBlockletProcess,
56
74
  stopBlockletProcess,
@@ -59,39 +77,59 @@ const {
59
77
  getBlockletStatusFromProcess,
60
78
  checkBlockletProcessHealthy,
61
79
  validateBlocklet,
62
- getChildrenMeta,
63
80
  statusMap,
64
- expandTarball,
65
- verifyIntegrity,
66
81
  pruneBlockletBundle,
67
82
  getDiskInfo,
68
- getRuntimeInfo,
69
- mergeMeta,
70
- getUpdateMetaList,
71
83
  getRuntimeEnvironments,
84
+ getTypeFromInstallParams,
85
+ parseComponents,
86
+ filterDuplicateComponents,
87
+ getBundleDir,
88
+ getBlocklet,
89
+ ensureEnvDefault,
90
+ getConfigFromPreferences,
91
+ consumeServerlessNFT,
92
+ validateAppConfig,
93
+ checkDuplicateMountPoint,
94
+ validateStore,
95
+ isRotatingAppSk,
96
+ isRotatingAppDid,
97
+ checkVersionCompatibility,
98
+ getBlockletKnownAs,
72
99
  } = require('../../util/blocklet');
73
100
  const states = require('../../states');
74
- const BlockletRegistry = require('../registry');
75
101
  const BaseBlockletManager = require('./base');
76
102
  const { get: getEngine } = require('./engine');
77
103
  const blockletPm2Events = require('./pm2-events');
78
- const { getFactoryState } = require('../../util/chain');
79
104
  const runMigrationScripts = require('../migration');
80
105
  const hooks = require('../hooks');
106
+ const { getDidDomainForBlocklet } = require('../../util/get-domain-for-blocklet');
107
+ const handleInstanceInStore = require('../../util/public-to-store');
108
+ const { BlockletRuntimeMonitor } = require('../../monitor/blocklet-runtime-monitor');
109
+ const getHistoryList = require('../../monitor/get-history-list');
110
+ const { SpacesBackup } = require('../storage/backup/spaces');
111
+ const { SpacesRestore } = require('../storage/restore/spaces');
112
+ const { installApplicationFromGeneral } = require('./helper/install-application-from-general');
113
+ const { installApplicationFromDev } = require('./helper/install-application-from-dev');
114
+ const { installApplicationFromBackup } = require('./helper/install-application-from-backup');
115
+ const { installComponentFromDev } = require('./helper/install-component-from-dev');
116
+ const { installComponentFromUrl } = require('./helper/install-component-from-url');
117
+ const { installComponentFromUpload, diff } = require('./helper/install-component-from-upload');
118
+ const UpgradeComponents = require('./helper/upgrade-components');
119
+ const BlockletDownloader = require('../downloader/blocklet-downloader');
120
+ const RollbackCache = require('./helper/rollback-cache');
121
+ const { migrateApplicationToStructV2 } = require('./helper/migrate-application-to-struct-v2');
81
122
 
82
123
  const {
83
124
  isInProgress,
84
125
  isBeforeInstalled,
85
- getBlockletInterfaces,
86
126
  formatEnvironments,
87
127
  shouldUpdateBlockletStatus,
88
128
  getBlockletMeta,
89
- validateBlockletMeta,
129
+ validateOwner,
90
130
  } = util;
91
131
 
92
- const preDownloadLock = new Lock('pre-download-lock');
93
-
94
- const asyncFs = fs.promises;
132
+ const statusLock = new Lock('blocklet-status-lock');
95
133
 
96
134
  const pm2StatusMap = {
97
135
  online: BlockletStatus.running,
@@ -105,42 +143,64 @@ const pm2StatusMap = {
105
143
  */
106
144
  const getBlockletEngineNameByPlatform = (blockletMeta) => getBlockletEngine(blockletMeta).interpreter;
107
145
 
146
+ const getSkippedProcessIds = ({ newBlocklet, oldBlocklet, context = {} }) => {
147
+ const { forceStartProcessIds = [] } = context;
148
+ const idMap = {};
149
+ const res = [];
150
+
151
+ forEachBlockletSync(oldBlocklet, (b, { ancestors }) => {
152
+ if (b.meta.dist?.integrity) {
153
+ idMap[getComponentProcessId(b, ancestors)] = b.meta.dist?.integrity;
154
+ }
155
+ });
156
+
157
+ forEachBlockletSync(newBlocklet, (b, { ancestors }) => {
158
+ const id = getComponentProcessId(b, ancestors);
159
+ if (forceStartProcessIds.includes(id)) {
160
+ return;
161
+ }
162
+
163
+ if (!b.meta.dist?.integrity || b.meta.dist.integrity === idMap[id]) {
164
+ res.push(id);
165
+ }
166
+ });
167
+
168
+ return res;
169
+ };
170
+
171
+ // 10s 上报统计一次
172
+ const MONITOR_RECORD_INTERVAL_SEC = 10;
173
+
174
+ // 保存当天数据, 每天上报 8640 次
175
+ const MONITOR_HISTORY_LENGTH = 86400 / MONITOR_RECORD_INTERVAL_SEC;
176
+
108
177
  class BlockletManager extends BaseBlockletManager {
109
178
  /**
110
179
  * @param {*} dataDirs generate by ../../util:getDataDirs
111
180
  */
112
- constructor({ dataDirs, registry, startQueue, installQueue, daemon = false }) {
181
+ constructor({ dataDirs, startQueue, installQueue, backupQueue, daemon = false, teamManager }) {
113
182
  super();
114
183
 
115
184
  this.dataDirs = dataDirs;
116
185
  this.installDir = dataDirs.blocklets;
117
- this.state = states.blocklet;
118
- this.node = states.node;
119
186
  this.startQueue = startQueue;
120
187
  this.installQueue = installQueue;
121
- /**
122
- * { did: Map({ <childDid>: <downloadFile.cancelCtrl> }) }
123
- */
124
- this.downloadCtrls = {};
125
- /**
126
- * { [download-did-version]: Lock }
127
- */
128
- this.downloadLocks = {};
129
- this.registry = registry;
130
- this.notification = states.notification;
131
- this.session = states.session;
132
- this.extras = states.blockletExtras;
133
- this.cache = states.cache;
188
+ this.backupQueue = backupQueue;
189
+ this.teamManager = teamManager;
134
190
 
135
191
  // cached installed blocklets for performance
136
192
  this.cachedBlocklets = null;
137
193
 
138
- // cached blocklet latest versions from each registries
139
- this.cachedBlockletVersions = new LRU({
140
- max: 40, // cache at most 40 blocklets
141
- maxAge: process.env.NODE_ENV === 'test' ? 500 : 5 * 60 * 1000, // cache for 5 minute
194
+ this.runtimeMonitor = new BlockletRuntimeMonitor({ historyLength: MONITOR_HISTORY_LENGTH, states });
195
+
196
+ this.blockletDownloader = new BlockletDownloader({
197
+ installDir: this.installDir,
198
+ downloadDir: this.dataDirs.tmp,
199
+ cache: states.cache,
142
200
  });
143
201
 
202
+ this._rollbackCache = new RollbackCache({ dir: this.dataDirs.tmp });
203
+
144
204
  if (daemon) {
145
205
  blockletPm2Events.on('online', (data) => this._syncPm2Status('online', data.blockletDid));
146
206
  blockletPm2Events.on('stop', (data) => this._syncPm2Status('stop', data.blockletDid));
@@ -148,126 +208,278 @@ class BlockletManager extends BaseBlockletManager {
148
208
  }
149
209
 
150
210
  // ============================================================================================
151
- // Public API that can be call from GQL, should have the same signature: doXXX(params, context)
211
+ // Public API for Installing/Upgrading Application or Components
152
212
  // ============================================================================================
153
213
 
154
214
  /**
155
- * @param {Object} params
156
- * @param {string} params.installId
157
- * @param {string} params.sync default: false
215
+ *
216
+ *
217
+ * @param {{
218
+ * url: string;
219
+ * did: string;
220
+ * title: string;
221
+ * description: string;
222
+ * storeUrl: string;
223
+ * appSk: string;
224
+ * sync: boolean = false; // download synchronously, not use queue
225
+ * delay: number; // push download task to queue after a delay
226
+ * downloadTokenList: Array<{did: string, token: string}>;
227
+ * startImmediately: boolean;
228
+ * controller: Controller
229
+ * type: BLOCKLET_INSTALL_TYPE
230
+ * }} params
231
+ * @param {{
232
+ * [key: string]: any
233
+ * }} context
234
+ * @return {*}
235
+ * @memberof BlockletManager
158
236
  */
159
- async install(params, context) {
237
+ async install(params, context = {}) {
160
238
  logger.debug('install blocklet', { params, context });
161
- if (params.url) {
162
- return this._installFromUrl({ url: params.url, sync: params.sync }, context);
239
+
240
+ const type = getTypeFromInstallParams(params);
241
+
242
+ const { appSk } = params;
243
+ if (!appSk) {
244
+ throw new Error('appSk is required');
245
+ }
246
+
247
+ if (!params.controller && context?.user?.controller) {
248
+ params.controller = context.user.controller;
249
+ }
250
+
251
+ const info = await states.node.read();
252
+
253
+ // Note: if you added new header here, please change core/state/lib/blocklet/downloader/bundle-downloader.js to use that header
254
+ context.headers = Object.assign(context?.headers || {}, {
255
+ 'x-server-did': info.did,
256
+ 'x-server-public-key': info.pk,
257
+ 'x-server-signature': sign(info.did, info.sk, {
258
+ exp: (Date.now() + 5 * 60 * 1000) / 1000,
259
+ }),
260
+ });
261
+ context.downloadTokenList = params.downloadTokenList || [];
262
+
263
+ if (typeof context.startImmediately === 'undefined') {
264
+ context.startImmediately = !!params.startImmediately;
163
265
  }
164
266
 
165
- if (params.file) {
166
- const { file, did, diffVersion, deleteSet } = params;
167
- return this._installFromUpload({ file, did, diffVersion, deleteSet, context });
267
+ if (type === BLOCKLET_INSTALL_TYPE.RESTORE) {
268
+ const { url } = params;
269
+ return installApplicationFromBackup({ url, appSk, context, manager: this, states });
168
270
  }
169
271
 
170
- if (params.did) {
171
- return this._installFromRegistry({ did: params.did }, context);
272
+ if ([BLOCKLET_INSTALL_TYPE.URL, BLOCKLET_INSTALL_TYPE.STORE, BLOCKLET_INSTALL_TYPE.CREATE].includes(type)) {
273
+ return installApplicationFromGeneral({ ...params, type, context, manager: this, states });
172
274
  }
173
275
 
174
- throw new Error('Can only install blocklet from url/file/did');
276
+ // should not be here
277
+ throw new Error(`install from ${type} is not supported`);
175
278
  }
176
279
 
177
- // eslint-disable-next-line no-unused-vars
178
- async getMetaFromUrl({ url, checkPrice = false }, context) {
179
- const meta = await getBlockletMetaFromUrl(url);
180
- let isFree = isFreeBlocklet(meta);
280
+ /**
281
+ * @param {String} rootDid
282
+ * @param {String} mountPoint
283
+ *
284
+ * installFromUrl
285
+ * @param {String} url
286
+ *
287
+ * InstallFromUpload
288
+ * @param {Object} file
289
+ * @param {String} did for diff upload or custom component did
290
+ * @param {String} diffVersion for diff upload
291
+ * @param {Array} deleteSet for diff upload
292
+ *
293
+ * Custom info
294
+ * @param {String} title custom component title
295
+ * @param {String} name custom component name
296
+ *
297
+ * @param {ConfigEntry} configs pre configs
298
+ */
299
+ async installComponent(
300
+ {
301
+ rootDid,
302
+ mountPoint: tmpMountPoint,
303
+ url,
304
+ file,
305
+ did,
306
+ diffVersion,
307
+ deleteSet,
308
+ title,
309
+ name,
310
+ configs,
311
+ sync,
312
+ downloadTokenList,
313
+ },
314
+ context = {}
315
+ ) {
316
+ const mountPoint = await updateMountPointSchema.validateAsync(tmpMountPoint);
317
+ logger.debug('start install component', { rootDid, mountPoint, url });
181
318
 
182
- if (checkPrice && !isFree && meta.nftFactory) {
183
- try {
184
- const registryMeta = await BlockletRegistry.getRegistryMeta(new URL(url).origin);
319
+ if (file) {
320
+ // TODO: 如何触发这种场景?
321
+ const info = await states.node.read();
322
+ if (info.mode === NODE_MODES.SERVERLESS) {
323
+ throw new Error("Can't install component in serverless-mode server via upload");
324
+ }
185
325
 
186
- if (registryMeta.chainHost) {
187
- const state = await getFactoryState(registryMeta.chainHost, meta.nftFactory);
188
- if (state) {
189
- isFree = false;
190
- }
191
- }
192
- } catch (error) {
193
- logger.warn('failed when checking if the blocklet is free', { did: meta.did, error });
326
+ return installComponentFromUpload({
327
+ rootDid,
328
+ mountPoint,
329
+ file,
330
+ did,
331
+ diffVersion,
332
+ deleteSet,
333
+ context,
334
+ states,
335
+ manager: this,
336
+ });
337
+ }
338
+
339
+ if (url) {
340
+ const info = await states.node.read();
341
+ if (info.mode === NODE_MODES.SERVERLESS) {
342
+ validateStore(info, url);
194
343
  }
344
+
345
+ return installComponentFromUrl({
346
+ rootDid,
347
+ mountPoint,
348
+ url,
349
+ context,
350
+ title,
351
+ did,
352
+ name,
353
+ configs,
354
+ sync,
355
+ downloadTokenList,
356
+ states,
357
+ manager: this,
358
+ });
195
359
  }
196
360
 
197
- meta.isFree = isFree;
361
+ // should not be here
362
+ throw new Error('Unknown source');
363
+ }
198
364
 
199
- return meta;
365
+ async diff({ did, hashFiles, rootDid }) {
366
+ return diff({ did, hashFiles, rootDid, states, manager: this });
200
367
  }
201
368
 
202
- async installBlockletFromVc({ vcPresentation, challenge }, context) {
203
- logger.info('Install from vc');
204
- const vc = getVcFromPresentation(vcPresentation);
369
+ /**
370
+ * After the dev function finished, the caller should send a BlockletEvents.installed event to the daemon
371
+ * @returns {Object} blocklet
372
+ */
373
+ async dev(folder, { rootDid, mountPoint, defaultStoreUrl } = {}) {
374
+ logger.info('dev component', { folder, rootDid, mountPoint });
205
375
 
206
- // FIXME: 这里的 trustedIssuers 相当于相信任何 VC,需要想更安全的方法
207
- verifyPresentation({ presentation: vcPresentation, trustedIssuers: [get(vc, 'issuer.id')], challenge });
376
+ const meta = getBlockletMeta(folder, { defaultStoreUrl });
377
+ if (meta.group !== 'static' && (!meta.scripts || !meta.scripts.dev)) {
378
+ throw new Error('Incorrect blocklet.yml: missing `scripts.dev` field');
379
+ }
208
380
 
209
- if (!vc.type.includes(BLOCKLET_PURCHASE_NFT_TYPE)) {
210
- throw new Error(`Expect ${BLOCKLET_PURCHASE_NFT_TYPE} VC type`);
381
+ if (!rootDid) {
382
+ return installApplicationFromDev({ folder, meta, manager: this, states });
211
383
  }
212
384
 
213
- const blockletUrl = get(vc, 'credentialSubject.purchased.blocklet.url');
214
- const urlObject = new URL(blockletUrl);
215
- const did = get(vc, 'credentialSubject.purchased.blocklet.id');
216
- const registry = urlObject.origin;
385
+ return installComponentFromDev({ folder, meta, rootDid, mountPoint, manager: this, states });
386
+ }
387
+
388
+ async checkComponentsForUpdates({ did }) {
389
+ return UpgradeComponents.check({ did, states });
390
+ }
391
+
392
+ async upgradeComponents({ updateId, selectedComponents: selectedComponentDids }, context = {}) {
393
+ return UpgradeComponents.upgrade({ updateId, selectedComponentDids, context, states, manager: this });
394
+ }
395
+
396
+ async migrateApplicationToStructV2({ did, appSk, context = {} }) {
397
+ return migrateApplicationToStructV2({ did, appSk, context, manager: this, states });
398
+ }
399
+
400
+ // ============================================================================================
401
+ // Public API for GQL or internal
402
+ // ============================================================================================
217
403
 
218
- return this._installFromRegistry({ did, registry }, { ...context, blockletPurchaseVerified: true });
404
+ async getBlockletForLauncher({ did }) {
405
+ const blocklet = await states.blocklet.getBlocklet(did);
406
+ const isRunning = blocklet ? blocklet.status === BlockletStatus.running : false;
407
+ return { did, isInstalled: !!blocklet, isRunning };
219
408
  }
220
409
 
221
- async start({ did, checkHealthImmediately = false, throwOnError }, context) {
410
+ async start({ did, throwOnError, checkHealthImmediately = false, e2eMode = false }, context) {
222
411
  logger.info('start blocklet', { did });
223
- const blocklet = await this.ensureBlocklet(did);
412
+ // should check blocklet integrity
413
+ const blocklet = await this.ensureBlocklet(did, { e2eMode });
224
414
 
225
415
  try {
416
+ // blocklet may be manually stopped durning starting
417
+ // so error message would not be sent if blocklet is stopped
418
+ // so we need update status first
419
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.starting);
420
+ blocklet.status = BlockletStatus.starting;
421
+
422
+ // validate requirement and engine
423
+ await validateBlocklet(blocklet);
424
+
425
+ if (!hasRunnableComponent(blocklet)) {
426
+ throw new Error('No runnable component found');
427
+ }
428
+
226
429
  // check required config
227
- const missingProps = getRequiredMissingConfigs(blocklet);
430
+ const missingProps = getAppMissingConfigs(blocklet);
228
431
  if (missingProps.length) {
229
432
  throw new Error(
230
433
  `Missing required configuration to start the blocklet: ${missingProps.map((x) => x.key).join(',')}`
231
434
  );
232
435
  }
233
436
 
234
- // update status
235
- await this.state.setBlockletStatus(did, BlockletStatus.starting);
236
- blocklet.status = BlockletStatus.starting;
237
437
  this.emit(BlockletEvents.statusChange, blocklet);
238
438
 
239
439
  if (blocklet.mode === BLOCKLET_MODES.DEVELOPMENT) {
240
440
  const { logsDir } = blocklet.env;
241
- fs.removeSync(logsDir);
242
- fs.mkdirSync(logsDir, { recursive: true });
441
+
442
+ try {
443
+ fs.removeSync(logsDir);
444
+ fs.mkdirSync(logsDir, { recursive: true });
445
+ } catch {
446
+ // Windows && Node.js 18.x 下会发生删除错误(ENOTEMPTY)
447
+ // 但是这个错误并不影响后续逻辑,所以这里对这个错误做了 catch
448
+ }
243
449
  }
244
450
 
245
- // start process
246
- const nodeEnvironments = await this.node.getEnvironments();
247
- await startBlockletProcess(blocklet, {
248
- preStart: (b) =>
249
- hooks.preStart(b, {
451
+ const getHookFn =
452
+ (hookName) =>
453
+ (b, { env }) =>
454
+ hooks[hookName](b, {
250
455
  appDir: b.env.appDir,
251
456
  hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
252
- env: getRuntimeEnvironments(b, nodeEnvironments),
457
+ env,
253
458
  did, // root blocklet did,
254
- progress: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT,
255
- }),
459
+ });
460
+
461
+ // start process
462
+ const nodeEnvironments = await states.node.getEnvironments();
463
+ await startBlockletProcess(blocklet, {
464
+ ...context,
465
+ preStart: getHookFn('preStart'),
466
+ postStart: getHookFn('postStart'),
256
467
  nodeEnvironments,
257
- nodeInfo: await this.node.read(),
468
+ nodeInfo: await states.node.read(),
469
+ e2eMode,
258
470
  });
259
471
 
260
472
  // check blocklet healthy
261
473
  const { startTimeout, minConsecutiveTime } = getHealthyCheckTimeout(blocklet, { checkHealthImmediately });
262
474
  const params = {
263
- blocklet,
475
+ did,
264
476
  context,
265
477
  minConsecutiveTime,
266
478
  timeout: startTimeout,
267
479
  };
268
480
 
269
481
  if (checkHealthImmediately) {
270
- await this.onCheckIfStarted(params, { throwOnError });
482
+ await this._onCheckIfStarted(params, { throwOnError });
271
483
  } else {
272
484
  this.startQueue.push({
273
485
  entity: 'blocklet',
@@ -279,10 +491,16 @@ class BlockletManager extends BaseBlockletManager {
279
491
 
280
492
  return blocklet;
281
493
  } catch (err) {
494
+ const status = await states.blocklet.getBlockletStatus(did);
495
+ if ([BlockletStatus.stopping, BlockletStatus.stopped].includes(status)) {
496
+ logger.info('Failed to start blocklet maybe due to manually stopped');
497
+ return states.blocklet.getBlocklet(did);
498
+ }
499
+
282
500
  const error = Array.isArray(err) ? err[0] : err;
283
501
  logger.error('Failed to start blocklet', { error, did, name: blocklet.meta.name });
284
502
  const description = `Start blocklet ${blocklet.meta.name} failed with error: ${error.message}`;
285
- this.notification.create({
503
+ this._createNotification(did, {
286
504
  title: 'Start Blocklet Failed',
287
505
  description,
288
506
  entityType: 'blocklet',
@@ -291,8 +509,8 @@ class BlockletManager extends BaseBlockletManager {
291
509
  });
292
510
 
293
511
  await this.deleteProcess({ did });
294
- const res = await this.state.setBlockletStatus(did, BlockletStatus.error);
295
- this.emit(BlockletEvents.startFailed, res);
512
+ const res = await states.blocklet.setBlockletStatus(did, BlockletStatus.error);
513
+ this.emit(BlockletEvents.startFailed, { ...res, error: { message: error.message } });
296
514
 
297
515
  if (throwOnError) {
298
516
  throw new Error(description);
@@ -302,60 +520,119 @@ class BlockletManager extends BaseBlockletManager {
302
520
  }
303
521
  }
304
522
 
305
- async stop({ did, updateStatus = true }, context) {
523
+ async stop({ did, updateStatus = true, silent = false }, context) {
306
524
  logger.info('stop blocklet', { did });
307
525
 
308
- const blocklet = await this.ensureBlocklet(did);
309
- const { appId } = blocklet.env;
526
+ const blocklet = await this.getBlocklet(did);
527
+ const { processId } = blocklet.env;
310
528
 
311
529
  if (updateStatus) {
312
- await this.state.setBlockletStatus(did, BlockletStatus.stopping);
530
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.stopping);
313
531
  blocklet.status = BlockletStatus.stopping;
314
532
  this.emit(BlockletEvents.statusChange, blocklet);
315
533
  }
316
534
 
317
- const nodeEnvironments = await this.node.getEnvironments();
535
+ const nodeEnvironments = await states.node.getEnvironments();
318
536
 
319
537
  await stopBlockletProcess(blocklet, {
320
- preStop: (b) =>
321
- hooks.preStop({
538
+ preStop: (b, { ancestors }) =>
539
+ hooks.preStop(b.env.processId, {
322
540
  appDir: b.env.appDir,
323
541
  hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
324
- env: getRuntimeEnvironments(b, nodeEnvironments),
542
+ env: getRuntimeEnvironments(b, nodeEnvironments, ancestors),
325
543
  did, // root blocklet did
326
- notification: this.notification,
544
+ notification: states.notification,
327
545
  context,
328
546
  exitOnError: false,
329
- progress: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT,
547
+ silent,
330
548
  }),
331
549
  });
332
550
 
333
- logger.info('blocklet stopped successfully', { appId, did });
551
+ logger.info('blocklet stopped successfully', { processId, did });
334
552
 
335
553
  if (updateStatus) {
336
554
  const res = await this.status(did, { forceSync: true });
555
+ // send notification to websocket channel
337
556
  this.emit(BlockletEvents.statusChange, res);
557
+
558
+ // send notification to wallet
559
+ this.emit(BlockletEvents.stopped, res);
560
+
338
561
  return res;
339
562
  }
340
563
 
341
564
  return blocklet;
342
565
  }
343
566
 
567
+ /**
568
+ * FIXME: @wangshijun create audit log for this
569
+ * @param {import('@abtnode/client').RequestBackupToSpacesInput} input
570
+ * @memberof BlockletManager
571
+ */
572
+ // eslint-disable-next-line no-unused-vars
573
+ async backupToSpaces({ appDid }, context) {
574
+ const blocklet = await states.blocklet.getBlocklet(appDid);
575
+ if (blocklet.structVersion !== APP_STRUCT_VERSION) {
576
+ throw new Error('Only new version app can be backup to spaces, please migrate this app first');
577
+ }
578
+
579
+ const userDid = context.user.did;
580
+ const { referrer } = context;
581
+
582
+ const spacesBackup = new SpacesBackup({ appDid, event: this, userDid, referrer });
583
+ this.emit(BlockletEvents.backupProgress, { appDid, message: 'Start backup...', progress: 10, completed: false });
584
+ await spacesBackup.backup();
585
+ this.emit(BlockletEvents.backupProgress, { appDid, completed: true, progress: 100 });
586
+ }
587
+
588
+ /**
589
+ * FIXME: @linchen support cancel
590
+ * FIXME: @wangshijun create audit log for this
591
+ * @param {import('@abtnode/client').RequestRestoreFromSpacesInput} input
592
+ * @memberof BlockletManager
593
+ */
594
+ // eslint-disable-next-line no-unused-vars
595
+ async restoreFromSpaces(input, context) {
596
+ this.emit(BlockletEvents.restoreProgress, { appDid: input.appDid, message: 'Start restore...', completed: false });
597
+
598
+ const userDid = context.user.did;
599
+
600
+ const spacesRestore = new SpacesRestore({ ...input, event: this, userDid, referrer: context.referrer });
601
+ const params = await spacesRestore.restore();
602
+
603
+ this.emit(BlockletEvents.restoreProgress, { appDid: input.appDid, message: 'Installing blocklet...' });
604
+ await installApplicationFromBackup({
605
+ url: `file://${spacesRestore.restoreDir}`,
606
+ moveDir: true,
607
+ ...merge(...params),
608
+ manager: this,
609
+ states,
610
+ });
611
+
612
+ this.emit(BlockletEvents.restoreProgress, { appDid: input.appDid, completed: true });
613
+ }
614
+
615
+ /**
616
+ *
617
+ * @param {import('@abtnode/client').RequestBlockletInput} param0
618
+ * @param {Record<string, any>} context
619
+ * @returns {import('@abtnode/client').BlockletState}
620
+ */
344
621
  async restart({ did }, context) {
345
622
  logger.info('restart blocklet', { did });
346
623
 
347
- await this.state.setBlockletStatus(did, BlockletStatus.stopping);
348
- const result = await this.state.getBlocklet(did);
624
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.stopping);
625
+ const result = await states.blocklet.getBlocklet(did);
349
626
  this.emit(BlockletEvents.statusChange, result);
350
627
 
351
628
  const ticket = this.startQueue.push({ entity: 'blocklet', action: 'restart', id: did, did, context });
352
629
  ticket.on('failed', async (err) => {
353
630
  logger.error('failed to restart blocklet', { did, error: err });
354
631
 
355
- const state = await this.state.setBlockletStatus(did, BlockletStatus.stopped);
632
+ const state = await states.blocklet.setBlockletStatus(did, BlockletStatus.stopped);
356
633
  this.emit(BlockletEvents.statusChange, state);
357
634
 
358
- this.notification.create({
635
+ this._createNotification(did, {
359
636
  title: 'Blocklet Restart Failed',
360
637
  description: `Blocklet ${did} restart failed with error: ${err.message || 'queue exception'}`,
361
638
  entityType: 'blocklet',
@@ -369,109 +646,293 @@ class BlockletManager extends BaseBlockletManager {
369
646
 
370
647
  // eslint-disable-next-line no-unused-vars
371
648
  async reload({ did }, context) {
372
- const blocklet = await this.ensureBlocklet(did);
649
+ const blocklet = await this.getBlocklet(did);
373
650
 
374
- await this.state.setBlockletStatus(did, BlockletStatus.stopping);
651
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.stopping);
375
652
  await reloadBlockletProcess(blocklet);
376
- await this.state.setBlockletStatus(did, BlockletStatus.running);
653
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.running);
377
654
  logger.info('blocklet reload successfully', { did });
378
655
 
379
656
  const res = await this.status(did);
380
- this.emit(BlockletEvents.statusUpdated, res);
657
+ this.emit(BlockletEvents.statusChange, res);
381
658
  return res;
382
659
  }
383
660
 
384
661
  async delete({ did, keepData, keepLogsDir, keepConfigs }, context) {
385
662
  logger.info('delete blocklet', { did, keepData });
386
663
 
387
- try {
388
- const blocklet = await this.ensureBlocklet(did);
664
+ const blocklet = await this.getBlocklet(did);
389
665
 
390
- const nodeEnvironments = await this.node.getEnvironments();
666
+ try {
667
+ if (isDeletableBlocklet(blocklet) === false) {
668
+ throw new Error('Blocklet is protected from accidental deletion');
669
+ }
391
670
 
671
+ const nodeEnvironments = await states.node.getEnvironments();
392
672
  await deleteBlockletProcess(blocklet, {
393
- preDelete: (b) =>
394
- hooks.preUninstall({
673
+ preDelete: (b, { ancestors }) =>
674
+ hooks.preUninstall(b.env.processId, {
395
675
  appDir: b.env.appDir,
396
676
  hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
397
- env: getRuntimeEnvironments(b, nodeEnvironments),
677
+ env: getRuntimeEnvironments(b, nodeEnvironments, ancestors),
398
678
  did, // root blocklet did
399
- notification: this.notification,
679
+ notification: states.notification,
400
680
  context,
401
681
  exitOnError: false,
402
682
  }),
403
683
  });
404
684
 
405
- return this._deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context);
406
- } catch (err) {
685
+ const doc = await this._deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context);
686
+ this._createNotification(doc.meta.did, {
687
+ title: 'Blocklet Deleted',
688
+ description: `Blocklet ${doc.meta.name}@${doc.meta.version} is deleted.`,
689
+ entityType: 'blocklet',
690
+ entityId: doc.meta.did,
691
+ severity: 'success',
692
+ });
693
+ return doc;
694
+ } catch (error) {
407
695
  // If we installed a corrupted blocklet accidentally, just cleanup the disk and state db
408
- if (err.code === 'BLOCKLET_CORRUPTED') {
409
- logger.info('blocklet is corrupted, will delete again', { did });
410
- return this._deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context);
696
+ logger.error('blocklet delete failed, will delete again', { did, error });
697
+ const doc = await this._deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context);
698
+
699
+ this._createNotification(doc.meta.did, {
700
+ title: 'Blocklet Deleted',
701
+ description: `Blocklet ${doc.meta.name}@${doc.meta.version} is deleted.`,
702
+ entityType: 'blocklet',
703
+ entityId: doc.meta.did,
704
+ severity: 'success',
705
+ });
706
+
707
+ return doc;
708
+ }
709
+ }
710
+
711
+ async reset({ did, childDid }, context = {}) {
712
+ logger.info('reset blocklet', { did, childDid });
713
+
714
+ const blocklet = await this.getBlocklet(did);
715
+
716
+ if (isInProgress(blocklet.status || blocklet.status === BlockletStatus.running)) {
717
+ throw new Error('Cannot reset when blocklet is in progress');
718
+ }
719
+
720
+ try {
721
+ await this.deleteProcess({ did }, context);
722
+ } catch {
723
+ // do nothing
724
+ }
725
+
726
+ if (!childDid) {
727
+ // Cleanup disk storage
728
+ const { cacheDir, logsDir, dataDir } = blocklet.env;
729
+ fs.removeSync(cacheDir);
730
+ fs.removeSync(dataDir);
731
+ fs.removeSync(logsDir);
732
+
733
+ // Reset config in db
734
+ await states.blockletExtras.remove({ did: blocklet.meta.did });
735
+ await this._setConfigsFromMeta(did);
736
+ await this._updateBlockletEnvironment(did);
737
+ await this.resetSiteByDid(did, context);
738
+ } else {
739
+ const child = blocklet.children.find((x) => x.meta.did === childDid);
740
+
741
+ if (!child) {
742
+ throw new Error('Child does not exist');
411
743
  }
412
744
 
413
- throw err;
745
+ // Cleanup disk storage
746
+ const { cacheDir, logsDir, dataDir } = child.env;
747
+ fs.removeSync(cacheDir);
748
+ fs.removeSync(dataDir);
749
+ fs.removeSync(logsDir);
750
+
751
+ // Reset config in db
752
+ await states.blockletExtras.delConfigs([blocklet.meta.did, child.meta.did]);
753
+ await this._setConfigsFromMeta(blocklet.meta.did, child.meta.did);
754
+ await this._updateBlockletEnvironment(did);
755
+ }
756
+
757
+ logger.info('blocklet reset', { did, childDid });
758
+ return blocklet;
759
+ }
760
+
761
+ async deleteComponent({ did, rootDid, keepData, keepState }, context) {
762
+ logger.info('delete blocklet component', { did, rootDid, keepData });
763
+
764
+ const blocklet = await this.getBlocklet(rootDid);
765
+
766
+ const child = blocklet.children.find((x) => x.meta.did === did);
767
+ if (!child) {
768
+ throw new Error('Component does not exist');
769
+ }
770
+
771
+ // delete state
772
+ const doc = await states.blocklet.getBlocklet(rootDid);
773
+ doc.children = doc.children.filter((x) => x.meta.did !== did);
774
+ const deletedChildren = await states.blockletExtras.getSettings(did, 'children', []);
775
+ if (keepData !== false && keepState !== false) {
776
+ deletedChildren.push({
777
+ meta: pick(child.meta, ['did', 'name', 'bundleDid', 'bundleName', 'version', 'title', 'description']),
778
+ mountPoint: child.mountPoint,
779
+ status: BlockletStatus.deleted,
780
+ deletedAt: new Date(),
781
+ });
782
+ }
783
+
784
+ await states.blocklet.updateBlocklet(rootDid, doc);
785
+ states.blockletExtras.setSettings(doc.meta.did, { children: deletedChildren });
786
+
787
+ // delete process
788
+ try {
789
+ const skippedProcessIds = [];
790
+ forEachBlockletSync(blocklet, (b) => {
791
+ if (!b.env.id.startsWith(child.env.id)) {
792
+ skippedProcessIds.push(b.env.processId);
793
+ }
794
+ });
795
+ await deleteBlockletProcess(blocklet, { skippedProcessIds });
796
+ logger.info('delete blocklet process for deleting component', { did, rootDid });
797
+ } catch (err) {
798
+ logger.error('delete blocklet process for deleting component', { did, rootDid, error: err });
799
+ }
800
+
801
+ // delete storage
802
+ const childBlocklet = blocklet.children.find((x) => x.meta.did === did);
803
+ const { cacheDir, logsDir, dataDir } = childBlocklet.env;
804
+ fs.removeSync(cacheDir);
805
+ fs.removeSync(logsDir);
806
+ if (keepData === false) {
807
+ fs.removeSync(dataDir);
808
+ await states.blockletExtras.delConfigs([blocklet.meta.did, child.meta.did]);
414
809
  }
810
+
811
+ const newBlocklet = await this.getBlocklet(rootDid);
812
+
813
+ await this._updateDependents(rootDid);
814
+
815
+ this.emit(BlockletEvents.upgraded, { blocklet: newBlocklet, context: { ...context, createAuditLog: false } }); // trigger router refresh
816
+
817
+ this._createNotification(newBlocklet.meta.did, {
818
+ title: 'Component Deleted',
819
+ description: `Component ${child.meta.name} of ${newBlocklet.meta.name} is successfully deleted.`,
820
+ entityType: 'blocklet',
821
+ entityId: newBlocklet.meta.did,
822
+ severity: 'success',
823
+ action: `/blocklets/${newBlocklet.meta.did}/components`,
824
+ });
825
+
826
+ return newBlocklet;
415
827
  }
416
828
 
417
- async cancelDownload({ did }, context) {
418
- await preDownloadLock.acquire();
829
+ async cancelDownload({ did: inputDid }) {
419
830
  try {
420
- const blocklet = await this.state.getBlocklet(did);
831
+ await statusLock.acquire();
832
+ const blocklet = await states.blocklet.getBlocklet(inputDid);
421
833
  if (!blocklet) {
422
- throw new Error('Can not cancel download for non-exist blocklet in database.', { did });
834
+ throw new Error(`Can not cancel download for non-exist blocklet in database. did: ${inputDid}`);
835
+ }
836
+
837
+ const { name, did, version } = blocklet.meta;
838
+
839
+ if (![BlockletStatus.downloading, BlockletStatus.waiting].includes(blocklet.status)) {
840
+ throw new Error(`Can not cancel blocklet that status is ${fromBlockletStatus(blocklet.status)}`);
423
841
  }
424
842
 
843
+ const job = await this.installQueue.get(did);
844
+
845
+ // cancel job
425
846
  if (blocklet.status === BlockletStatus.downloading) {
426
- await this._cancelDownload(blocklet.meta, context);
847
+ try {
848
+ await this.blockletDownloader.cancelDownload(blocklet.meta.did);
849
+ } catch (error) {
850
+ logger.error('failed to exec blockletDownloader.download', { did: blocklet.meta.did, error });
851
+ }
427
852
  } else if (blocklet.status === BlockletStatus.waiting) {
428
- await this._cancelWaiting(blocklet.meta, context);
853
+ try {
854
+ await this.installQueue.cancel(blocklet.meta.did);
855
+ } catch (error) {
856
+ logger.error('failed to cancel waiting', { did: blocklet.meta.did, error });
857
+ }
858
+ }
859
+
860
+ // rollback
861
+ if (job) {
862
+ const { postAction, oldBlocklet } = job;
863
+ await this._rollback(postAction, did, oldBlocklet);
429
864
  } else {
430
- throw new Error(`Can not cancel blocklet that status is ${fromBlockletStatus(blocklet.status)}`);
865
+ const data = await this._rollbackCache.restore({ did });
866
+ if (data) {
867
+ const { action, oldBlocklet } = data;
868
+ await this._rollback(action, did, oldBlocklet);
869
+ await this._rollbackCache.remove({ did });
870
+ } else {
871
+ throw new Error(`Cannot find rollback data in queue or backup file of blocklet ${inputDid}`);
872
+ }
431
873
  }
432
874
 
433
- preDownloadLock.release();
875
+ logger.info('cancel download blocklet', { did, name, version, status: fromBlockletStatus(blocklet.status) });
876
+
877
+ statusLock.release();
434
878
  return blocklet;
435
879
  } catch (error) {
436
- preDownloadLock.release();
880
+ try {
881
+ // fallback blocklet status to error
882
+ const blocklet = await states.blocklet.getBlocklet(inputDid);
883
+ if (blocklet) {
884
+ await states.blocklet.setBlockletStatus(blocklet.meta.did, BlockletStatus.error);
885
+ }
886
+ statusLock.release();
887
+ } catch (err) {
888
+ statusLock.release();
889
+ logger.error('Failed to fallback blocklet status to error on cancelDownload', { error });
890
+ }
891
+
437
892
  throw error;
438
893
  }
439
894
  }
440
895
 
441
896
  // eslint-disable-next-line no-unused-vars
442
897
  async deleteProcess({ did }, context) {
443
- const blocklet = await this.ensureBlocklet(did);
898
+ const blocklet = await this.getBlocklet(did);
444
899
 
445
900
  logger.info('delete blocklet process', { did });
446
901
 
447
- await deleteBlockletProcess(blocklet);
902
+ await deleteBlockletProcess(blocklet, context);
448
903
 
449
- const result = await this.state.setBlockletStatus(did, BlockletStatus.stopped);
904
+ const result = await states.blocklet.setBlockletStatus(did, BlockletStatus.stopped);
450
905
  logger.info('blocklet process deleted successfully', { did });
451
906
  return result;
452
907
  }
453
908
 
454
- async detail({ did, attachRuntimeInfo = true }, context) {
909
+ // Get blocklet by blockletDid or appDid
910
+ async detail({ did, attachConfig = true, attachRuntimeInfo = true }, context) {
455
911
  if (!did) {
456
912
  throw new Error('did should not be empty');
457
913
  }
458
914
 
459
- const nodeInfo = await this.node.read();
915
+ if (!attachConfig) {
916
+ return states.blocklet.getBlocklet(did);
917
+ }
460
918
 
461
919
  if (!attachRuntimeInfo) {
462
- return this.state.getBlocklet(did);
920
+ try {
921
+ const blocklet = await this.getBlocklet(did, { throwOnNotExist: false });
922
+ return blocklet;
923
+ } catch (e) {
924
+ logger.error('get blocklet detail error', { error: e });
925
+ return states.blocklet.getBlocklet(did);
926
+ }
463
927
  }
464
928
 
465
- return this.attachRuntimeInfo({ did, nodeInfo, diskInfo: true, context });
466
- }
929
+ const nodeInfo = await states.node.read();
467
930
 
468
- async list({ includeRuntimeInfo = true, useCache = true } = {}, context) {
469
- const blocklets = await this.state.getBlocklets();
470
- if (!includeRuntimeInfo) {
471
- return blocklets;
472
- }
931
+ return this._attachRuntimeInfo({ did, nodeInfo, diskInfo: true, context });
932
+ }
473
933
 
474
- const nodeInfo = await this.node.read();
934
+ async attachBlockletListRuntimeInfo({ blocklets, useCache }, context) {
935
+ const nodeInfo = await states.node.read();
475
936
  const updated = (
476
937
  await Promise.all(
477
938
  blocklets.map((x) => {
@@ -482,7 +943,7 @@ class BlockletManager extends BaseBlockletManager {
482
943
  const cachedBlocklet =
483
944
  useCache && this.cachedBlocklets ? this.cachedBlocklets.find((y) => y.meta.did === x.meta.did) : null;
484
945
 
485
- return this.attachRuntimeInfo({
946
+ return this._attachRuntimeInfo({
486
947
  did: x.meta.did,
487
948
  nodeInfo,
488
949
  diskInfo: false,
@@ -497,411 +958,296 @@ class BlockletManager extends BaseBlockletManager {
497
958
  return updated;
498
959
  }
499
960
 
961
+ async list({ includeRuntimeInfo = true, useCache = true, query, filter } = {}, context) {
962
+ const condition = { ...flat(query || {}) };
963
+ if (filter === 'external-only') {
964
+ condition.controller = {
965
+ $exists: true,
966
+ };
967
+ }
968
+
969
+ if (filter === 'external-excluded') {
970
+ condition.controller = {
971
+ $exists: false,
972
+ };
973
+ }
974
+
975
+ const blocklets = await states.blocklet.getBlocklets(condition);
976
+
977
+ if (includeRuntimeInfo) {
978
+ return this.attachBlockletListRuntimeInfo({ blocklets, useCache }, context);
979
+ }
980
+
981
+ return blocklets;
982
+ }
983
+
984
+ // CAUTION: this method currently only support config by blocklet.meta.did
500
985
  // eslint-disable-next-line no-unused-vars
501
- async config({ did, childDid, configs: newConfigs }, context) {
502
- logger.info('config blocklet', { did });
986
+ async config({ did, configs: newConfigs, skipHook, skipDidDocument }, context) {
987
+ // todo: skipDidDocument will be deleted
503
988
  if (!Array.isArray(newConfigs)) {
504
989
  throw new Error('configs list is not an array');
505
990
  }
506
991
 
507
- let blocklet = await this.ensureBlocklet(did);
508
- if (childDid) {
992
+ const dids = Array.isArray(did) ? did : [did];
993
+ const [rootDid, ...childDids] = dids;
994
+ logger.info('config blocklet', { dids });
995
+
996
+ let blocklet = await this.getBlocklet(rootDid);
997
+ for (const childDid of childDids) {
509
998
  blocklet = blocklet.children.find((x) => x.meta.did === childDid);
510
999
  if (!blocklet) {
511
- throw new Error('Child blocklet does not exist', { did, childDid });
1000
+ throw new Error('Child blocklet does not exist', { dids });
512
1001
  }
513
1002
  }
514
1003
 
515
1004
  // run hook
516
- const nodeEnvironments = await this.node.getEnvironments();
517
- newConfigs.forEach((x) => {
518
- if (x.key === 'BLOCKLET_APP_SK') {
519
- try {
520
- fromSecretKey(x.value);
521
- } catch {
522
- try {
523
- fromSecretKey(x.value, 'eth');
524
- } catch {
525
- throw new Error('Invalid custom blocklet secret key');
526
- }
1005
+ const nodeEnvironments = await states.node.getEnvironments();
1006
+ for (const x of newConfigs) {
1007
+ if (x.custom === true) {
1008
+ // custom key
1009
+ await environmentNameSchema.validateAsync(x.key);
1010
+ } else if (BLOCKLET_CONFIGURABLE_KEY[x.key] && x.key.startsWith('BLOCKLET_')) {
1011
+ // app key
1012
+ if (childDids.length) {
1013
+ logger.error(`Cannot set ${x.key} to child blocklet`, [dids]);
1014
+ throw new Error(`Cannot set ${x.key} to child blocklet`);
527
1015
  }
528
- }
529
1016
 
530
- if (x.key === 'BLOCKLET_WALLET_TYPE') {
531
- if (['default', 'eth'].includes(x.value) === false) {
532
- throw new Error('Invalid blocklet wallet type, only "default" and "eth" are supported');
1017
+ await validateAppConfig(x, states);
1018
+ } else if (!BLOCKLET_CONFIGURABLE_KEY[x.key] && !isPreferenceKey(x)) {
1019
+ if (!(blocklet.meta.environments || []).some((y) => y.name === x.key)) {
1020
+ // forbid unknown format key
1021
+ throw new Error(`unknown format key: ${x.key}`);
533
1022
  }
534
1023
  }
535
1024
 
536
1025
  blocklet.configObj[x.key] = x.value;
537
- });
538
- await hooks.preConfig({
539
- appDir: blocklet.env.appDir,
540
- hooks: Object.assign(blocklet.meta.hooks || {}, blocklet.meta.scripts || {}),
541
- exitOnError: true,
542
- env: { ...getRuntimeEnvironments(blocklet, nodeEnvironments), ...blocklet.configObj },
543
- notification: this.notification,
544
- did,
545
- context,
546
- progress: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT,
547
- });
1026
+ }
1027
+
1028
+ if (!skipHook) {
1029
+ // FIXME: we should also call preConfig for child blocklets
1030
+ await hooks.preConfig(blocklet.env.processId, {
1031
+ appDir: blocklet.env.appDir,
1032
+ hooks: Object.assign(blocklet.meta.hooks || {}, blocklet.meta.scripts || {}),
1033
+ exitOnError: true,
1034
+ env: { ...getRuntimeEnvironments(blocklet, nodeEnvironments), ...blocklet.configObj },
1035
+ did,
1036
+ context,
1037
+ });
1038
+ }
1039
+
1040
+ const willAppSkChange = isRotatingAppSk(newConfigs, blocklet.configs, blocklet.externalSk);
1041
+ const willAppDidChange = isRotatingAppDid(newConfigs, blocklet.configs, blocklet.externalSk);
548
1042
 
549
1043
  // update db
550
- const configs = childDid
551
- ? await this.extras.setChildConfigs(did, childDid, newConfigs)
552
- : await this.extras.setConfigs(did, newConfigs);
1044
+ await states.blockletExtras.setConfigs(dids, newConfigs);
1045
+
1046
+ if (willAppSkChange) {
1047
+ const info = await states.node.read();
1048
+ const { wallet } = getBlockletInfo(blocklet, info.sk);
1049
+ const migratedFrom = Array.isArray(blocklet.migratedFrom) ? blocklet.migratedFrom : [];
1050
+ await states.blocklet.updateBlocklet(rootDid, {
1051
+ migratedFrom: [
1052
+ ...migratedFrom,
1053
+ { appSk: wallet.secretKey, appDid: wallet.address, at: new Date().toISOString() },
1054
+ ],
1055
+ });
1056
+ }
553
1057
 
554
- const newState = await this.updateBlockletEnvironment(did);
1058
+ // Reload nginx to make sure did-space can embed content from this app
1059
+ if (newConfigs.find((x) => x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACE_ENDPOINT)?.value) {
1060
+ this.emit(BlockletEvents.spaceConnected, blocklet);
1061
+ }
555
1062
 
556
- // response
557
- if (!childDid) {
558
- newState.configs = configs;
559
- } else {
560
- const c = newState.children.find((x) => x.meta.did === childDid);
561
- c.configs = configs;
1063
+ if (willAppDidChange && !skipDidDocument) {
1064
+ await this._updateDidDocument(blocklet);
562
1065
  }
563
1066
 
1067
+ await this._updateBlockletEnvironment(rootDid);
1068
+
1069
+ // response
1070
+ const newState = await this.getBlocklet(rootDid);
564
1071
  this.emit(BlockletEvents.updated, newState);
565
1072
  return newState;
566
1073
  }
567
1074
 
568
- /**
569
- * upgrade blocklet from registry
570
- */
571
- async upgrade({ did, registryUrl }, context) {
572
- const blocklet = await this.state.getBlocklet(did);
1075
+ async configPublicToStore({ did, publicToStore = false }) {
1076
+ const blocklet = await this.getBlocklet(did);
1077
+ // publicToStore 由用户传入
1078
+ // handleInstanceInStore 方法写在前面,保证向 store 操作成功后才会更改 blocklet 中的 publicToStore值
1079
+ // handleInstanceInStore 中会校验修改 publicToStore字段 的条件,不符合则会抛错,就不会执行下面更新 publicToStore 的逻辑
1080
+ await handleInstanceInStore(blocklet, { publicToStore });
1081
+ await states.blockletExtras.setSettings(did, { publicToStore });
1082
+
1083
+ const newState = await this.getBlocklet(did);
1084
+ return newState;
1085
+ }
573
1086
 
574
- // TODO: 查看了下目前页面中的升级按钮,都是会传 registryUrl 过来的,这个函数里的逻辑感觉需要在以后做一个简化
575
- if (!registryUrl && blocklet.source !== BlockletSource.registry) {
576
- throw new Error('Wrong upgrade source, empty registryUrl or not installed from blocklet registry');
1087
+ async configNavigations({ did, navigations = [] }) {
1088
+ if (!Array.isArray(navigations)) {
1089
+ throw new Error('navigations is not an array');
577
1090
  }
1091
+ await states.blockletExtras.setSettings(did, { navigations });
578
1092
 
579
- const upgradeFromRegistry = registryUrl || blocklet.deployedFrom;
1093
+ const newState = await this.getBlocklet(did);
1094
+ this.emit(BlockletEvents.updated, newState);
1095
+ return newState;
1096
+ }
580
1097
 
581
- const newVersionBlocklet = await this.registry.getBlockletMeta(
582
- {
583
- did,
584
- registryUrl: upgradeFromRegistry,
585
- },
586
- context
587
- );
1098
+ async updateWhoCanAccess({ did, whoCanAccess }) {
1099
+ const dids = Array.isArray(did) ? did : [did];
588
1100
 
589
- function getSignature(signatures = [], oldSignatures = []) {
590
- // if blocklet installed from local, upload, url, the signature is undefined, should return null
591
- if (!Array.isArray(signatures) || signatures.length === 0) {
592
- return null;
593
- }
594
- if (signatures.length > 3) {
595
- throw new Error('Invalid blocklet signature length');
596
- }
597
- // if old signature is old registry version, return new signatures last one signature
598
- if (oldSignatures.length > 0 && oldSignatures.length < 3) {
599
- return signatures[signatures.length - 1];
1101
+ const [rootDid] = dids;
1102
+
1103
+ const isApp = dids.length === 1;
1104
+
1105
+ try {
1106
+ // check exist
1107
+ if (!(await this.hasBlocklet({ did: rootDid }))) {
1108
+ throw new Error('The blocklet does not exist');
600
1109
  }
601
- // old registry signatures: [ registry 签名, developer-sk 签名]
602
- // new registry signatures [ registry 签名, user wallet 签名, access-token 签名 ]
603
- // old -> old: 需要对比 developer-sk 签名
604
- // old -> new: 需要对比 developer-sk 和 access-token 签名
605
- // new -> new: 需要对比 user-wallet 签名
606
- return signatures.length === 1 ? signatures[0] : signatures[1];
607
- }
608
1110
 
609
- const currentDeveloperSignature = getSignature(blocklet.meta.signatures);
610
- const newVersionDeveloperSignature = getSignature(newVersionBlocklet.signatures, blocklet.meta.signatures);
1111
+ // validate input
1112
+ if (
1113
+ !whoCanAccess.startsWith(WHO_CAN_ACCESS_PREFIX_ROLES) &&
1114
+ !Object.values(WHO_CAN_ACCESS).includes(whoCanAccess)
1115
+ ) {
1116
+ throw new Error(`The value of whoCanAccess is invalid: ${whoCanAccess}`);
1117
+ } else if (whoCanAccess.startsWith(WHO_CAN_ACCESS_PREFIX_ROLES)) {
1118
+ if (!whoCanAccess.substring(WHO_CAN_ACCESS_PREFIX_ROLES.length).trim()) {
1119
+ throw new Error('Roles in whoCanAccess cannot be empty');
1120
+ }
611
1121
 
612
- if (!newVersionDeveloperSignature) {
613
- throw new Error('Invalid upgrade blocklet signature');
614
- }
1122
+ if (whoCanAccess.length > 200) {
1123
+ throw new Error('The length of whoCanAccess should not exceed 200');
1124
+ }
615
1125
 
616
- if (
617
- currentDeveloperSignature &&
618
- blocklet.source === BlockletSource.registry &&
619
- (currentDeveloperSignature.signer !== newVersionDeveloperSignature.signer ||
620
- currentDeveloperSignature.pk !== newVersionDeveloperSignature.pk)
621
- ) {
622
- logger.error('invalid developer signature', { did, currentDeveloperSignature, newVersionDeveloperSignature });
623
- throw new Error('Invalid developer signature');
1126
+ const roleNames = (await this.teamManager.getRoles(rootDid)).map((x) => x.name);
1127
+ const accessRoleNames = getRolesFromAuthConfig({ whoCanAccess });
1128
+ const noExistNames = accessRoleNames.filter((x) => !roleNames.includes(x));
1129
+ if (noExistNames.length) {
1130
+ throw new Error(`Found no exist role names: ${noExistNames.join(',')}`);
1131
+ }
1132
+ }
1133
+ } catch (error) {
1134
+ logger.error(error.message);
1135
+ throw error;
624
1136
  }
625
1137
 
626
- if (blocklet.meta.version === newVersionBlocklet.version) {
627
- throw new Error('Upgrade/downgrade blocklet to same version is noop');
1138
+ if (isApp) {
1139
+ await states.blockletExtras.setSettings(rootDid, { whoCanAccess });
1140
+ } else {
1141
+ const configs = [{ key: BLOCKLET_CONFIGURABLE_KEY.COMPONENT_ACCESS_WHO, value: whoCanAccess }];
1142
+ await states.blockletExtras.setConfigs(dids, configs);
628
1143
  }
629
1144
 
630
- const action = semver.gt(blocklet.meta.version, newVersionBlocklet.version) ? 'downgrade' : 'upgrade';
631
- logger.info(`${action} blocklet`, { did });
1145
+ const blocklet = await this.getBlocklet(rootDid);
632
1146
 
633
- return this._upgrade({
634
- meta: newVersionBlocklet,
635
- source: BlockletSource.registry,
636
- deployedFrom: upgradeFromRegistry,
637
- context,
638
- });
1147
+ this.emit(BlockletEvents.updated, { meta: { did: blocklet.meta.did } });
1148
+
1149
+ return blocklet;
639
1150
  }
640
1151
 
641
- // eslint-disable-next-line no-unused-vars
642
- async diff({ did, hashFiles: clientFiles }, context) {
643
- if (!did) {
644
- throw new Error('did is empty');
645
- }
646
- if (!clientFiles || !clientFiles.length) {
647
- throw new Error('hashFiles is empty');
648
- }
1152
+ async updateComponentTitle({ did, rootDid: inputRootDid, title }) {
1153
+ await titleSchema.validateAsync(title);
649
1154
 
650
- logger.info('Get blocklet diff', { did, clientFilesNumber: clientFiles.length });
1155
+ const blocklet = await states.blocklet.getBlocklet(inputRootDid);
651
1156
 
652
- const state = await this.state.getBlocklet(did);
653
- if (!state) {
654
- return {
655
- hasBlocklet: false,
656
- };
657
- }
658
- if (state.source === BlockletSource.local) {
659
- throw new Error(`Blocklet ${state.meta.name} is already deployed from local, can not deployed from remote.`);
1157
+ if (!blocklet) {
1158
+ throw new Error('blocklet does not exist');
660
1159
  }
661
- const { name, version } = state.meta;
662
- const installDir = path.join(this.installDir, name, version);
663
- // eslint-disable-next-line no-param-reassign
664
- clientFiles = clientFiles.reduce((obj, item) => {
665
- obj[item.file] = item.hash;
666
- return obj;
667
- }, {});
668
1160
 
669
- const { files } = await hashFiles(installDir, {
670
- filter: (x) => x.indexOf('node_modules') === -1,
671
- concurrentHash: 1,
672
- });
673
- logger.info('Get files hash', { filesNum: Object.keys(files).length });
674
-
675
- const addSet = [];
676
- const changeSet = [];
677
- const deleteSet = [];
678
- const diffFiles = diff(files, clientFiles);
679
- if (diffFiles) {
680
- diffFiles.forEach((item) => {
681
- if (item.kind === 'D') {
682
- deleteSet.push(item.path[0]);
683
- }
684
- if (item.kind === 'E') {
685
- changeSet.push(item.path[0]);
686
- }
687
- if (item.kind === 'N') {
688
- addSet.push(item.path[0]);
689
- }
690
- });
691
- }
692
- logger.info('Diff files', {
693
- name: state.meta.name,
694
- did: state.meta.did,
695
- version: state.meta.version,
696
- addNum: addSet.length,
697
- changeNum: changeSet.length,
698
- deleteNum: deleteSet.length,
699
- });
700
- return {
701
- hasBlocklet: true,
702
- version,
703
- addSet,
704
- changeSet,
705
- deleteSet,
706
- };
707
- }
1161
+ const rootDid = blocklet.meta.did;
708
1162
 
709
- async checkChildrenForUpdates({ did }) {
710
- const blocklet = await this.state.getBlocklet(did);
711
- const childrenMeta = await getChildrenMeta(blocklet.meta);
712
- const updateList = getUpdateMetaList(
713
- blocklet.children.map((x) => x.meta),
714
- childrenMeta
715
- );
1163
+ const { children } = blocklet;
1164
+ const component = children.find((x) => x.meta.did === did);
716
1165
 
717
- if (!updateList.length) {
718
- return {};
1166
+ if (!component) {
1167
+ throw new Error('component does not exist');
719
1168
  }
720
1169
 
721
- // start session
722
- const { id: updateId } = await this.session.start({
723
- did,
724
- childrenMeta,
725
- });
726
-
727
- return {
728
- updateId,
729
- updateList,
730
- };
731
- }
732
-
733
- async updateChildren({ updateId }, context) {
734
- const { did, childrenMeta } = await this.session.end(updateId);
1170
+ component.meta.title = title;
1171
+ await states.blocklet.updateBlocklet(rootDid, { children });
735
1172
 
736
- // get old blocklet
737
- const oldBlocklet = await this.state.getBlocklet(did);
738
- const { meta } = oldBlocklet;
739
- const { name, version } = meta;
1173
+ // trigger meta.js refresh
1174
+ // trigger dashboard frontend refresh
1175
+ this.emit(BlockletEvents.updated, blocklet);
740
1176
 
741
- const action = 'upgrade';
1177
+ return this.getBlocklet(rootDid);
1178
+ }
742
1179
 
743
- logger.info(`${action} blocklet children`, {
744
- did,
745
- name,
746
- version,
747
- children: childrenMeta.map((x) => ({ name: x.name, version: x.version })),
748
- });
1180
+ async updateComponentMountPoint({ did, rootDid: inputRootDid, mountPoint: tmpMountPoint }, context) {
1181
+ const mountPoint = await updateMountPointSchema.validateAsync(tmpMountPoint);
749
1182
 
750
- // new blocklet
751
- const newBlocklet = await this.state.setBlockletStatus(did, BlockletStatus.waiting);
752
- mergeMeta(meta, childrenMeta);
753
- newBlocklet.meta = meta;
754
- newBlocklet.children = await this.state.getChildrenFromMetas(childrenMeta);
755
- await validateBlocklet(newBlocklet);
1183
+ const blocklet = await states.blocklet.getBlocklet(inputRootDid);
756
1184
 
757
- this.emit(BlockletEvents.statusChange, newBlocklet);
1185
+ if (!blocklet) {
1186
+ throw new Error('blocklet does not exist');
1187
+ }
758
1188
 
759
- // add to queue
760
- const ticket = this.installQueue.push(
761
- {
762
- entity: 'blocklet',
763
- action: 'download',
764
- id: did,
765
- oldBlocklet: { ...oldBlocklet },
766
- blocklet: { ...newBlocklet },
767
- version,
768
- context,
769
- postAction: action,
770
- },
771
- did
772
- );
1189
+ const rootDid = blocklet.meta.did;
773
1190
 
774
- ticket.on('failed', async (err) => {
775
- logger.error('queue failed', { entity: 'blocklet', action, did, version, name, error: err });
776
- await this._rollback(action, did, oldBlocklet);
777
- this.emit(`blocklet.${action}.failed`, { did, version, err });
778
- this.notification.create({
779
- title: `Blocklet ${capitalize(action)} Failed`,
780
- description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message || 'queue exception'}`,
781
- entityType: 'blocklet',
782
- entityId: did,
783
- severity: 'error',
784
- });
785
- });
786
- return newBlocklet;
787
- }
1191
+ const isRootComponent = !did;
788
1192
 
789
- // ============================================================================================
790
- // Internal API that are used by public APIs and called from CLI
791
- // ============================================================================================
792
- async dev(folder) {
793
- logger.info('dev blocklet', { folder });
1193
+ const component = isRootComponent ? blocklet : blocklet.children.find((x) => x.meta.did === did);
1194
+ if (!component) {
1195
+ throw new Error('component does not exist');
1196
+ }
794
1197
 
795
- const meta = getBlockletMeta(folder);
796
- if (meta.group !== 'static' && (!meta.scripts || !meta.scripts.dev)) {
797
- throw new Error('Incorrect blocklet manifest: missing `scripts.dev` field');
1198
+ if (isRootComponent && component.group === BlockletGroup.gateway) {
1199
+ throw new Error('cannot update mountPoint of gateway blocklet');
798
1200
  }
799
1201
 
800
- const { did, version } = meta;
801
- meta.title = `[DEV] ${meta.title || meta.name}`;
1202
+ checkDuplicateMountPoint(blocklet, mountPoint);
802
1203
 
803
- const exist = await this.state.getBlocklet(did);
804
- if (exist) {
805
- if (exist.mode === BLOCKLET_MODES.PRODUCTION) {
806
- throw new Error('The blocklet of production mode already exists, please remove it before developing');
807
- }
1204
+ component.mountPoint = mountPoint;
808
1205
 
809
- const status = fromBlockletStatus(exist.status);
810
- if (['starting', 'running'].includes(status)) {
811
- throw new Error(`The blocklet is already on ${status}, please stop it before developing`);
812
- }
1206
+ await states.blocklet.updateBlocklet(rootDid, { mountPoint: blocklet.mountPoint, children: blocklet.children });
813
1207
 
814
- logger.info('remove blocklet for dev', { did, version });
1208
+ this.emit(BlockletEvents.upgraded, { blocklet, context: { ...context, createAuditLog: false } }); // trigger router refresh
815
1209
 
816
- await this.delete({ did, keepLogsDir: false });
817
- }
1210
+ return this.getBlocklet(rootDid);
1211
+ }
818
1212
 
819
- // delete process
820
- try {
821
- await this.deleteProcess({ did });
822
- logger.info('delete blocklet precess for dev', { did, version });
823
- } catch (err) {
824
- logger.error('failed to delete blocklet process for dev', { error: err });
825
- }
1213
+ // eslint-disable-next-line no-unused-vars
1214
+ async getRuntimeHistory({ did, hours }, context) {
1215
+ const metaDid = await states.blocklet.getBlockletMetaDid(did);
826
1216
 
827
- const childrenMeta = await getChildrenMeta(meta);
828
- mergeMeta(meta, childrenMeta);
829
- const blocklet = await this.state.addBlocklet({
830
- did,
831
- meta,
832
- source: BlockletSource.local,
833
- deployedFrom: folder,
834
- mode: BLOCKLET_MODES.DEVELOPMENT,
835
- childrenMeta,
836
- });
837
- logger.info('add blocklet for dev', { did, version, meta });
1217
+ const history = this.runtimeMonitor.getHistory(metaDid);
838
1218
 
839
- await this._downloadBlocklet(blocklet);
840
- await this.state.setBlockletStatus(did, BlockletStatus.installed);
1219
+ return getHistoryList({
1220
+ history,
1221
+ hours,
1222
+ recordIntervalSec: MONITOR_RECORD_INTERVAL_SEC,
1223
+ props: ['date', 'cpu', 'mem'],
1224
+ });
1225
+ }
841
1226
 
842
- // Add environments
843
- await this._setConfigs(did);
844
- await this.updateBlockletEnvironment(did);
1227
+ async ensureBlocklet(did, opts = {}) {
1228
+ return getBlocklet({ ...opts, states, dataDirs: this.dataDirs, did, ensureIntegrity: true });
1229
+ }
845
1230
 
846
- this.emit(BlockletEvents.deployed, { blocklet, context: {} });
847
- return this.ensureBlocklet(did);
1231
+ async getBlocklet(did, opts = {}) {
1232
+ return getBlocklet({ ...opts, states, dataDirs: this.dataDirs, did, ensureIntegrity: false });
848
1233
  }
849
1234
 
850
- async ensureBlocklet(did) {
851
- if (!isValidDid(did)) {
852
- throw new Error(`Blocklet did is invalid: ${did}`);
853
- }
1235
+ async hasBlocklet({ did }) {
1236
+ return states.blocklet.hasBlocklet(did);
1237
+ }
854
1238
 
855
- const blocklet = await this.state.getBlocklet(did);
856
- if (!blocklet) {
857
- throw new Error(`Can not find blocklet in database by did ${did}`);
1239
+ async setInitialized({ did, owner }) {
1240
+ if (!validateOwner(owner)) {
1241
+ throw new Error('Blocklet owner is invalid');
858
1242
  }
859
1243
 
860
- blocklet.env = {
861
- appId: blocklet.meta.name,
862
- // dataDir is /dataDir.data/blocklet.meta.name
863
- // cacheDir is /dataDirs.cache/blocklet.meta.name
864
- // logDir is /dataDirs.log/blocklet.meta.name
865
- ...getBlockletDirs(blocklet, {
866
- dataDirs: this.dataDirs,
867
- ensure: true,
868
- }),
869
- };
870
-
871
- const configs = await this.extras.getConfigs(did);
872
- fillBlockletConfigs(blocklet, configs);
873
-
874
- // merge settings to blocklet
875
- const settings = await this.extras.getSettings(did);
876
- blocklet.trustedPassports = get(settings, 'trustedPassports') || [];
877
- blocklet.enablePassportIssuance = get(settings, 'enablePassportIssuance', true);
878
-
879
- // handle child env
880
- for (const child of blocklet.children) {
881
- const {
882
- meta: { did: childDid, name: childName },
883
- } = child;
884
- const {
885
- meta: { name },
886
- } = blocklet;
887
-
888
- child.env = {
889
- appId: `${encodeURIComponent(name)}/${encodeURIComponent(childName)}`,
890
- // dataDir is /dataDir.data/blocklet.meta.name/child.meta.name
891
- // cacheDir is /dataDirs.cache/blocklet.meta.name/child.meta.name
892
- // logDir is /dataDirs.log/blocklet.meta.name
893
- ...getBlockletDirs(child, {
894
- dataDirs: this.dataDirs,
895
- ensure: true,
896
- rootBlocklet: blocklet,
897
- }),
898
- };
1244
+ const blocklet = await states.blocklet.getBlocklet(did);
1245
+ await states.blockletExtras.setSettings(blocklet.meta.did, { initialized: true, owner });
1246
+ logger.info('Blocklet initialized', { did, owner });
899
1247
 
900
- const childConfigs = await this.extras.getChildConfigs(did, childDid);
901
- fillBlockletConfigs(child, childConfigs);
902
- }
1248
+ this.emit(BlockletEvents.updated, { meta: { did: blocklet.meta.did } });
903
1249
 
904
- return blocklet;
1250
+ return this.getBlocklet(did);
905
1251
  }
906
1252
 
907
1253
  async status(did, { forceSync = false } = {}) {
@@ -913,10 +1259,12 @@ class BlockletManager extends BaseBlockletManager {
913
1259
  return blocklet;
914
1260
  }
915
1261
 
916
- return this.state.setBlockletStatus(did, BlockletStatus.stopped);
1262
+ const res = await states.blocklet.setBlockletStatus(did, BlockletStatus.stopped);
1263
+ this.emit(BlockletEvents.statusChange, res);
1264
+ return res;
917
1265
  };
918
1266
 
919
- const blocklet = await this.ensureBlocklet(did);
1267
+ const blocklet = await this.getBlocklet(did);
920
1268
 
921
1269
  let shouldUpdateStatus = forceSync || shouldUpdateBlockletStatus(blocklet.status);
922
1270
  if (isInProgress(blocklet.status)) {
@@ -927,104 +1275,21 @@ class BlockletManager extends BaseBlockletManager {
927
1275
  }
928
1276
  }
929
1277
 
930
- try {
931
- const status = await getBlockletStatusFromProcess(blocklet);
932
- if (shouldUpdateStatus) {
933
- if (status === BlockletStatus.stopped) {
934
- await this.state.stopBlocklet(did);
935
- }
936
- return this.state.setBlockletStatus(did, status);
937
- }
938
- } catch (err) {
939
- if (shouldUpdateStatus) {
940
- return fastReturnOnForceSync(blocklet);
941
- }
942
- throw err;
943
- }
944
-
945
- return blocklet;
946
- }
947
-
948
- async getBlockletInterfaces({ blocklet, nodeInfo, context }) {
949
- const routingRules = (await this.getRoutingRulesByDid(blocklet.meta.did)).sort((a, b) => {
950
- // Put user-defined rules first
951
- if (a.isProtected !== b.isProtected) {
952
- return a.isProtected ? 1 : -1;
953
- }
954
- // Put shorter url first
955
- return a.from.pathPrefix.length < b.from.pathPrefix ? 1 : -1;
956
- });
957
- const nodeIp = await getAccessibleExternalNodeIp(nodeInfo);
958
- return getBlockletInterfaces({ blocklet, context, nodeInfo, routingRules, nodeIp });
959
- }
960
-
961
- async attachRuntimeInfo({ did, nodeInfo, diskInfo = true, context, cachedBlocklet }) {
962
- if (!did) {
963
- throw new Error('did should not be empty');
1278
+ if (!shouldUpdateStatus) {
1279
+ return blocklet;
964
1280
  }
965
1281
 
966
1282
  try {
967
- let blocklet = await this.ensureBlocklet(did);
968
- const fromCache = !!cachedBlocklet;
969
-
970
- // if from cached data, only use cache data of runtime info (engine, diskInfo, runtimeInfo...)
971
- if (fromCache) {
972
- blocklet = { ...cachedBlocklet, ...blocklet };
973
- }
974
-
975
- blocklet.interfaces = await this.getBlockletInterfaces({ blocklet, nodeInfo, context });
976
-
977
- if (!fromCache) {
978
- blocklet.engine = getEngine(getBlockletEngineNameByPlatform(blocklet.meta)).describe();
979
- blocklet.diskInfo = await getDiskInfo(blocklet, { useFakeDiskInfo: !diskInfo });
980
-
981
- try {
982
- const { appId } = blocklet.meta.group === BlockletGroup.gateway ? blocklet.children[0].env : blocklet.env;
983
- blocklet.runtimeInfo = await getRuntimeInfo(appId);
984
- if (blocklet.runtimeInfo.status && shouldUpdateBlockletStatus(blocklet.status)) {
985
- blocklet.status = statusMap[blocklet.runtimeInfo.status];
986
- }
987
- } catch (err) {
988
- if (err.code !== 'BLOCKLET_PROCESS_404') {
989
- logger.error('failed to construct blocklet runtime info', { did, error: err });
990
- }
991
-
992
- if (blocklet.status === BlockletStatus.running) {
993
- await this.state.setBlockletStatus(did, BlockletStatus.stopped);
994
- blocklet.status = BlockletStatus.stopped;
995
- }
996
- }
997
-
998
- await Promise.all(
999
- blocklet.children.map(async (child) => {
1000
- child.engine = getEngine(getBlockletEngineNameByPlatform(child.meta)).describe();
1001
- child.diskInfo = await getDiskInfo(child, {
1002
- useFakeDiskInfo: !diskInfo,
1003
- });
1004
- if (child.meta.group !== BlockletGroup.gateway) {
1005
- try {
1006
- child.runtimeInfo = await getRuntimeInfo(child.env.appId);
1007
- child.status = statusMap[blocklet.runtimeInfo.status];
1008
- } catch (err) {
1009
- if (err.code !== 'BLOCKLET_PROCESS_404') {
1010
- logger.error('failed to construct blocklet runtime info', { did, error: err });
1011
- }
1012
- }
1013
- }
1014
- })
1015
- );
1283
+ const status = await getBlockletStatusFromProcess(blocklet);
1284
+ if (blocklet.status !== status) {
1285
+ const res = await states.blocklet.setBlockletStatus(did, status);
1286
+ this.emit(BlockletEvents.statusChange, res);
1287
+ return res;
1016
1288
  }
1017
1289
 
1018
1290
  return blocklet;
1019
1291
  } catch (err) {
1020
- const simpleState = await this.state.getBlocklet(did);
1021
- logger.error('failed to get blocklet info', {
1022
- did,
1023
- name: get(simpleState, 'meta.name'),
1024
- status: get(simpleState, 'status'),
1025
- error: err,
1026
- });
1027
- return simpleState;
1292
+ return fastReturnOnForceSync(blocklet);
1028
1293
  }
1029
1294
  }
1030
1295
 
@@ -1034,63 +1299,177 @@ class BlockletManager extends BaseBlockletManager {
1034
1299
  });
1035
1300
  }
1036
1301
 
1302
+ async updateAllBlockletEnvironment() {
1303
+ const blocklets = await states.blocklet.getBlocklets();
1304
+ for (let i = 0; i < blocklets.length; i++) {
1305
+ const blocklet = blocklets[i];
1306
+ await this._updateBlockletEnvironment(blocklet.meta.did);
1307
+ }
1308
+ }
1309
+
1310
+ async prune() {
1311
+ const blocklets = await states.blocklet.getBlocklets();
1312
+ const settings = await states.blockletExtras.listSettings();
1313
+ await pruneBlockletBundle({
1314
+ installDir: this.dataDirs.blocklets,
1315
+ blocklets,
1316
+ blockletSettings: settings
1317
+ .filter((x) => x.settings.children && x.settings.children.length)
1318
+ .map((x) => x.settings),
1319
+ });
1320
+ }
1321
+
1037
1322
  async onJob(job) {
1038
1323
  if (job.entity === 'blocklet') {
1039
1324
  if (job.action === 'download') {
1040
- await this.onDownload(job);
1325
+ await this._downloadAndInstall(job);
1041
1326
  }
1042
1327
  if (job.action === 'restart') {
1043
- await this.onRestart(job);
1328
+ await this._onRestart(job);
1044
1329
  }
1045
1330
 
1046
1331
  if (job.action === 'check_if_started') {
1047
- await this.onCheckIfStarted(job);
1332
+ await this._onCheckIfStarted(job);
1048
1333
  }
1049
1334
  }
1050
1335
  }
1051
1336
 
1052
- async onDownload({ blocklet, context, postAction, oldBlocklet, throwOnError }) {
1337
+ getCrons() {
1338
+ return [
1339
+ {
1340
+ name: 'sync-blocklet-status',
1341
+ time: '*/60 * * * * *', // 60s
1342
+ fn: this._syncBlockletStatus.bind(this),
1343
+ },
1344
+ {
1345
+ name: 'sync-blocklet-list',
1346
+ time: '*/60 * * * * *', // 60s
1347
+ fn: this.refreshListCache.bind(this),
1348
+ },
1349
+ {
1350
+ name: 'refresh-accessible-ip',
1351
+ time: '0 */10 * * * *', // 10min
1352
+ fn: async () => {
1353
+ const nodeInfo = await states.node.read();
1354
+ await refreshAccessibleExternalNodeIp(nodeInfo);
1355
+ },
1356
+ },
1357
+ {
1358
+ name: 'delete-expired-external-blocklet',
1359
+ time: '0 */30 * * * *', // 30min
1360
+ options: { runOnInit: false },
1361
+ fn: () => this._deleteExpiredExternalBlocklet(),
1362
+ },
1363
+ {
1364
+ name: 'clean-expired-blocklet-data',
1365
+ time: '0 */20 0 * * *', // 每天凌晨 0 点的每 20 分钟
1366
+ fn: () => this._cleanExpiredBlockletData(),
1367
+ },
1368
+ {
1369
+ name: 'record-blocklet-runtime-history',
1370
+ time: `*/${MONITOR_RECORD_INTERVAL_SEC} * * * * *`, // 10s
1371
+ fn: () => this.runtimeMonitor.monitAll(),
1372
+ },
1373
+ ];
1374
+ }
1375
+
1376
+ // ============================================================================================
1377
+ // Private API that are used by self of helper function
1378
+ // ============================================================================================
1379
+
1380
+ /**
1381
+ *
1382
+ *
1383
+ * @param {{
1384
+ * blocklet: {},
1385
+ * context: {},
1386
+ * postAction: 'install' | 'upgrade',
1387
+ * oldBlocklet: {},
1388
+ * throwOnError: Error,
1389
+ * skipCheckStatusBeforeDownload: boolean,
1390
+ * selectedComponentDids: Array<did>,
1391
+ * }} params
1392
+ * @return {*}
1393
+ * @memberof BlockletManager
1394
+ */
1395
+ async _downloadAndInstall(params) {
1396
+ const {
1397
+ blocklet,
1398
+ context,
1399
+ postAction,
1400
+ oldBlocklet,
1401
+ throwOnError,
1402
+ skipCheckStatusBeforeDownload,
1403
+ selectedComponentDids,
1404
+ } = params;
1053
1405
  const { meta } = blocklet;
1054
1406
  const { name, did, version } = meta;
1055
1407
 
1056
- try {
1057
- await preDownloadLock.acquire();
1058
-
1059
- const b0 = await this.state.getBlocklet(did);
1060
- if (!b0 || ![BlockletStatus.waiting, BlockletStatus.downloading].includes(b0.status)) {
1061
- if (!b0) {
1062
- logger.error('blocklet does not exist before downloading', { name, did });
1063
- } else {
1064
- logger.error('blocklet status is invalid before downloading', {
1065
- name,
1066
- did,
1067
- status: fromBlockletStatus(b0.status),
1068
- });
1408
+ // check status
1409
+ if (!skipCheckStatusBeforeDownload) {
1410
+ try {
1411
+ await statusLock.acquire();
1412
+
1413
+ const b0 = await states.blocklet.getBlocklet(did);
1414
+ if (!b0 || ![BlockletStatus.waiting].includes(b0.status)) {
1415
+ if (!b0) {
1416
+ throw new Error('blocklet does not exist before downloading');
1417
+ } else {
1418
+ throw new Error(`blocklet status is invalid before downloading: ${fromBlockletStatus(b0.status)}`);
1419
+ }
1069
1420
  }
1070
- preDownloadLock.release();
1421
+ statusLock.release();
1422
+ } catch (error) {
1423
+ statusLock.release();
1424
+ logger.error('Check blocklet status failed before downloading', {
1425
+ name,
1426
+ did,
1427
+ error,
1428
+ });
1071
1429
  await this._rollback(postAction, did, oldBlocklet);
1072
1430
  return;
1073
1431
  }
1432
+ }
1074
1433
 
1075
- const blocklet1 = await this.state.setBlockletStatus(did, BlockletStatus.downloading);
1076
- this.emit(BlockletEvents.statusChange, blocklet1);
1077
-
1078
- preDownloadLock.release();
1079
-
1080
- const { isCancelled } = await this._downloadBlocklet(blocklet, oldBlocklet);
1434
+ // download bundle
1435
+ try {
1436
+ const blockletForDownload = {
1437
+ ...blocklet,
1438
+ children: (blocklet.children || []).filter((x) => {
1439
+ if (selectedComponentDids?.length) {
1440
+ return selectedComponentDids.includes(x.meta.did);
1441
+ }
1442
+ return x;
1443
+ }),
1444
+ };
1445
+ const { isCancelled } = await this._downloadBlocklet(blockletForDownload, context);
1081
1446
 
1082
1447
  if (isCancelled) {
1083
- logger.info('Download was canceled manually', { name, did, version });
1084
- await this._rollback(postAction, did, oldBlocklet);
1448
+ logger.info('Download was canceled', { name, did, version });
1449
+
1450
+ // rollback on download cancelled
1451
+ await statusLock.acquire();
1452
+ try {
1453
+ if ((await states.blocklet.getBlockletStatus(did)) === BlockletStatus.downloading) {
1454
+ await this._rollback(postAction, did, oldBlocklet);
1455
+ }
1456
+ statusLock.release();
1457
+ } catch (error) {
1458
+ statusLock.release();
1459
+ logger.error('Rollback blocklet failed on download canceled', { postAction, name, did, version, error });
1460
+ }
1085
1461
  return;
1086
1462
  }
1087
1463
  } catch (err) {
1088
1464
  logger.error('Download blocklet tarball failed', { name, did, version });
1089
1465
 
1090
- preDownloadLock.release();
1091
-
1092
- this.emit(BlockletEvents.downloadFailed, { meta: did });
1093
- this.notification.create({
1466
+ this.emit(BlockletEvents.downloadFailed, {
1467
+ meta: { did },
1468
+ error: {
1469
+ message: err.message,
1470
+ },
1471
+ });
1472
+ this._createNotification(did, {
1094
1473
  title: 'Blocklet Download Failed',
1095
1474
  description: `Blocklet ${name}@${version} download failed with error: ${err.message}`,
1096
1475
  entityType: 'blocklet',
@@ -1098,10 +1477,16 @@ class BlockletManager extends BaseBlockletManager {
1098
1477
  severity: 'error',
1099
1478
  });
1100
1479
 
1480
+ // rollback on download failed
1481
+ await statusLock.acquire();
1101
1482
  try {
1102
- await this._rollback(postAction, did, oldBlocklet);
1103
- } catch (e) {
1104
- logger.error('Rollback blocklet failed', { postAction, name, did, version });
1483
+ if ((await states.blocklet.getBlockletStatus(did)) === BlockletStatus.downloading) {
1484
+ await this._rollback(postAction, did, oldBlocklet);
1485
+ }
1486
+ statusLock.release();
1487
+ } catch (error) {
1488
+ statusLock.release();
1489
+ logger.error('Rollback blocklet failed on download failed', { postAction, name, did, version, error });
1105
1490
  }
1106
1491
 
1107
1492
  if (throwOnError) {
@@ -1111,16 +1496,41 @@ class BlockletManager extends BaseBlockletManager {
1111
1496
  return;
1112
1497
  }
1113
1498
 
1499
+ // update status
1500
+ try {
1501
+ await statusLock.acquire();
1502
+
1503
+ if ((await states.blocklet.getBlockletStatus(did)) !== BlockletStatus.downloading) {
1504
+ throw new Error('blocklet status changed durning download');
1505
+ }
1506
+
1507
+ if (postAction === 'install') {
1508
+ const state = await states.blocklet.setBlockletStatus(did, BlockletStatus.installing);
1509
+ this.emit(BlockletEvents.statusChange, state);
1510
+ }
1511
+
1512
+ if (postAction === 'upgrade') {
1513
+ const state = await states.blocklet.setBlockletStatus(did, BlockletStatus.upgrading);
1514
+ this.emit(BlockletEvents.statusChange, state);
1515
+ }
1516
+
1517
+ statusLock.release();
1518
+ } catch (error) {
1519
+ logger.error(error.message);
1520
+ statusLock.release();
1521
+ }
1522
+
1523
+ // install
1114
1524
  try {
1115
1525
  // install blocklet
1116
1526
  if (postAction === 'install') {
1117
- await this.onInstall({ blocklet, context });
1527
+ await this._onInstall({ blocklet, context, oldBlocklet });
1118
1528
  return;
1119
1529
  }
1120
1530
 
1121
1531
  // upgrade blocklet
1122
- if (['upgrade', 'downgrade'].includes(postAction)) {
1123
- await this.onUpgrade({ oldBlocklet, newBlocklet: blocklet, action: postAction, context });
1532
+ if (postAction === 'upgrade') {
1533
+ await this._onUpgrade({ oldBlocklet, newBlocklet: blocklet, context });
1124
1534
  }
1125
1535
  } catch (error) {
1126
1536
  if (throwOnError) {
@@ -1129,26 +1539,27 @@ class BlockletManager extends BaseBlockletManager {
1129
1539
  }
1130
1540
  }
1131
1541
 
1132
- async onInstall({ blocklet, context }) {
1542
+ async _onInstall({ blocklet, context, oldBlocklet }) {
1133
1543
  const { meta } = blocklet;
1134
1544
  const { did, version } = meta;
1135
1545
  logger.info('do install blocklet', { did, version });
1136
1546
 
1137
- const state = await this.state.setBlockletStatus(did, BlockletStatus.installing);
1138
- this.emit(BlockletEvents.statusChange, state);
1139
-
1140
1547
  try {
1141
- await this._installBlocklet({
1548
+ const installedBlocklet = await this._installBlocklet({
1142
1549
  did,
1143
1550
  context,
1551
+ oldBlocklet,
1144
1552
  });
1145
1553
 
1146
1554
  if (context.startImmediately) {
1147
- try {
1148
- logger.info('start blocklet after installed', { did });
1149
- await this.start({ did, checkHealthImmediately: true });
1150
- } catch (error) {
1151
- logger.warn('attempt to start immediately failed', { did, error });
1555
+ const missingProps = getAppMissingConfigs(installedBlocklet);
1556
+ if (!missingProps.length) {
1557
+ try {
1558
+ logger.info('start blocklet after installed', { did });
1559
+ await this.start({ did, checkHealthImmediately: true });
1560
+ } catch (error) {
1561
+ logger.warn('attempt to start immediately failed', { did, error });
1562
+ }
1152
1563
  }
1153
1564
  }
1154
1565
  } catch (err) {
@@ -1156,12 +1567,9 @@ class BlockletManager extends BaseBlockletManager {
1156
1567
  }
1157
1568
  }
1158
1569
 
1159
- async onUpgrade({ oldBlocklet, newBlocklet, action, context }) {
1570
+ async _onUpgrade({ oldBlocklet, newBlocklet, context }) {
1160
1571
  const { version, did } = newBlocklet.meta;
1161
- logger.info(`do ${action} blocklet`, { did, version });
1162
-
1163
- const state = await this.state.setBlockletStatus(did, BlockletStatus.upgrading);
1164
- this.emit(BlockletEvents.statusChange, state);
1572
+ logger.info('do upgrade blocklet', { did, version });
1165
1573
 
1166
1574
  try {
1167
1575
  await this._upgradeBlocklet({
@@ -1174,15 +1582,17 @@ class BlockletManager extends BaseBlockletManager {
1174
1582
  }
1175
1583
  }
1176
1584
 
1177
- async onRestart({ did, context }) {
1585
+ async _onRestart({ did, context }) {
1178
1586
  await this.stop({ did, updateStatus: false }, context);
1179
1587
  await this.start({ did }, context);
1180
1588
  }
1181
1589
 
1182
- async onCheckIfStarted(jobInfo, { throwOnError } = {}) {
1183
- const { blocklet, context, minConsecutiveTime = 5000, timeout } = jobInfo;
1590
+ async _onCheckIfStarted(jobInfo, { throwOnError } = {}) {
1591
+ const { did, context, minConsecutiveTime = 5000, timeout } = jobInfo;
1592
+ const blocklet = await this.getBlocklet(did);
1593
+
1184
1594
  const { meta } = blocklet;
1185
- const { did, name } = meta;
1595
+ const { name } = meta;
1186
1596
 
1187
1597
  try {
1188
1598
  // healthy check
@@ -1192,12 +1602,18 @@ class BlockletManager extends BaseBlockletManager {
1192
1602
  const res = await this.status(did, { forceSync: true });
1193
1603
  this.emit(BlockletEvents.started, res);
1194
1604
  } catch (error) {
1605
+ const status = await states.blocklet.getBlockletStatus(did);
1606
+ if ([BlockletStatus.stopping, BlockletStatus.stopped].includes(status)) {
1607
+ logger.info(`Check blocklet healthy failing because blocklet is ${fromBlockletStatus(status)}`);
1608
+ return;
1609
+ }
1610
+
1195
1611
  logger.error('check blocklet if started failed', { did, name, context, timeout, error });
1196
1612
 
1197
1613
  await this.deleteProcess({ did }, context);
1198
- await this.state.setBlockletStatus(did, BlockletStatus.error);
1614
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.error);
1199
1615
 
1200
- this.notification.create({
1616
+ this._createNotification(did, {
1201
1617
  title: 'Blocklet Start Failed',
1202
1618
  description: `Blocklet ${name} start failed: ${error.message}`,
1203
1619
  entityType: 'blocklet',
@@ -1213,620 +1629,212 @@ class BlockletManager extends BaseBlockletManager {
1213
1629
  }
1214
1630
  }
1215
1631
 
1216
- async updateBlockletEnvironment(did) {
1217
- const blockletWithEnv = await this.ensureBlocklet(did);
1218
- const blocklet = await this.state.getBlocklet(did);
1219
- const nodeInfo = await this.node.read();
1632
+ async _updateBlockletEnvironment(did) {
1633
+ const blockletWithEnv = await this.getBlocklet(did);
1634
+ const blocklet = await states.blocklet.getBlocklet(did);
1635
+ const nodeInfo = await states.node.read();
1220
1636
 
1221
- const rootSystemEnvironments = getRootSystemEnvironments(blockletWithEnv, nodeInfo);
1222
- const rootConfig = blockletWithEnv.configObj;
1223
- const overwrittenEnvironments = getOverwrittenEnvironments(blockletWithEnv, nodeInfo);
1637
+ const appSystemEnvironments = {
1638
+ ...getAppSystemEnvironments(blockletWithEnv, nodeInfo),
1639
+ ...getAppOverwrittenEnvironments(blockletWithEnv, nodeInfo),
1640
+ };
1224
1641
 
1225
- // fill environments to blocklet and blocklet.children
1642
+ // fill environments to blocklet and components
1226
1643
  blocklet.environments = formatEnvironments({
1227
- ...rootConfig,
1228
- ...getSystemEnvironments(blockletWithEnv),
1229
- ...rootSystemEnvironments,
1230
- ...overwrittenEnvironments,
1644
+ ...getComponentSystemEnvironments(blockletWithEnv),
1645
+ ...appSystemEnvironments,
1231
1646
  });
1232
1647
 
1233
- for (const child of blocklet.children) {
1234
- const childWithEnv = blockletWithEnv.children.find((x) => x.meta.did === child.meta.did);
1648
+ const envMap = {};
1649
+ forEachBlockletSync(blockletWithEnv, (child, { ancestors }) => {
1650
+ const id = getComponentId(child, ancestors);
1651
+ envMap[id] = child;
1652
+ });
1653
+
1654
+ forEachChildSync(blocklet, (child, { ancestors }) => {
1655
+ const id = getComponentId(child, ancestors);
1656
+
1657
+ const childWithEnv = envMap[id];
1235
1658
  if (childWithEnv) {
1236
- const childConfig = childWithEnv.configObj;
1237
1659
  child.environments = formatEnvironments({
1238
- ...childConfig, // custom env of child blocklet
1239
- ...rootConfig, // // custom env of root blocklet FIXME: use options or hooks to make merge logic flexible
1240
- ...getSystemEnvironments(childWithEnv), // system env of child blocklet
1241
- ...rootSystemEnvironments, // system env of root blocklet
1242
- ...overwrittenEnvironments,
1660
+ ...getComponentSystemEnvironments(childWithEnv), // system env of child blocklet
1661
+ ...appSystemEnvironments, // system env of root blocklet
1243
1662
  });
1244
1663
  }
1245
- }
1664
+ });
1246
1665
 
1247
- // update state to db
1248
- return this.state.updateById(blocklet._id, blocklet);
1249
- }
1250
-
1251
- async updateAllBlockletEnvironment() {
1252
- const blocklets = await this.state.getBlocklets();
1253
- for (let i = 0; i < blocklets.length; i++) {
1254
- const blocklet = blocklets[i];
1255
- await this.updateBlockletEnvironment(blocklet.meta.did);
1256
- }
1257
- }
1258
-
1259
- async _installFromRegistry({ did, registry }, context) {
1260
- logger.debug('start install blocklet', { did });
1261
- if (!isValidDid(did)) {
1262
- throw new Error('Blocklet did is invalid');
1263
- }
1666
+ // put BLOCKLET_APP_ID at root level for indexing
1667
+ blocklet.appDid = appSystemEnvironments.BLOCKLET_APP_ID;
1264
1668
 
1265
- const registryUrl = registry || (await states.node.getBlockletRegistry());
1266
- const info = await BlockletRegistry.getRegistryMeta(registryUrl);
1267
- const blocklet = await this.registry.getBlocklet(did, registryUrl);
1268
- if (!blocklet) {
1269
- throw new Error('Can not install blocklet that not found in registry');
1669
+ if (!Array.isArray(blocklet.migratedFrom)) {
1670
+ blocklet.migratedFrom = [];
1270
1671
  }
1271
-
1272
- const state = await this.state.getBlocklet(blocklet.did);
1273
- if (state) {
1274
- throw new Error('Can not install an already installed blocklet');
1275
- }
1276
-
1277
- if (isFreeBlocklet(blocklet) === false && !context.blockletPurchaseVerified) {
1278
- throw new Error('Can not install a non-free blocklet directly');
1672
+ // This can only be set once, can be used for indexing, will not change ever
1673
+ if (!blocklet.appPid) {
1674
+ blocklet.appPid = appSystemEnvironments.BLOCKLET_APP_PID;
1279
1675
  }
1280
1676
 
1281
- // install
1282
- return this._install({
1283
- meta: blocklet,
1284
- source: BlockletSource.registry,
1285
- deployedFrom: info.cdnUrl || registryUrl,
1286
- context,
1287
- });
1677
+ // update state to db
1678
+ return states.blocklet.updateBlocklet(did, blocklet);
1288
1679
  }
1289
1680
 
1290
- async _installFromUrl({ url, sync }, context) {
1291
- logger.debug('start install blocklet', { url });
1292
-
1293
- const meta = await getBlockletMetaFromUrl(url);
1294
-
1295
- if (!meta) {
1296
- throw new Error(`Can not install blocklet that not found by url: ${url}`);
1681
+ async _attachRuntimeInfo({ did, nodeInfo, diskInfo = true, context, cachedBlocklet }) {
1682
+ if (!did) {
1683
+ throw new Error('did should not be empty');
1297
1684
  }
1298
1685
 
1299
- // upgrade
1300
- const exist = await this.state.getBlocklet(meta.did);
1301
- if (exist) {
1302
- return this._upgrade({
1303
- meta,
1304
- source: BlockletSource.url,
1305
- deployedFrom: url,
1306
- sync,
1307
- context,
1308
- });
1309
- }
1686
+ try {
1687
+ const blocklet = await this.getBlocklet(did, { throwOnNotExist: false });
1310
1688
 
1311
- // install
1312
- return this._install({
1313
- meta,
1314
- source: BlockletSource.url,
1315
- deployedFrom: url,
1316
- sync,
1317
- context,
1318
- });
1319
- }
1689
+ if (!blocklet) {
1690
+ return null;
1691
+ }
1320
1692
 
1321
- async _installFromUpload({ file, did, diffVersion, deleteSet, context }) {
1322
- logger.info('install blocklet', { from: 'upload file' });
1323
- // download
1324
- // const { filename, mimetype, encoding, createReadStream } = await file;
1325
- const { filename, createReadStream } = await file;
1326
- const cwd = path.join(this.dataDirs.tmp, 'download');
1327
- const tarFile = path.join(cwd, `${path.basename(filename, path.extname(filename))}.tgz`);
1328
- await fs.ensureDir(cwd);
1329
- await new Promise((resolve, reject) => {
1330
- const readStream = createReadStream();
1331
- const writeStream = fs.createWriteStream(tarFile);
1332
- readStream
1333
- .pipe(new Throttle({ rate: 1024 * 1024 * 20 })) // 20MB
1334
- .pipe(writeStream);
1335
- readStream.on('error', (error) => {
1336
- logger.error('File upload read stream failed', { error });
1337
- writeStream.destroy(new Error('File upload read stream failed'));
1338
- });
1339
- writeStream.on('error', (error) => {
1340
- logger.error('File upload write stream failed', { error });
1341
- fs.removeSync(tarFile);
1342
- reject(error);
1343
- });
1344
- writeStream.on('finish', resolve);
1345
- });
1693
+ const fromCache = !!cachedBlocklet;
1346
1694
 
1347
- // diff deploy
1348
- if (did && diffVersion) {
1349
- const oldBlocklet = await this.state.getBlocklet(did);
1350
- if (!oldBlocklet) {
1351
- throw new Error(`Blocklet ${did} not found when diff deploying`);
1352
- }
1353
- if (oldBlocklet.meta.version !== diffVersion) {
1354
- logger.error('Diff deploy: Blocklet version changed', {
1355
- preVersion: diffVersion,
1356
- changedVersion: oldBlocklet.meta.version,
1357
- name: oldBlocklet.meta.name,
1358
- did: oldBlocklet.meta.did,
1695
+ // if from cached data, only use cache data of runtime info (engine, diskInfo, runtimeInfo...)
1696
+ if (fromCache) {
1697
+ const cached = {};
1698
+ forEachBlockletSync(cachedBlocklet, (component, { id }) => {
1699
+ cached[id] = component;
1359
1700
  });
1360
- throw new Error('Blocklet version changed when diff deploying');
1361
- }
1362
- if (isInProgress(oldBlocklet.status)) {
1363
- logger.error(`Can not deploy blocklet when it is ${fromBlockletStatus(oldBlocklet.status)}`);
1364
- throw new Error(`Can not deploy blocklet when it is ${fromBlockletStatus(oldBlocklet.status)}`);
1365
- }
1366
-
1367
- const { meta } = await this._resolveDiffDownload(cwd, tarFile, deleteSet, oldBlocklet);
1368
- const childrenMeta = await getChildrenMeta(meta);
1369
- mergeMeta(meta, childrenMeta);
1370
- const newBlocklet = await this.state.getBlocklet(did);
1371
- newBlocklet.meta = meta;
1372
- newBlocklet.source = BlockletSource.upload;
1373
- newBlocklet.deployedFrom = `Upload by ${context.user.did}`;
1374
- newBlocklet.children = await this.state.getChildrenFromMetas(childrenMeta);
1375
- await validateBlocklet(newBlocklet);
1376
- await this._downloadBlocklet(newBlocklet, oldBlocklet);
1377
-
1378
- return this._upgradeBlocklet({
1379
- oldBlocklet,
1380
- newBlocklet,
1381
- context,
1382
- });
1383
- }
1384
1701
 
1385
- // full deploy
1386
- const { meta } = await this._resolveDownload(cwd, tarFile);
1387
- const oldBlocklet = await this.state.getBlocklet(meta.did);
1702
+ Object.assign(blocklet, pick(cachedBlocklet, ['appRuntimeInfo', 'diskInfo']));
1388
1703
 
1389
- if (oldBlocklet) {
1390
- if (isInProgress(oldBlocklet.status)) {
1391
- logger.error(`Can not deploy blocklet when it is ${fromBlockletStatus(oldBlocklet.status)}`);
1392
- throw new Error(`Can not deploy blocklet when it is ${fromBlockletStatus(oldBlocklet.status)}`);
1704
+ forEachBlockletSync(blocklet, (component, { id }) => {
1705
+ if (cached[id]) {
1706
+ Object.assign(component, pick(cached[id], ['runtimeInfo']));
1707
+ }
1708
+ });
1393
1709
  }
1394
1710
 
1395
- const childrenMeta = await getChildrenMeta(meta);
1396
- mergeMeta(meta, childrenMeta);
1397
- const newBlocklet = await this.state.getBlocklet(meta.did);
1398
- newBlocklet.meta = meta;
1399
- newBlocklet.source = BlockletSource.upload;
1400
- newBlocklet.deployedFrom = `Upload by ${context.user.did}`;
1401
- newBlocklet.children = await this.state.getChildrenFromMetas(childrenMeta);
1402
-
1403
- await validateBlocklet(newBlocklet);
1404
- await this._downloadBlocklet(newBlocklet, oldBlocklet);
1405
-
1406
- return this._upgradeBlocklet({
1407
- oldBlocklet,
1408
- newBlocklet,
1409
- context,
1410
- });
1411
- }
1412
-
1413
- const childrenMeta = await getChildrenMeta(meta);
1414
- mergeMeta(meta, childrenMeta);
1415
- const blocklet = await this.state.addBlocklet({
1416
- did: meta.did,
1417
- meta,
1418
- source: BlockletSource.upload,
1419
- deployedFrom: `Upload by ${context.user.did}`,
1420
- childrenMeta,
1421
- });
1422
-
1423
- await validateBlocklet(blocklet);
1424
-
1425
- await this._setConfigs(meta.did);
1426
-
1427
- // download
1428
- await this._downloadBlocklet(blocklet);
1429
- return this._installBlocklet({
1430
- did: meta.did,
1431
- context,
1432
- });
1433
- }
1434
-
1435
- /**
1436
- * add to download job queue
1437
- * @param {string} did blocklet did
1438
- * @param {object} blocklet object
1439
- */
1440
- async download(did, blocklet) {
1441
- await this.state.setBlockletStatus(did, BlockletStatus.waiting);
1442
- this.installQueue.push(
1443
- {
1444
- entity: 'blocklet',
1445
- action: 'download',
1446
- id: did,
1447
- blocklet: { ...blocklet },
1448
- postAction: 'install',
1449
- },
1450
- did
1451
- );
1452
- }
1453
-
1454
- async getStatus(did) {
1455
- if (!did) {
1456
- throw new Error('did is required');
1457
- }
1458
-
1459
- const blocklet = await this.state.findOne({ 'meta.did': did }, { meta: 1, status: 1 });
1460
- if (!blocklet) {
1461
- return null;
1462
- }
1463
-
1464
- return { name: blocklet.meta.name, did: blocklet.meta.did, status: blocklet.status };
1465
- }
1466
-
1467
- async prune() {
1468
- const blocklets = await this.state.getBlocklets();
1469
- await pruneBlockletBundle(blocklets, this.dataDirs.blocklets);
1470
- }
1471
-
1472
- async getLatestBlockletVersion({ did, version }) {
1473
- const blocklet = await this.state.getBlocklet(did);
1474
- if (!blocklet) {
1475
- throw new Error('the blocklet is not installed');
1476
- }
1477
-
1478
- let versions = this.cachedBlockletVersions.get(did);
1711
+ // 处理 domainAliases#value SLOT_FOR_IP_DNS_SITE
1712
+ if (blocklet?.site?.domainAliases?.length) {
1713
+ const nodeIp = await getAccessibleExternalNodeIp(nodeInfo);
1714
+ blocklet.site.domainAliases = blocklet.site.domainAliases.map((x) => ({
1715
+ ...x,
1716
+ value: util.replaceDomainSlot({ domain: x.value, context, nodeIp }),
1717
+ }));
1718
+ }
1479
1719
 
1480
- if (!versions) {
1481
- const { blockletRegistryList } = await this.node.read();
1482
- const tasks = blockletRegistryList.map((registry) =>
1483
- this.registry
1484
- .getBlockletMeta({ did, registryUrl: registry.url })
1485
- .then((item) => ({ did, version: item.version, registryUrl: registry.url }))
1486
- .catch((error) => {
1487
- if (error.response && error.response.status === 404) {
1488
- return;
1489
- }
1490
- logger.error('get blocklet meta from registry failed', { did, error });
1491
- })); // prettier-ignore
1720
+ // app runtime info, app status
1721
+ blocklet.appRuntimeInfo = this.runtimeMonitor.getRuntimeInfo(blocklet.meta.did);
1492
1722
 
1493
- versions = await Promise.all(tasks);
1494
- this.cachedBlockletVersions.set(did, versions);
1495
- }
1723
+ if (!fromCache) {
1724
+ // app disk info, component runtime info, component status, component engine
1725
+ await forEachBlocklet(blocklet, async (component, { level }) => {
1726
+ component.engine = getEngine(getBlockletEngineNameByPlatform(component.meta)).describe();
1496
1727
 
1497
- versions = versions.filter((item) => item && semver.gt(item.version, version));
1728
+ if (level === 0) {
1729
+ component.diskInfo = await getDiskInfo(component, {
1730
+ useFakeDiskInfo: !diskInfo,
1731
+ });
1732
+ }
1498
1733
 
1499
- if (versions.length === 0) {
1500
- return null;
1501
- }
1734
+ component.runtimeInfo = this.runtimeMonitor.getRuntimeInfo(blocklet.meta.did, component.env.id);
1502
1735
 
1503
- let latestBlockletVersion = versions[0];
1504
- versions.forEach((item) => {
1505
- if (semver.lt(latestBlockletVersion.version, item.version)) {
1506
- latestBlockletVersion = item;
1736
+ if (component.runtimeInfo?.status && shouldUpdateBlockletStatus(component.status)) {
1737
+ component.status = statusMap[component.runtimeInfo.status];
1738
+ }
1739
+ });
1507
1740
  }
1508
- });
1509
-
1510
- return latestBlockletVersion;
1511
- }
1512
1741
 
1513
- getCrons() {
1514
- return [
1515
- {
1516
- name: 'sync-blocklet-status',
1517
- time: '*/60 * * * * *', // 60s
1518
- fn: this._syncBlockletStatus.bind(this),
1519
- },
1520
- {
1521
- name: 'sync-blocklet-list',
1522
- time: '*/60 * * * * *', // 60s
1523
- fn: this.refreshListCache.bind(this),
1524
- },
1525
- {
1526
- name: 'refresh-accessible-ip',
1527
- time: '0 */10 * * * *', // 10min
1528
- fn: async () => {
1529
- const nodeInfo = await this.node.read();
1530
- await refreshAccessibleExternalNodeIp(nodeInfo);
1531
- },
1532
- },
1533
- ];
1742
+ return blocklet;
1743
+ } catch (err) {
1744
+ const simpleState = await states.blocklet.getBlocklet(did);
1745
+ logger.error('failed to get blocklet info', {
1746
+ did,
1747
+ name: get(simpleState, 'meta.name'),
1748
+ status: get(simpleState, 'status'),
1749
+ error: err,
1750
+ });
1751
+ return simpleState;
1752
+ }
1534
1753
  }
1535
1754
 
1536
1755
  async _syncBlockletStatus() {
1537
1756
  const run = async (blocklet) => {
1538
1757
  try {
1539
- await this.status(blocklet.meta.did, { forceSync: true });
1758
+ await this.status(blocklet.meta.did);
1540
1759
  } catch (err) {
1541
1760
  logger.error('sync blocklet status failed', { error: err });
1542
1761
  }
1543
1762
  };
1544
- const blocklets = await this.state.getBlocklets();
1763
+ const blocklets = await states.blocklet.getBlocklets();
1545
1764
  blocklets.forEach(run);
1546
1765
  }
1547
1766
 
1548
- async _install({ meta, source, deployedFrom, context, sync }) {
1549
- validateBlockletMeta(meta, { ensureDist: true });
1550
-
1551
- const { name, did, version } = meta;
1552
-
1553
- const childrenMeta = await getChildrenMeta(meta);
1554
- mergeMeta(meta, childrenMeta);
1555
- try {
1556
- const blocklet = await this.state.addBlocklet({
1557
- did: meta.did,
1558
- meta,
1559
- source,
1560
- deployedFrom,
1561
- childrenMeta,
1562
- });
1563
-
1564
- await validateBlocklet(blocklet);
1565
-
1566
- await this._setConfigs(did);
1567
-
1568
- logger.info('blocklet added to database', { meta });
1569
-
1570
- const blocklet1 = await this.state.setBlockletStatus(did, BlockletStatus.waiting);
1571
- this.emit(BlockletEvents.added, blocklet1);
1572
-
1573
- // download
1574
- const downloadParams = {
1575
- blocklet: { ...blocklet1 },
1576
- context,
1577
- postAction: 'install',
1578
- };
1579
-
1580
- if (sync) {
1581
- await this.onDownload({ ...downloadParams, throwOnError: true });
1582
- return this.state.getBlocklet(did);
1583
- }
1584
-
1585
- const ticket = this.installQueue.push(
1586
- {
1587
- entity: 'blocklet',
1588
- action: 'download',
1589
- id: did,
1590
- ...downloadParams,
1591
- },
1592
- did
1593
- );
1594
- ticket.on('failed', async (err) => {
1595
- logger.error('failed to install blocklet', { name, did, version, error: err });
1596
- try {
1597
- await this._delExtras(did);
1598
- await this.state.deleteBlocklet(did);
1599
- } catch (e) {
1600
- logger.error('failed to remove blocklet on install error', { did: meta.did, error: e });
1601
- }
1602
-
1603
- this.notification.create({
1604
- title: 'Blocklet Install Failed',
1605
- description: `Blocklet ${name}@${version} install failed with error: ${err.message || 'queue exception'}`,
1606
- entityType: 'blocklet',
1607
- entityId: did,
1608
- severity: 'error',
1609
- });
1610
- });
1611
-
1612
- return blocklet1;
1613
- } catch (err) {
1614
- logger.error('failed to install blocklet', { name, did, version, error: err });
1615
- this.notification.create({
1616
- title: 'Blocklet Install Failed',
1617
- description: `Blocklet ${name}@${version} install failed with error: ${err.message || 'queue exception'}`,
1618
- entityType: 'blocklet',
1619
- entityId: did,
1620
- severity: 'error',
1621
- });
1622
-
1623
- try {
1624
- await this._rollback('install', did);
1625
- } catch (e) {
1626
- logger.error('failed to remove blocklet on install error', { did: meta.did, error: e });
1627
- }
1628
-
1629
- throw err;
1767
+ async _getChildrenForInstallation(component) {
1768
+ if (!component) {
1769
+ return [];
1630
1770
  }
1631
- }
1632
-
1633
- async _upgrade({ meta, source, deployedFrom, context, sync }) {
1634
- validateBlockletMeta(meta, { ensureDist: true });
1635
-
1636
- const { name, version, did } = meta;
1637
-
1638
- const oldBlocklet = await this.state.getBlocklet(did);
1639
-
1640
- // NOTE: 目前的版本移除了降级通道,所以不需要考虑降级通道的情况
1641
- const action = semver.gt(oldBlocklet.meta.version, version) ? 'downgrade' : 'upgrade';
1642
- logger.info(`${action} blocklet`, { did, version });
1643
-
1644
- const newBlocklet = await this.state.setBlockletStatus(did, BlockletStatus.waiting);
1645
- const childrenMeta = await getChildrenMeta(meta);
1646
- mergeMeta(meta, childrenMeta);
1647
- newBlocklet.meta = meta;
1648
- newBlocklet.source = source;
1649
- newBlocklet.deployedFrom = deployedFrom;
1650
- newBlocklet.children = await this.state.getChildrenFromMetas(childrenMeta);
1651
-
1652
- await validateBlocklet(newBlocklet);
1653
-
1654
- this.emit(BlockletEvents.statusChange, newBlocklet);
1655
1771
 
1656
- // download
1657
- const downloadParams = {
1658
- oldBlocklet: { ...oldBlocklet },
1659
- blocklet: { ...newBlocklet },
1660
- version,
1661
- context,
1662
- postAction: action,
1663
- };
1664
-
1665
- if (sync) {
1666
- await this.onDownload({ ...downloadParams, throwOnError: true });
1667
- return this.state.getBlocklet(did);
1772
+ const { dynamicComponents } = await parseComponents(component);
1773
+ if (component.meta.group !== BlockletGroup.gateway) {
1774
+ dynamicComponents.unshift(component);
1668
1775
  }
1669
1776
 
1670
- const ticket = this.installQueue.push(
1671
- {
1672
- entity: 'blocklet',
1673
- action: 'download',
1674
- id: did,
1675
- ...downloadParams,
1676
- },
1677
- did
1678
- );
1679
-
1680
- ticket.on('failed', async (err) => {
1681
- logger.error('queue failed', { entity: 'blocklet', action, did, version, name, error: err });
1682
- await this._rollback(action, did, oldBlocklet);
1683
- this.emit(`blocklet.${action}.failed`, { did, version, err });
1684
- this.notification.create({
1685
- title: `Blocklet ${capitalize(action)} Failed`,
1686
- description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message || 'queue exception'}`,
1687
- entityType: 'blocklet',
1688
- entityId: did,
1689
- severity: 'error',
1690
- });
1691
- });
1692
- return newBlocklet;
1777
+ const children = filterDuplicateComponents(dynamicComponents);
1778
+ checkVersionCompatibility(children);
1779
+ return children;
1693
1780
  }
1694
1781
 
1695
- /**
1696
- * decompress file, format dir and move to installDir
1697
- * @param {string} cwd
1698
- * @param {string} tarFile
1699
- * @param {object} originalMeta for verification
1700
- * @param {object} option
1701
- */
1702
- async _resolveDownload(cwd, tarFile, originalMeta, { removeTarFile = true } = {}) {
1703
- const downloadDir = path.join(cwd, `${path.basename(tarFile, path.extname(tarFile))}`);
1704
- const tmp = `${downloadDir}-tmp`;
1705
- try {
1706
- await expandTarball({ source: tarFile, dest: tmp, strip: 0 });
1707
- } catch (error) {
1708
- logger.error('expand blocklet tar file error');
1709
- throw error;
1710
- } finally {
1711
- if (removeTarFile) {
1712
- fs.removeSync(tarFile);
1713
- }
1714
- }
1715
- let installDir;
1716
- let meta;
1717
- try {
1718
- // resolve dir
1719
- let dir = tmp;
1720
- const files = await asyncFs.readdir(dir);
1721
- if (files.includes('package')) {
1722
- dir = path.join(tmp, 'package');
1723
- } else if (files.length === 1) {
1724
- const d = path.join(dir, files[0]);
1725
- if ((await asyncFs.stat(d)).isDirectory()) {
1726
- dir = d;
1727
- }
1728
- }
1729
-
1730
- if (fs.existsSync(path.join(dir, BLOCKLET_BUNDLE_FOLDER))) {
1731
- dir = path.join(dir, BLOCKLET_BUNDLE_FOLDER);
1732
- }
1733
-
1734
- logger.info('Move downloadDir to installDir', { downloadDir });
1735
- await fs.move(dir, downloadDir, { overwrite: true });
1736
- fs.removeSync(tmp);
1737
-
1738
- meta = getBlockletMeta(downloadDir, { ensureMain: true });
1739
- const { did, name, version } = meta;
1740
-
1741
- // validate
1742
- if (
1743
- originalMeta &&
1744
- (originalMeta.did !== did || originalMeta.name !== name || originalMeta.version !== version)
1745
- ) {
1746
- logger.error('Meta has differences', { originalMeta, meta });
1747
- throw new Error('There are differences between the meta from tarball file and the original meta');
1748
- }
1749
- await validateBlockletEntry(downloadDir, meta);
1782
+ async _addBlocklet({ component, mode = BLOCKLET_MODES.PRODUCTION, name, did, title, description }) {
1783
+ const meta = {
1784
+ name,
1785
+ did,
1786
+ title: title || component?.meta?.title || '',
1787
+ description: description || component?.meta?.description || '',
1788
+ version: BLOCKLET_DEFAULT_VERSION,
1789
+ group: BlockletGroup.gateway,
1790
+ interfaces: [
1791
+ {
1792
+ type: BLOCKLET_INTERFACE_TYPE_WEB,
1793
+ name: BLOCKLET_INTERFACE_PUBLIC,
1794
+ path: BLOCKLET_DEFAULT_PATH_REWRITE,
1795
+ prefix: BLOCKLET_DYNAMIC_PATH_PREFIX,
1796
+ port: BLOCKLET_DEFAULT_PORT_NAME,
1797
+ protocol: BLOCKLET_INTERFACE_PROTOCOL_HTTP,
1798
+ },
1799
+ ],
1800
+ specVersion: BLOCKLET_LATEST_SPEC_VERSION,
1801
+ environments: component?.meta?.environments || [],
1802
+ timeout: {
1803
+ start: process.env.NODE_ENV === 'test' ? 10 : 60,
1804
+ },
1805
+ };
1750
1806
 
1751
- await ensureBlockletExpanded(meta, downloadDir);
1807
+ const children = component ? await this._getChildrenForInstallation(component) : [];
1752
1808
 
1753
- installDir = path.join(this.installDir, name, version);
1754
- if (fs.existsSync(installDir)) {
1755
- fs.removeSync(installDir);
1756
- logger.info('cleanup blocklet upgrade dir', { name, version, installDir });
1757
- } else {
1758
- fs.mkdirSync(installDir, { recursive: true });
1759
- }
1760
- await fs.move(downloadDir, installDir, { overwrite: true });
1761
- } catch (error) {
1762
- fs.removeSync(downloadDir);
1763
- fs.removeSync(tmp);
1764
- throw error;
1809
+ if (children[0]?.meta?.logo) {
1810
+ meta.logo = children[0].meta.logo;
1765
1811
  }
1766
1812
 
1767
- return { meta, installDir };
1768
- }
1769
-
1770
- async _resolveDiffDownload(cwd, tarFile, deleteSet, blocklet) {
1771
- logger.info('Resolve diff download', { tarFile, cwd });
1772
- const downloadDir = path.join(cwd, `${path.basename(tarFile, path.extname(tarFile))}`);
1773
- const diffDir = `${downloadDir}-diff`;
1774
- try {
1775
- await expandTarball({ source: tarFile, dest: diffDir, strip: 0 });
1776
- fs.removeSync(tarFile);
1777
- } catch (error) {
1778
- fs.removeSync(tarFile);
1779
- logger.error('expand blocklet tar file error');
1780
- throw error;
1781
- }
1782
- logger.info('Copy installDir to downloadDir', { installDir: this.installDir, downloadDir });
1783
- await fs.copy(path.join(this.installDir, blocklet.meta.name, blocklet.meta.version), downloadDir);
1784
- try {
1785
- // delete
1786
- logger.info('Delete files from downloadDir', { fileNum: deleteSet.length });
1787
- // eslint-disable-next-line no-restricted-syntax
1788
- for (const file of deleteSet) {
1789
- await fs.remove(path.join(downloadDir, file));
1790
- }
1791
- // walk & cover
1792
- logger.info('Move files from diffDir to downloadDir', { diffDir, downloadDir });
1793
- const walkDiff = async (dir) => {
1794
- const files = await asyncFs.readdir(dir);
1795
- // eslint-disable-next-line no-restricted-syntax
1796
- for (const file of files) {
1797
- const p = path.join(dir, file);
1798
- const stat = await asyncFs.stat(p);
1799
- if (stat.isDirectory()) {
1800
- await walkDiff(p);
1801
- } else if (stat.isFile()) {
1802
- await fs.move(p, path.join(downloadDir, path.relative(diffDir, p)), { overwrite: true });
1803
- }
1804
- }
1805
- };
1806
- await walkDiff(diffDir);
1807
- fs.removeSync(diffDir);
1808
- const meta = getBlockletMeta(downloadDir, { ensureMain: true });
1813
+ await validateBlocklet({ meta, children });
1809
1814
 
1810
- await ensureBlockletExpanded(meta, downloadDir);
1815
+ // fake install bundle
1816
+ const bundleDir = getBundleDir(this.installDir, meta);
1817
+ fs.mkdirSync(bundleDir, { recursive: true });
1818
+ updateMetaFile(path.join(bundleDir, BLOCKLET_META_FILE), meta);
1811
1819
 
1812
- const installDir = path.join(this.installDir, meta.name, meta.version);
1813
- logger.info('Move downloadDir to installDir', { downloadDir, installDir });
1814
- await fs.move(downloadDir, installDir, { overwrite: true });
1820
+ // add blocklet to db
1821
+ const blocklet = await states.blocklet.addBlocklet({
1822
+ meta,
1823
+ source: BlockletSource.custom,
1824
+ children,
1825
+ mode,
1826
+ });
1815
1827
 
1816
- return { meta, installDir };
1817
- } catch (error) {
1818
- fs.removeSync(downloadDir);
1819
- fs.removeSync(diffDir);
1820
- throw error;
1821
- }
1828
+ return blocklet;
1822
1829
  }
1823
1830
 
1824
1831
  /**
1825
1832
  * @param {string} opt.did
1826
1833
  * @param {object} opt.context
1827
1834
  */
1828
- async _installBlocklet({ did, context }) {
1835
+ async _installBlocklet({ did, oldBlocklet, context, createNotification = true }) {
1829
1836
  try {
1837
+ // should ensure blocklet integrity
1830
1838
  let blocklet = await this.ensureBlocklet(did);
1831
1839
  const { meta, source, deployedFrom } = blocklet;
1832
1840
 
@@ -1839,58 +1847,82 @@ class BlockletManager extends BaseBlockletManager {
1839
1847
  }
1840
1848
 
1841
1849
  // pre install
1842
- const nodeEnvironments = await this.node.getEnvironments();
1843
- const preInstall = (b) =>
1844
- hooks.preInstall({
1845
- hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
1846
- env: { ...nodeEnvironments },
1847
- appDir: blocklet.env.appDir,
1848
- did, // root blocklet did
1849
- notification: this.notification,
1850
- context,
1851
- });
1852
- await forEachBlocklet(blocklet, preInstall, { parallel: true });
1850
+ await this._runPreInstallHook(blocklet, context);
1853
1851
 
1854
1852
  // Add environments
1855
- await this.updateBlockletEnvironment(meta.did);
1856
- blocklet = await this.ensureBlocklet(did);
1853
+ await this._updateBlockletEnvironment(meta.did);
1854
+ blocklet = await this.getBlocklet(did);
1857
1855
 
1858
1856
  // post install
1859
- const postInstall = (b) =>
1860
- hooks.postInstall({
1861
- hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
1862
- env: getRuntimeEnvironments(b, nodeEnvironments),
1863
- appDir: blocklet.env.appDir,
1864
- did, // root blocklet did
1865
- notification: this.notification,
1866
- context,
1857
+ await this._runPostInstallHook(blocklet, context);
1858
+
1859
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.installed);
1860
+ blocklet = await this.getBlocklet(did);
1861
+ logger.info('blocklet installed', { source, did: meta.did });
1862
+
1863
+ // logo
1864
+ if (blocklet.children[0]?.meta?.logo) {
1865
+ const fileName = blocklet.children[0].meta.logo;
1866
+ const src = path.join(getBundleDir(this.installDir, blocklet.children[0].meta), fileName);
1867
+ const dist = path.join(getBundleDir(this.installDir, blocklet.meta), fileName);
1868
+ await fs.copy(src, dist);
1869
+ }
1870
+
1871
+ await fs.writeFile(path.join(blocklet.env.dataDir, 'logo.svg'), createDidLogo(blocklet.meta.did));
1872
+
1873
+ // Init db
1874
+ await this.teamManager.initTeam(blocklet.meta.did);
1875
+
1876
+ // Update dependents
1877
+ await this._updateDependents(did);
1878
+ blocklet = await this.getBlocklet(did);
1879
+
1880
+ this.emit(BlockletEvents.installed, { blocklet, context });
1881
+
1882
+ // Update dynamic component meta in blocklet settings
1883
+ await this._ensureDeletedChildrenInSettings(blocklet);
1884
+
1885
+ if (context?.downloadTokenList?.length) {
1886
+ await states.blocklet.updateBlocklet(did, {
1887
+ tokens: {
1888
+ downloadTokenList: context.downloadTokenList,
1889
+ },
1890
+ });
1891
+ }
1892
+
1893
+ if (blocklet.controller && process.env.NODE_ENV !== 'test') {
1894
+ const nodeInfo = await states.node.read();
1895
+ await consumeServerlessNFT({ nftId: blocklet.controller.nftId, nodeInfo, blocklet });
1896
+ }
1897
+
1898
+ if (createNotification) {
1899
+ this._createNotification(did, {
1900
+ title: 'Blocklet Installed',
1901
+ description: `Blocklet ${meta.name}@${meta.version} is installed successfully. (Source: ${
1902
+ deployedFrom || fromBlockletSource(source)
1903
+ })`,
1904
+ action: `/blocklets/${did}/overview`,
1905
+ entityType: 'blocklet',
1906
+ entityId: did,
1907
+ severity: 'success',
1867
1908
  });
1868
- await forEachBlocklet(blocklet, postInstall, { parallel: true });
1869
-
1870
- await this.state.setBlockletStatus(did, BlockletStatus.installed);
1871
- blocklet = await this.ensureBlocklet(did);
1872
- logger.info('blocklet installed', { source, meta });
1873
- this.emit(BlockletEvents.installed, { blocklet, context });
1909
+ }
1874
1910
 
1875
- this.notification.create({
1876
- title: 'Blocklet Installed',
1877
- description: `Blocklet ${meta.name}@${meta.version} is installed successfully. (Source: ${
1878
- deployedFrom || fromBlockletSource(source)
1879
- })`,
1880
- action: `/blocklets/${did}/overview`,
1881
- entityType: 'blocklet',
1882
- entityId: did,
1883
- severity: 'success',
1884
- });
1911
+ await this._rollbackCache.remove({ did: blocklet.meta.did });
1885
1912
  return blocklet;
1886
1913
  } catch (err) {
1887
- const { meta } = await this.state.getBlocklet(did);
1914
+ const { meta } = await states.blocklet.getBlocklet(did);
1888
1915
  const { name, version } = meta;
1889
1916
  logger.error('failed to install blocklet', { name, did, version, error: err });
1890
1917
  try {
1891
- await this._rollback('install', did);
1892
- this.emit(BlockletEvents.installFailed, { meta: { did } });
1893
- this.notification.create({
1918
+ await this._rollback('install', did, oldBlocklet);
1919
+ this.emit(BlockletEvents.installFailed, {
1920
+ meta: { did },
1921
+ error: {
1922
+ message: err.message,
1923
+ },
1924
+ });
1925
+ this._createNotification(did, {
1894
1926
  title: 'Blocklet Install Failed',
1895
1927
  description: `Blocklet ${meta.name}@${meta.version} install failed with error: ${err.message}`,
1896
1928
  entityType: 'blocklet',
@@ -1905,12 +1937,13 @@ class BlockletManager extends BaseBlockletManager {
1905
1937
  }
1906
1938
  }
1907
1939
 
1908
- async _upgradeBlocklet({ newBlocklet, oldBlocklet, context }) {
1940
+ async _upgradeBlocklet({ newBlocklet, oldBlocklet, context = {} }) {
1909
1941
  const { meta, source, deployedFrom, children } = newBlocklet;
1910
1942
  const { did, version, name } = meta;
1911
1943
 
1912
- const oldVersion = oldBlocklet.meta.version;
1913
- const action = semver.gt(oldBlocklet.meta.version, version) ? 'downgrade' : 'upgrade';
1944
+ // ids
1945
+ context.skippedProcessIds = getSkippedProcessIds({ newBlocklet, oldBlocklet, context });
1946
+
1914
1947
  try {
1915
1948
  // delete old process
1916
1949
  try {
@@ -1921,60 +1954,38 @@ class BlockletManager extends BaseBlockletManager {
1921
1954
  }
1922
1955
 
1923
1956
  // update state
1924
- await this.state.upgradeBlocklet({ meta, source, deployedFrom, children });
1925
- await this._setConfigs(did);
1957
+ await states.blocklet.upgradeBlocklet({ meta, source, deployedFrom, children });
1958
+ await this._setConfigsFromMeta(did);
1926
1959
 
1960
+ // should ensure blocklet integrity
1927
1961
  let blocklet = await this.ensureBlocklet(did);
1928
1962
 
1929
1963
  // pre install
1930
- const nodeEnvironments = await this.node.getEnvironments();
1931
- const preInstall = (b) =>
1932
- hooks.preInstall({
1933
- hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
1934
- env: { ...nodeEnvironments },
1935
- appDir: blocklet.env.appDir,
1936
- did, // root blocklet did
1937
- notification: this.notification,
1938
- context,
1939
- });
1940
- await forEachBlocklet(blocklet, preInstall, { parallel: true });
1964
+ await this._runPreInstallHook(blocklet, context);
1941
1965
 
1942
1966
  // Add environments
1943
- await this.updateBlockletEnvironment(did);
1944
- blocklet = await this.ensureBlocklet(did);
1967
+ await this._updateBlockletEnvironment(did);
1968
+ blocklet = await this.getBlocklet(did);
1945
1969
 
1946
1970
  // post install
1947
- const postInstall = (b) =>
1948
- hooks.postInstall({
1949
- hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
1950
- env: getRuntimeEnvironments(b, nodeEnvironments),
1951
- appDir: b.env.appDir,
1952
- did, // root blocklet did
1953
- notification: this.notification,
1954
- context,
1955
- });
1956
- await forEachBlocklet(blocklet, postInstall, { parallel: true });
1971
+ await this._runPostInstallHook(blocklet, context);
1957
1972
 
1958
- // run migrations
1959
- const runMigration = (b) => {
1960
- // BUG: 本身的定义是在执行 upgrade 操作时进行 migration,但是父 blocklet upgrade 时,子 blocklet 可能不需要 upgrade,就会导致子 blocklet 多走了一遍 migration 流程
1961
- if (b.meta.did === did) {
1973
+ logger.info('start migration');
1974
+ try {
1975
+ const oldVersions = {};
1976
+ forEachBlockletSync(oldBlocklet, (b, { id }) => {
1977
+ oldVersions[id] = b.meta.version;
1978
+ });
1979
+ const nodeEnvironments = await states.node.getEnvironments();
1980
+ const runMigration = (b, { id, ancestors }) => {
1962
1981
  return runMigrationScripts({
1963
1982
  blocklet: b,
1964
- oldVersion,
1965
- newVersion: version,
1966
- env: getRuntimeEnvironments(b, nodeEnvironments),
1967
1983
  appDir: b.env.appDir,
1968
- did: b.meta.did,
1969
- notification: this.notification,
1970
- context,
1984
+ env: getRuntimeEnvironments(b, nodeEnvironments, ancestors),
1985
+ oldVersion: oldVersions[id],
1986
+ newVersion: b.meta.version,
1971
1987
  });
1972
- }
1973
- return Promise.resolve();
1974
- };
1975
- logger.info('start migration');
1976
-
1977
- try {
1988
+ };
1978
1989
  await forEachBlocklet(blocklet, runMigration, { parallel: true });
1979
1990
  } catch (error) {
1980
1991
  logger.error('Failed to migrate blocklet', { did, error });
@@ -1984,28 +1995,29 @@ class BlockletManager extends BaseBlockletManager {
1984
1995
 
1985
1996
  logger.info('updated blocklet for upgrading', { did, version, source, name });
1986
1997
 
1998
+ const status =
1999
+ oldBlocklet.status === BlockletStatus.installed ? BlockletStatus.installed : BlockletStatus.stopped;
2000
+ await states.blocklet.setBlockletStatus(did, status, { children: 'all' });
2001
+
1987
2002
  // start new process
1988
2003
  if (oldBlocklet.status === BlockletStatus.running) {
1989
2004
  await this.start({ did }, context);
1990
2005
  logger.info('started blocklet for upgrading', { did, version });
1991
- } else {
1992
- const status =
1993
- oldBlocklet.status === BlockletStatus.installed ? BlockletStatus.installed : BlockletStatus.stopped;
1994
- await this.state.setBlockletStatus(did, status);
1995
2006
  }
1996
2007
 
1997
- blocklet = await this.ensureBlocklet(did, context);
2008
+ blocklet = await this.getBlocklet(did, context);
2009
+
2010
+ await fs.writeFile(path.join(blocklet.env.dataDir, 'logo.svg'), createDidLogo(blocklet.meta.did));
2011
+
2012
+ await this._updateDependents(did);
2013
+
1998
2014
  this.refreshListCache();
1999
2015
 
2000
2016
  try {
2001
- const eventNames = {
2002
- upgrade: BlockletEvents.upgraded,
2003
- downgrade: BlockletEvents.downgraded,
2004
- };
2005
- this.emit(eventNames[action], { blocklet, context });
2006
- this.notification.create({
2007
- title: `Blocklet ${capitalize(action)} Success`,
2008
- description: `Blocklet ${name}@${version} ${action} successfully. (Source: ${
2017
+ this.emit(BlockletEvents.upgraded, { blocklet, context });
2018
+ this._createNotification(did, {
2019
+ title: 'Blocklet Upgrade Success',
2020
+ description: `Blocklet ${name}@${version} upgrade successfully. (Source: ${
2009
2021
  deployedFrom || fromBlockletSource(source)
2010
2022
  })`,
2011
2023
  action: `/blocklets/${did}/overview`,
@@ -2017,14 +2029,26 @@ class BlockletManager extends BaseBlockletManager {
2017
2029
  logger.error('emit upgrade notification failed', { name, version, error });
2018
2030
  }
2019
2031
 
2032
+ // Update dynamic component meta in blocklet settings
2033
+ await this._ensureDeletedChildrenInSettings(blocklet);
2034
+
2035
+ await this._rollbackCache.remove({ did: blocklet.meta.did });
2036
+
2020
2037
  return blocklet;
2021
2038
  } catch (err) {
2022
- const b = await this._rollback(action, did, oldBlocklet);
2023
- logger.error(`failed to ${action} blocklet`, { did, version, name, error: err });
2024
- this.emit(BlockletEvents.updated, { blocklet: b });
2025
- this.notification.create({
2026
- title: `Blocklet ${capitalize(action)} Failed`,
2027
- description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message}`,
2039
+ const b = await this._rollback('upgrade', did, oldBlocklet);
2040
+ logger.error('failed to upgrade blocklet', { did, version, name, error: err });
2041
+
2042
+ this.emit(BlockletEvents.updated, b);
2043
+
2044
+ this.emit(BlockletEvents.upgradeFailed, {
2045
+ blocklet: { ...oldBlocklet, error: { message: err.message } },
2046
+ context,
2047
+ });
2048
+
2049
+ this._createNotification(did, {
2050
+ title: 'Blocklet Upgrade Failed',
2051
+ description: `Blocklet ${name}@${version} upgrade failed with error: ${err.message}`,
2028
2052
  entityType: 'blocklet',
2029
2053
  entityId: did,
2030
2054
  severity: 'error',
@@ -2033,331 +2057,420 @@ class BlockletManager extends BaseBlockletManager {
2033
2057
  }
2034
2058
  }
2035
2059
 
2036
- /**
2037
- * for download: cwd, tarball, did
2038
- * for verify: verify, integrity
2039
- * for cancel control: ctrlStore, rootDid
2040
- */
2041
- async _downloadTarball({ url, cwd, tarball, did, integrity, verify = true, ctrlStore = {}, rootDid }) {
2042
- fs.mkdirSync(cwd, { recursive: true });
2060
+ // Refresh deleted component in blocklet settings
2061
+ async _ensureDeletedChildrenInSettings(blocklet) {
2062
+ const { did } = blocklet.meta;
2043
2063
 
2044
- const tarballName = url.split('/').slice(-1)[0];
2064
+ // TODO 不从 settings 中取值, 直接存在 extra 中
2065
+ let deletedChildren = await states.blockletExtras.getSettings(did, 'children', []);
2066
+ deletedChildren = deletedChildren.filter(
2067
+ (x) => x.status === BlockletStatus.deleted && !blocklet.children.some((y) => y.meta.did === x.meta.did)
2068
+ );
2045
2069
 
2046
- const tarballPath = path.join(cwd, tarballName);
2070
+ await states.blockletExtras.setSettings(did, { children: deletedChildren });
2071
+ }
2047
2072
 
2048
- const { protocol, pathname } = new URL(url);
2073
+ /**
2074
+ *
2075
+ *
2076
+ * @param {{}} blocklet
2077
+ * @param {{}} [context={}]
2078
+ * @return {*}
2079
+ * @memberof BlockletManager
2080
+ */
2081
+ async _downloadBlocklet(blocklet, context = {}) {
2082
+ const {
2083
+ meta: { did },
2084
+ } = blocklet;
2049
2085
 
2050
- const cachedTarFile = await this._getCachedTarFile(integrity);
2051
- if (cachedTarFile) {
2052
- logger.info('found cache tarFile', { did, tarballName, integrity });
2053
- await fs.move(cachedTarFile, tarballPath);
2054
- } else if (protocol.startsWith('file')) {
2055
- await fs.copy(decodeURIComponent(pathname), tarballPath);
2056
- } else {
2057
- const cancelCtrl = new downloadFile.CancelCtrl();
2086
+ return this.blockletDownloader.download(blocklet, {
2087
+ ...context,
2088
+ preDownload: async ({ downloadComponentIds }) => {
2089
+ // update children status
2090
+ const blocklet1 = await states.blocklet.setBlockletStatus(did, BlockletStatus.downloading, {
2091
+ children: downloadComponentIds,
2092
+ });
2093
+ this.emit(BlockletEvents.statusChange, blocklet1);
2094
+ },
2095
+ postDownload: async ({ isCancelled }) => {
2096
+ if (!isCancelled) {
2097
+ // since preferences only exist in blocklet bundle, we need to populate then after downloaded
2098
+ await this._setConfigsFromMeta(did);
2099
+ }
2100
+ },
2101
+ });
2102
+ }
2058
2103
 
2059
- if (!ctrlStore[rootDid]) {
2060
- ctrlStore[rootDid] = new Map();
2104
+ async _syncPm2Status(pm2Status, did) {
2105
+ try {
2106
+ const state = await states.blocklet.getBlocklet(did);
2107
+ if (state && util.shouldUpdateBlockletStatus(state.status)) {
2108
+ const newStatus = pm2StatusMap[pm2Status];
2109
+ await states.blocklet.setBlockletStatus(did, newStatus);
2110
+ logger.info('sync pm2 status to blocklet', { did, pm2Status, newStatus });
2061
2111
  }
2062
- ctrlStore[rootDid].set(did, cancelCtrl);
2112
+ } catch (error) {
2113
+ logger.error('sync pm2 status to blocklet failed', { did, pm2Status, error });
2114
+ }
2115
+ }
2063
2116
 
2064
- await downloadFile(url, path.join(cwd, tarballName), { cancelCtrl });
2117
+ /**
2118
+ * @param {string} action install, upgrade, downgrade
2119
+ * @param {string} did
2120
+ * @param {object} oldBlocklet
2121
+ */
2122
+ async _rollback(action, did, oldBlocklet) {
2123
+ if (action === 'install') {
2124
+ const extraState = oldBlocklet?.extraState;
2065
2125
 
2066
- if (ctrlStore[rootDid]) {
2067
- ctrlStore[rootDid].delete(did);
2068
- if (!ctrlStore[rootDid].size) {
2069
- delete ctrlStore[rootDid];
2070
- }
2126
+ // rollback blocklet extra state
2127
+ if (extraState) {
2128
+ await states.blockletExtras.update({ did }, extraState);
2129
+ } else {
2130
+ await states.blockletExtras.remove({ did });
2071
2131
  }
2072
2132
 
2073
- if (cancelCtrl.isCancelled) {
2074
- return downloadFile.CANCEL;
2075
- }
2133
+ // remove blocklet state
2134
+ return this._deleteBlocklet({ did, keepData: true });
2076
2135
  }
2077
2136
 
2078
- if (verify) {
2079
- try {
2080
- await verifyIntegrity({ file: tarballPath, integrity });
2081
- } catch (error) {
2082
- logger.error('verify integrity error', { error, tarball, url });
2083
- throw new Error(`${tarball} integrity check failed.`);
2084
- }
2137
+ if (['upgrade', 'downgrade'].includes(action)) {
2138
+ const { extraState, ...blocklet } = oldBlocklet;
2139
+ // rollback blocklet state
2140
+ const result = await states.blocklet.updateBlocklet(did, blocklet);
2141
+
2142
+ // rollback blocklet extra state
2143
+ await states.blockletExtras.update({ did: blocklet.meta.did }, extraState);
2144
+
2145
+ logger.info('blocklet rollback successfully', { did });
2146
+ this.emit(BlockletEvents.updated, result);
2147
+ return result;
2085
2148
  }
2086
2149
 
2087
- return tarballPath;
2150
+ logger.error('rollback action is invalid', { action });
2151
+ throw new Error(`rollback action is invalid: ${action}`);
2088
2152
  }
2089
2153
 
2090
- /**
2091
- * use LRU algorithm
2092
- */
2093
- async _addCacheTarFile(tarballPath, integrity) {
2094
- // eslint-disable-next-line no-param-reassign
2095
- integrity = toBase58(integrity);
2096
-
2097
- // move tarball to cache dir
2098
- const cwd = path.join(this.dataDirs.tmp, 'download-cache');
2099
- const cachePath = path.join(cwd, `${integrity}.tar.gz`);
2100
- await fs.ensureDir(cwd);
2101
- await fs.move(tarballPath, cachePath, { overwrite: true });
2102
-
2103
- const key = 'blocklet:manager:downloadCache';
2104
- const cacheList = (await this.cache.get(key)) || [];
2105
- const exist = cacheList.find((x) => x.integrity === integrity);
2106
-
2107
- // update
2108
- if (exist) {
2109
- logger.info('update cache tarFile', { base58: integrity });
2110
- exist.accessAt = Date.now();
2111
- await this.cache.set(key, cacheList);
2112
- return;
2154
+ async _deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context) {
2155
+ const blocklet = await states.blocklet.getBlocklet(did);
2156
+ const { name } = blocklet.meta;
2157
+ const cacheDir = path.join(this.dataDirs.cache, name);
2158
+
2159
+ // Cleanup db
2160
+ await this.teamManager.deleteTeam(blocklet.meta.did);
2161
+
2162
+ // Cleanup disk storage
2163
+ fs.removeSync(cacheDir);
2164
+ await this._cleanBlockletData({ blocklet, keepData, keepLogsDir, keepConfigs });
2165
+
2166
+ if (blocklet.mode !== BLOCKLET_MODES.DEVELOPMENT) {
2167
+ const nodeInfo = await states.node.read();
2168
+ const { wallet } = getBlockletInfo(blocklet, nodeInfo.sk);
2169
+ didDocument
2170
+ .disableDNS({ wallet, didRegistryUrl: nodeInfo.didRegistry })
2171
+ .then(() => {
2172
+ logger.info(`disabled blocklet ${blocklet.appDid} dns`);
2173
+ })
2174
+ .catch((err) => {
2175
+ logger.error(`disable blocklet ${blocklet.appDid} dns failed`, { error: err });
2176
+ });
2113
2177
  }
2114
2178
 
2115
- // add
2116
- cacheList.push({ integrity, accessAt: Date.now() });
2117
- if (cacheList.length > 10) {
2118
- // find and remove
2119
- let minIndex = 0;
2120
- let min = cacheList[0];
2121
- cacheList.forEach((x, i) => {
2122
- if (x.accessAt < min.accessAt) {
2123
- minIndex = i;
2124
- min = x;
2125
- }
2126
- });
2179
+ const result = await states.blocklet.deleteBlocklet(did);
2180
+ logger.info('blocklet removed successfully', { did });
2127
2181
 
2128
- cacheList.splice(minIndex, 1);
2129
- await fs.remove(path.join(cwd, `${min.integrity}.tar.gz`));
2130
- logger.info('remove cache tarFile', { base58: min.integrity });
2182
+ let keepRouting = true;
2183
+ if (keepData === false || keepConfigs === false) {
2184
+ keepRouting = false;
2131
2185
  }
2132
- logger.info('add cache tarFile', { base58: integrity });
2133
2186
 
2134
- // update
2135
- await this.cache.set(key, cacheList);
2136
- }
2187
+ this.runtimeMonitor.delete(blocklet.meta.did);
2137
2188
 
2138
- async _getCachedTarFile(integrity) {
2139
- // eslint-disable-next-line no-param-reassign
2140
- integrity = toBase58(integrity);
2189
+ this.emit(BlockletEvents.removed, {
2190
+ blocklet: result,
2191
+ context: {
2192
+ ...context,
2193
+ keepRouting,
2194
+ },
2195
+ });
2196
+ return blocklet;
2197
+ }
2141
2198
 
2142
- const cwd = path.join(this.dataDirs.tmp, 'download-cache');
2143
- const cachePath = path.join(cwd, `${integrity}.tar.gz`);
2199
+ async _cleanBlockletData({ blocklet, keepData, keepLogsDir, keepConfigs }) {
2200
+ const { name } = blocklet.meta;
2144
2201
 
2145
- if (fs.existsSync(cachePath)) {
2146
- return cachePath;
2147
- }
2202
+ const dataDir = path.join(this.dataDirs.data, name);
2203
+ const logsDir = path.join(this.dataDirs.logs, name);
2148
2204
 
2149
- return null;
2150
- }
2205
+ logger.info(`clean blocklet ${blocklet.meta.did} data`, { keepData, keepLogsDir, keepConfigs });
2151
2206
 
2152
- /**
2153
- * download bundle, verify bundle, resolve bundle to installDir
2154
- * @param {object} meta
2155
- * @param {string} rootDid root blocklet did of the blocklet to be downloaded
2156
- * @return {object} { isCancelled: Boolean }
2157
- */
2158
- async _downloadBundle(meta, rootDid, url) {
2159
- const { name, did, version, dist = {} } = meta;
2160
- const { tarball, integrity } = dist;
2207
+ if (keepData === false) {
2208
+ fs.removeSync(dataDir);
2209
+ logger.info(`removed blocklet ${blocklet.meta.did} data dir: ${dataDir}`);
2161
2210
 
2162
- const lockName = `download-${did}-${version}`;
2163
- let lock = this.downloadLocks[lockName];
2164
- if (!lock) {
2165
- lock = new Lock(lockName);
2166
- this.downloadLocks[lockName] = lock;
2167
- }
2211
+ fs.removeSync(logsDir);
2212
+ logger.info(`removed blocklet ${blocklet.meta.did} logs dir: ${logsDir}`);
2168
2213
 
2169
- try {
2170
- await lock.acquire();
2171
- logger.info('downloaded blocklet for installing', { name, version, tarball, integrity });
2172
- const cwd = path.join(this.dataDirs.tmp, 'download', name);
2173
- await fs.ensureDir(cwd);
2174
- logger.info('start download blocklet', { name, version, cwd, tarball, integrity });
2175
- const tarballPath = await this._downloadTarball({
2176
- name,
2177
- did,
2178
- version,
2179
- cwd,
2180
- tarball,
2181
- integrity,
2182
- verify: true,
2183
- ctrlStore: this.downloadCtrls,
2184
- rootDid,
2185
- url,
2186
- });
2187
- logger.info('downloaded blocklet tar file', { name, version, tarballPath });
2188
- if (tarballPath === downloadFile.CANCEL) {
2189
- lock.release();
2190
- return { isCancelled: true };
2214
+ await states.blockletExtras.remove({ did: blocklet.meta.did });
2215
+ logger.info(`removed blocklet ${blocklet.meta.did} extra data`);
2216
+ } else {
2217
+ if (keepLogsDir === false) {
2218
+ fs.removeSync(logsDir);
2219
+ logger.info(`removed blocklet ${blocklet.meta.did} logs dir: ${logsDir}`);
2191
2220
  }
2192
2221
 
2193
- // resolve tarball and mv tarball to cache after resolved
2194
- await this._resolveDownload(cwd, tarballPath, null, { removeTarFile: false });
2195
- await this._addCacheTarFile(tarballPath, integrity);
2196
-
2197
- logger.info('resolved blocklet tar file to install dir', { name, version });
2198
- lock.release();
2199
- return { isCancelled: false };
2200
- } catch (error) {
2201
- lock.release();
2202
- throw error;
2222
+ if (keepConfigs === false) {
2223
+ await states.blockletExtras.remove({ did: blocklet.meta.did });
2224
+ logger.info(`removed blocklet ${blocklet.meta.did} extra data`);
2225
+ }
2203
2226
  }
2204
2227
  }
2205
2228
 
2206
- async _downloadBlocklet(blocklet, oldBlocklet = {}) {
2207
- const {
2208
- meta: { name, did },
2209
- } = blocklet;
2229
+ async _setConfigsFromMeta(did, childDid) {
2230
+ const blocklet = await getBlocklet({ states, dataDirs: this.dataDirs, did });
2210
2231
 
2211
- const metas = [];
2212
- if (
2213
- ![BlockletSource.upload, BlockletSource.local].includes(blocklet.source) &&
2214
- get(oldBlocklet, 'meta.dist.integrity') !== get(blocklet, 'meta.dist.integrity')
2215
- ) {
2216
- metas.push(blocklet.meta);
2217
- }
2232
+ if (!childDid) {
2233
+ await forEachBlocklet(blocklet, async (b, { ancestors }) => {
2234
+ const environments = [...get(b.meta, 'environments', []), ...getConfigFromPreferences(b)];
2218
2235
 
2219
- const oldChildren = (oldBlocklet.children || []).reduce((o, x) => {
2220
- o[x.meta.did] = x;
2221
- return o;
2222
- }, {});
2236
+ // remove default if ancestors has a value
2237
+ ensureEnvDefault(environments, ancestors);
2223
2238
 
2224
- for (const child of blocklet.children) {
2225
- const oldChild = oldChildren[child.meta.did];
2226
- if (!oldChild || oldChild.meta.dist.integrity !== child.meta.dist.integrity) {
2227
- metas.push(child.meta);
2228
- }
2239
+ // write configs to db
2240
+ await states.blockletExtras.setConfigs([...ancestors.map((x) => x.meta.did), b.meta.did], environments);
2241
+ });
2242
+ } else {
2243
+ const child = blocklet.children.find((x) => x.meta.did === childDid);
2244
+ await forEachBlocklet(child, async (b, { ancestors }) => {
2245
+ await states.blockletExtras.setConfigs(
2246
+ [blocklet.meta.did, ...ancestors.map((x) => x.meta.did), b.meta.did],
2247
+ [...get(b.meta, 'environments', []), ...getConfigFromPreferences(child)]
2248
+ );
2249
+ });
2229
2250
  }
2251
+ }
2230
2252
 
2231
- try {
2232
- const tasks = [];
2233
- for (const meta of metas) {
2234
- const url = await this.registry.resolveTarballURL({
2253
+ // to be deleted
2254
+ async _setAppSk(did, appSk, context) {
2255
+ if (process.env.NODE_ENV === 'production' && !appSk) {
2256
+ throw new Error(`appSk for blocklet ${did} is required`);
2257
+ }
2258
+
2259
+ if (appSk) {
2260
+ await this.config(
2261
+ {
2235
2262
  did,
2236
- tarball: get(meta, 'dist.tarball'),
2237
- registryUrl: blocklet.source === BlockletSource.registry ? blocklet.deployedFrom : undefined,
2238
- });
2239
- tasks.push(this._downloadBundle(meta, did, url));
2240
- }
2241
- const results = await Promise.all(tasks);
2242
- if (results.find((x) => x.isCancelled)) {
2243
- return { isCancelled: true };
2244
- }
2245
- } catch (error) {
2246
- logger.error('Download blocklet failed', { did, name, error });
2247
- await this._cancelDownload(blocklet.meta);
2248
- throw error;
2263
+ configs: [{ key: 'BLOCKLET_APP_SK', value: appSk, secure: true }],
2264
+ skipHook: true,
2265
+ skipDidDocument: true,
2266
+ },
2267
+ context
2268
+ );
2249
2269
  }
2270
+ }
2271
+
2272
+ async _getBlockletForInstallation(did) {
2273
+ const blocklet = await states.blocklet.getBlocklet(did, { decryptSk: false });
2274
+ if (!blocklet) {
2275
+ return null;
2276
+ }
2277
+
2278
+ const extraState = await states.blockletExtras.findOne({ did: blocklet.meta.did });
2279
+ blocklet.extraState = extraState;
2250
2280
 
2251
- return { isCancelled: false };
2281
+ return blocklet;
2252
2282
  }
2253
2283
 
2254
- async _syncPm2Status(pm2Status, did) {
2284
+ async _runPreInstallHook(blocklet, context) {
2285
+ const nodeEnvironments = await states.node.getEnvironments();
2286
+
2287
+ const preInstall = (b) =>
2288
+ hooks.preInstall(b.env.processId, {
2289
+ hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
2290
+ env: { ...nodeEnvironments },
2291
+ appDir: b.env.appDir,
2292
+ did: blocklet.meta.did, // root blocklet did
2293
+ notification: states.notification,
2294
+ context,
2295
+ });
2296
+
2297
+ await forEachBlocklet(blocklet, preInstall, { parallel: true });
2298
+ }
2299
+
2300
+ async _runPostInstallHook(blocklet, context) {
2301
+ const nodeEnvironments = await states.node.getEnvironments();
2302
+
2303
+ const postInstall = (b, { ancestors }) =>
2304
+ hooks.postInstall(b.env.processId, {
2305
+ hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
2306
+ env: getRuntimeEnvironments(b, nodeEnvironments, ancestors),
2307
+ appDir: b.env.appDir,
2308
+ did: blocklet.meta.did, // root blocklet did
2309
+ notification: states.notification,
2310
+ context,
2311
+ });
2312
+
2313
+ await forEachBlocklet(blocklet, postInstall, { parallel: true });
2314
+ }
2315
+
2316
+ async _createNotification(did, notification) {
2255
2317
  try {
2256
- const state = await this.state.getBlocklet(did);
2257
- if (state && util.shouldUpdateBlockletStatus(state.status)) {
2258
- const result = await this.state.setBlockletStatus(did, pm2StatusMap[pm2Status]);
2259
- logger.info('sync pm2 status to blocklet', { result });
2318
+ const extra = await states.blockletExtras.getMeta(did);
2319
+ const isExternal = !!extra?.controller;
2320
+
2321
+ if (isExternal) {
2322
+ return;
2260
2323
  }
2324
+
2325
+ await states.notification.create(notification);
2261
2326
  } catch (error) {
2262
- logger.error('sync pm2 status to blocklet failed', { did, pm2Status, error });
2327
+ logger.error('create notification failed', { error });
2263
2328
  }
2264
2329
  }
2265
2330
 
2266
- // eslint-disable-next-line no-unused-vars
2267
- async _cancelDownload(blockletMeta, context) {
2268
- const { did, name, version } = blockletMeta;
2331
+ async _deleteExpiredExternalBlocklet() {
2332
+ try {
2333
+ logger.info('start check expired external blocklet');
2334
+ const blockletExtras = await states.blockletExtras.find(
2335
+ {
2336
+ controller: {
2337
+ $exists: true,
2338
+ },
2339
+ 'controller.expiredAt': {
2340
+ $exists: false,
2341
+ },
2342
+ },
2343
+ { did: 1, meta: 1, controller: 1 }
2344
+ );
2345
+
2346
+ for (const data of blockletExtras) {
2347
+ try {
2348
+ const assetState = await util.getNFTState(data.controller.chainHost, data.controller.nftId);
2349
+ const isExpired = isNFTExpired(assetState);
2269
2350
 
2270
- if (this.downloadCtrls[did]) {
2271
- for (const cancelCtrl of this.downloadCtrls[did].values()) {
2272
- cancelCtrl.cancel();
2351
+ if (isExpired) {
2352
+ logger.info('the blocklet already expired', {
2353
+ blockletDid: data.meta.did,
2354
+ nftId: data.controller.nftId,
2355
+ });
2356
+
2357
+ await this.delete({ did: data.meta.did, keepData: true, keepConfigs: true, keepLogsDir: true });
2358
+ logger.info('the expired blocklet already deleted', {
2359
+ blockletDid: data.meta.did,
2360
+ nftId: data.controller.nftId,
2361
+ });
2362
+
2363
+ const expiredAt = getNftExpirationDate(assetState);
2364
+ await states.blockletExtras.updateExpireInfo({ did: data.meta.did, expiredAt });
2365
+ logger.info('updated expired blocklet extra info', {
2366
+ nftId: data.controller.nftId,
2367
+ blockletDid: data.meta.did,
2368
+ });
2369
+
2370
+ // 删除 blocklet 后会 reload nginx, 所以这里每次删除一个
2371
+ if (process.env.NODE_ENV !== 'test') {
2372
+ await sleep(10 * 1000);
2373
+ }
2374
+ }
2375
+ } catch (error) {
2376
+ logger.error('delete expired blocklet failed', {
2377
+ blockletDid: data.meta.did,
2378
+ nftId: data.controller.nftId,
2379
+ error,
2380
+ });
2381
+ }
2273
2382
  }
2274
- logger.info('cancel download blocklet', { did, name, version });
2383
+
2384
+ logger.info('check expired external blocklet end');
2385
+ } catch (error) {
2386
+ logger.info('check expired external blocklet failed', { error });
2275
2387
  }
2276
2388
  }
2277
2389
 
2278
- // eslint-disable-next-line no-unused-vars
2279
- async _cancelWaiting(blockletMeta, context) {
2280
- const { did, name, version } = blockletMeta;
2390
+ async _cleanExpiredBlockletData() {
2391
+ try {
2392
+ logger.info('start clean expired blocklet data');
2393
+ const blockletExtras = await states.blockletExtras.getExpiredList();
2394
+ if (blockletExtras.length === 0) {
2395
+ logger.info('no expired blocklet data');
2396
+ return;
2397
+ }
2281
2398
 
2282
- const {
2283
- job: { postAction, oldBlocklet },
2284
- } = await this.installQueue.cancel(did);
2285
- await this._rollback(postAction, did, oldBlocklet);
2399
+ const tasks = blockletExtras.map(async ({ did }) => {
2400
+ const blocklet = await states.blocklet.getBlocklet(did);
2401
+ await this._cleanBlockletData({ blocklet, keepData: false, keepLogsDir: false, keepConfigs: false });
2286
2402
 
2287
- logger.info('cancel waiting blocklet', { did, name, version });
2288
- }
2403
+ this.emit(BlockletEvents.dataCleaned, {
2404
+ blocklet,
2405
+ keepRouting: false,
2406
+ });
2289
2407
 
2290
- /**
2291
- * @param {string} action install, upgrade, downgrade
2292
- * @param {string} did
2293
- * @param {object} oldBlocklet
2294
- */
2295
- async _rollback(action, did, oldBlocklet) {
2296
- if (action === 'install') {
2297
- // remove blocklet
2298
- return this._deleteBlocklet({ did, keepData: false });
2299
- }
2408
+ logger.info(`cleaned expired blocklet blocklet ${did} data`);
2409
+ });
2300
2410
 
2301
- if (['upgrade', 'downgrade'].includes(action)) {
2302
- // rollback blocklet
2303
- const { _id } = await this.state.getBlocklet(did);
2304
- const result = await this.state.updateById(_id, { $set: oldBlocklet });
2305
- await this._setConfigs(did);
2306
- logger.info('blocklet rollback successfully', { did });
2307
- this.emit(BlockletEvents.updated, { blocklet: result });
2308
- return result;
2309
- }
2411
+ await Promise.all(tasks);
2310
2412
 
2311
- logger.error('rollback action is invalid', { action });
2312
- throw new Error(`rollback action is invalid: ${action}`);
2413
+ logger.info('clean expired blocklet data done');
2414
+ } catch (error) {
2415
+ logger.error('clean expired blocklet data failed', { error });
2416
+ }
2313
2417
  }
2314
2418
 
2315
- async _deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context) {
2316
- const blocklet = await this.state.getBlocklet(did);
2317
- const { name } = blocklet.meta;
2318
- const dataDir = path.join(this.dataDirs.data, name);
2319
- const logsDir = path.join(this.dataDirs.logs, name);
2320
- const cacheDir = path.join(this.dataDirs.cache, name);
2419
+ async _updateDidDocument(blocklet) {
2420
+ const nodeInfo = await states.node.read();
2321
2421
 
2322
- // Cleanup disk storage
2323
- fs.removeSync(cacheDir);
2324
- if (keepData === false) {
2325
- fs.removeSync(dataDir);
2326
- fs.removeSync(logsDir);
2327
- await this._delExtras(did);
2328
- } else {
2329
- if (keepLogsDir === false) {
2330
- fs.removeSync(logsDir);
2331
- }
2422
+ const { wallet } = getBlockletInfo(
2423
+ {
2424
+ meta: blocklet.meta,
2425
+ environments: [BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK, BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_WALLET_TYPE]
2426
+ .map((key) => ({ key, value: blocklet.configObj[key] }))
2427
+ .filter((x) => x.value),
2428
+ },
2429
+ nodeInfo.sk
2430
+ );
2431
+ const didDomain = getDidDomainForBlocklet({ appPid: blocklet.appPid, didDomain: nodeInfo.didDomain });
2332
2432
 
2333
- if (keepConfigs === false) {
2334
- await this._delExtras(did);
2335
- }
2336
- }
2433
+ const domainAliases = (get(blocklet, 'site.domainAliases') || []).filter(
2434
+ (item) => !item.value.endsWith(nodeInfo.didDomain) && !item.value.endsWith('did.staging.arcblock.io') // did.staging.arcblock.io 是旧 did domain, 但主要存在于比较旧的节点中, 需要做兼容
2435
+ );
2337
2436
 
2338
- const result = await this.state.deleteBlocklet(did);
2339
- logger.info('blocklet removed successfully', { did });
2437
+ domainAliases.push({ value: didDomain, isProtected: true });
2340
2438
 
2341
- this.emit(BlockletEvents.removed, { blocklet: result, context });
2342
- return blocklet;
2343
- }
2439
+ // 先更新 routing rule db 中的 domain aliases, 这一步的目的是为了后面用
2440
+ await states.site.updateDomainAliasList(blocklet.site.id, domainAliases);
2344
2441
 
2345
- async _setConfigs(did) {
2346
- const blocklet = await this.state.getBlocklet(did);
2347
- const { meta } = blocklet;
2348
- await this.extras.setConfigs(did, get(meta, 'environments', []));
2349
- for (const child of blocklet.children) {
2350
- await this.extras.setChildConfigs(did, child.meta.did, get(child.meta, 'environments'));
2351
- }
2442
+ this.emit(BlockletEvents.appDidChanged, blocklet);
2443
+
2444
+ await didDocument.updateBlockletDocument({
2445
+ wallet,
2446
+ appPid: blocklet.appPid,
2447
+ alsoKnownAs: getBlockletKnownAs(blocklet),
2448
+ daemonDidDomain: util.getServerDidDomain(nodeInfo),
2449
+ didRegistryUrl: nodeInfo.didRegistry,
2450
+ domain: nodeInfo.didDomain,
2451
+ });
2452
+ logger.info('updated blocklet dns document', { appPid: blocklet.appPid, appDid: blocklet.appDid });
2352
2453
  }
2353
2454
 
2354
- async _delExtras(did) {
2355
- const blocklet = await this.state.getBlocklet(did);
2356
- await this.extras.delConfigs(did);
2357
- await this.extras.delSettings(did);
2455
+ async _updateDependents(did) {
2456
+ const blocklet = await states.blocklet.getBlocklet(did);
2457
+ const map = {};
2358
2458
  for (const child of blocklet.children) {
2359
- await this.extras.delChildConfigs(did, child.meta.did);
2459
+ child.dependents = [];
2460
+ map[child.meta.did] = child;
2360
2461
  }
2462
+
2463
+ forEachBlockletSync(blocklet, (x, { id }) => {
2464
+ if (x.dependencies) {
2465
+ x.dependencies.forEach((y) => {
2466
+ if (map[y.did]) {
2467
+ map[y.did].dependents.push({ id, required: y.required });
2468
+ }
2469
+ });
2470
+ }
2471
+ });
2472
+
2473
+ await states.blocklet.updateBlocklet(blocklet.meta.did, { children: blocklet.children });
2361
2474
  }
2362
2475
  }
2363
2476