@abtnode/core 1.15.17 → 1.16.0-beta-8ee536d7

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