@blocklet/cli 1.16.33 → 1.16.34-beta-20241120-080738-bbbe036c

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 (87) hide show
  1. package/README.md +32 -25
  2. package/bin/blocklet.js +292 -1
  3. package/config.example.yml +33 -0
  4. package/lib/arcblock.js +53 -0
  5. package/lib/commands/blocklet/add.js +124 -0
  6. package/lib/commands/blocklet/assets/git-ignore +28 -0
  7. package/lib/commands/blocklet/assets/index.html +9 -0
  8. package/lib/commands/blocklet/assets/index.js +14 -0
  9. package/lib/commands/blocklet/assets/logo.png +0 -0
  10. package/lib/commands/blocklet/bundle/bundle.js +184 -0
  11. package/lib/commands/blocklet/bundle/bundlers/blocklet.js +138 -0
  12. package/lib/commands/blocklet/bundle/bundlers/changelog.js +100 -0
  13. package/lib/commands/blocklet/bundle/bundlers/logo.js +56 -0
  14. package/lib/commands/blocklet/bundle/bundlers/markdown.js +241 -0
  15. package/lib/commands/blocklet/bundle/bundlers/preference.js +50 -0
  16. package/lib/commands/blocklet/bundle/bundlers/readme.js +43 -0
  17. package/lib/commands/blocklet/bundle/bundlers/screenshots.js +94 -0
  18. package/lib/commands/blocklet/bundle/bundlers/simple.js +70 -0
  19. package/lib/commands/blocklet/bundle/compact/bundle-compact-file.js +48 -0
  20. package/lib/commands/blocklet/bundle/compact/bundle-merge-extra.js +66 -0
  21. package/lib/commands/blocklet/bundle/compact/default-external.js +5 -0
  22. package/lib/commands/blocklet/bundle/compact/index.js +88 -0
  23. package/lib/commands/blocklet/bundle/index.js +139 -0
  24. package/lib/commands/blocklet/bundle/pack.js +8 -0
  25. package/lib/commands/blocklet/bundle/parse-external-dependencies.js +97 -0
  26. package/lib/commands/blocklet/bundle/simple/index.js +62 -0
  27. package/lib/commands/blocklet/bundle/zip/archive.js +35 -0
  28. package/lib/commands/blocklet/bundle/zip/dependencies.js +333 -0
  29. package/lib/commands/blocklet/bundle/zip/index.js +165 -0
  30. package/lib/commands/blocklet/bundle/zip/main.js +124 -0
  31. package/lib/commands/blocklet/bundle/zip/node.js +59 -0
  32. package/lib/commands/blocklet/bundle/zip/resolve.js +93 -0
  33. package/lib/commands/blocklet/cleanup.js +52 -0
  34. package/lib/commands/blocklet/config.js +108 -0
  35. package/lib/commands/blocklet/connect.js +87 -0
  36. package/lib/commands/blocklet/create.js +38 -0
  37. package/lib/commands/blocklet/deploy.js +435 -0
  38. package/lib/commands/blocklet/dev.js +1000 -0
  39. package/lib/commands/blocklet/document.js +39 -0
  40. package/lib/commands/blocklet/exec.js +106 -0
  41. package/lib/commands/blocklet/init.js +300 -0
  42. package/lib/commands/blocklet/meta.js +22 -0
  43. package/lib/commands/blocklet/remove.js +35 -0
  44. package/lib/commands/blocklet/test.js +201 -0
  45. package/lib/commands/blocklet/upload.js +105 -0
  46. package/lib/commands/blocklet/version.js +81 -0
  47. package/lib/commands/server/cleanup.js +32 -0
  48. package/lib/commands/server/command.js +131 -0
  49. package/lib/commands/server/info.js +92 -0
  50. package/lib/commands/server/init.js +433 -0
  51. package/lib/commands/server/logs.js +99 -0
  52. package/lib/commands/server/rescue.js +71 -0
  53. package/lib/commands/server/start.js +821 -0
  54. package/lib/commands/server/status.js +107 -0
  55. package/lib/commands/server/stop.js +163 -0
  56. package/lib/commands/server/upgrade.js +123 -0
  57. package/lib/constant.js +21 -2
  58. package/lib/debug.js +20 -0
  59. package/lib/manager/config.js +122 -0
  60. package/lib/manager/deploy.js +75 -0
  61. package/lib/manager/index.js +23 -0
  62. package/lib/manager/process.js +47 -0
  63. package/lib/node.js +214 -0
  64. package/lib/port.js +19 -0
  65. package/lib/postinstall.js +3 -0
  66. package/lib/process/daemon.js +196 -0
  67. package/lib/process/service.js +86 -0
  68. package/lib/ui.js +137 -0
  69. package/lib/util/blocklet/config.js +78 -0
  70. package/lib/util/blocklet/env.js +172 -0
  71. package/lib/util/blocklet/meta.js +36 -0
  72. package/lib/util/blocklet/payment.js +88 -0
  73. package/lib/util/blocklet/sign.js +21 -0
  74. package/lib/util/blocklet/tar.js +119 -0
  75. package/lib/util/convert-to-nosources-sourcemap.js +37 -0
  76. package/lib/util/docker-status-log.js +17 -0
  77. package/lib/util/exit-when-server-stopped.js +44 -0
  78. package/lib/util/get-cli-binary-name.js +8 -0
  79. package/lib/util/get-download-bundle-step.js +36 -0
  80. package/lib/util/get-service-instance-number.js +12 -0
  81. package/lib/util/index.js +626 -0
  82. package/lib/util/print-error.js +11 -0
  83. package/lib/util/print.js +9 -0
  84. package/lib/util/what-uri.js +40 -0
  85. package/package.json +123 -27
  86. package/lib/run.d.ts +0 -2
  87. package/lib/run.js +0 -73
@@ -0,0 +1,821 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ /* eslint-disable prefer-destructuring */
3
+ const chalk = require('chalk');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+ const Table = require('cli-table3');
7
+ const inquirer = require('inquirer');
8
+ const shelljs = require('shelljs');
9
+ const omit = require('lodash/omit');
10
+ const get = require('lodash/get');
11
+ const isEqual = require('lodash/isEqual');
12
+ const isObject = require('lodash/isObject');
13
+ const readLastLines = require('read-last-lines');
14
+ const pm2 = require('@abtnode/util/lib/async-pm2');
15
+ const { isInServerlessMode } = require('@abtnode/util/lib/serverless');
16
+ const ensureEndpointHealthy = require('@abtnode/util/lib/ensure-endpoint-healthy');
17
+ const { runMigrationScripts, runSchemaMigrations } = require('@abtnode/core/lib/migrations');
18
+ const { clearRouterByConfigKeyword } = require('@abtnode/router-provider');
19
+ const yaml = require('js-yaml');
20
+ const semver = require('semver');
21
+ const { BlockletEvents } = require('@blocklet/constant');
22
+ const {
23
+ PROCESS_NAME_PROXY,
24
+ PROCESS_NAME_DAEMON,
25
+ PROCESS_NAME_UPDATER,
26
+ PROCESS_NAME_SERVICE,
27
+ PROCESS_NAME_LOG_ROTATE,
28
+ CONFIG_FOLDER_NAME,
29
+ CONFIG_FOLDER_NAME_OLD,
30
+ MAX_UPLOAD_FILE_SIZE,
31
+ DAEMON_MAX_MEM_LIMIT_IN_MB,
32
+ PROXY_MAX_MEM_LIMIT_IN_MB,
33
+ BLOCKLET_MAX_MEM_LIMIT_IN_MB,
34
+ EVENTS,
35
+ NODE_MODES,
36
+ MAX_NGINX_WORKER_CONNECTIONS,
37
+ SERVER_STATUS,
38
+ } = require('@abtnode/constant');
39
+
40
+ const { canUseFileSystemIsolateApi, SAFE_NODE_VERSION } = require('@abtnode/util/lib/security');
41
+
42
+ const { version } = require('../../../package.json');
43
+ const debug = require('../../debug')('start');
44
+ const util = require('../../util');
45
+ const getServiceInstanceNumber = require('../../util/get-service-instance-number');
46
+ const {
47
+ getConfigFileFromAncestors,
48
+ getRunningConfigFile,
49
+ getDaemonLogDir,
50
+ checkRunning,
51
+ getBaseConfigDataDirectory,
52
+ getDataDirectoryByConfigFile,
53
+ getConfigDirFromCurrentDir,
54
+ getConfigFile,
55
+ } = require('../../manager');
56
+ const { wrapSpinner } = require('../../ui');
57
+ const nodeLib = require('../../node');
58
+ const { getInternalPort: getPort } = require('../../port');
59
+ const { printInvalidModeInfo, isValidMode, prettyAvailableModes } = require('../../util');
60
+ const getDockerStatusLog = require('../../util/docker-status-log');
61
+
62
+ const {
63
+ print,
64
+ printError,
65
+ printInfo,
66
+ printWarning,
67
+ printSuccess,
68
+ getCLIBinaryName,
69
+ getCLICommandName,
70
+ printAccessUrls,
71
+ checkRoutingProvider,
72
+ getCliCwd,
73
+ startEventHub,
74
+ cleanupProcessByName,
75
+ } = util;
76
+
77
+ const ABT_NODE_BINARY_NAME = getCLIBinaryName();
78
+ const ABT_NODE_COMMAND_NAME = getCLICommandName();
79
+
80
+ const updateVersionInConfigFileAndState = async (config, configFile, node) => {
81
+ if (semver.neq(config.node.version, version)) {
82
+ const latest = yaml.load(fs.readFileSync(configFile).toString(), { json: true });
83
+
84
+ const headers = latest.node.routing.headers;
85
+ if (headers && headers['X-Powered-By'] && typeof headers['X-Powered-By'] === 'string') {
86
+ headers['X-Powered-By'] = `"Blocklet Server/${version}"`;
87
+ }
88
+
89
+ latest.node.version = version;
90
+ fs.writeFileSync(configFile, yaml.dump(latest));
91
+ printSuccess(`Blocklet Server config updated with version ${chalk.cyan(version)}`);
92
+
93
+ try {
94
+ const info = await node.getNodeInfo();
95
+
96
+ const data = {
97
+ version,
98
+ routing: Object.assign({}, info.routing || {}, {
99
+ headers,
100
+ }),
101
+ };
102
+
103
+ await node.states.node.updateNodeInfo(data);
104
+ printSuccess(`Blocklet Server state updated with version ${chalk.cyan(version)}`);
105
+ } catch (err) {
106
+ printError(`Failed to update node state version: ${err.message}`);
107
+ }
108
+
109
+ return latest;
110
+ }
111
+
112
+ return config;
113
+ };
114
+
115
+ const getDifferentAttributes = (dbInfo, config) => {
116
+ const differentAttributes = [];
117
+ const attributes = [
118
+ { name: 'port', type: Number, required: false },
119
+ { name: 'routing.https', type: Boolean, required: false },
120
+ { name: 'routing.maxUploadFileSize', type: null, required: true },
121
+ { name: 'routing.adminPath', type: null, required: true },
122
+ { name: 'routing.headers', type: null, required: false },
123
+ { name: 'routing.ipWildcardDomain', type: null, required: false },
124
+ { name: 'routing.wildcardCertHost', type: null, required: false },
125
+ { name: 'routing.provider', type: null, required: false },
126
+ { name: 'routing.httpPort', type: Number, required: false },
127
+ { name: 'routing.httpsPort', type: Number, required: false },
128
+ { name: 'mode', type: String, required: false },
129
+ { name: 'runtimeConfig.daemonMaxMemoryLimit', type: Number, required: false },
130
+ { name: 'runtimeConfig.proxyMaxMemoryLimit', type: Number, required: false },
131
+ { name: 'runtimeConfig.blockletMaxMemoryLimit', type: Number, required: false },
132
+ { name: 'didRegistry', type: String, required: true },
133
+ { name: 'didDomain', type: String, required: true },
134
+ ];
135
+
136
+ if (isInServerlessMode(config.node)) {
137
+ attributes.push({ name: 'slpDomain', type: String, required: true });
138
+ }
139
+
140
+ attributes.forEach(({ name, type, required }) => {
141
+ let dbValue = get(dbInfo, name);
142
+ let configValue = get(config.node, name);
143
+ if (typeof type === 'function') {
144
+ dbValue = type(dbValue);
145
+ configValue = type(configValue);
146
+ }
147
+
148
+ let hasChanged = false;
149
+ // If both value are required
150
+ if (required) {
151
+ hasChanged = dbValue && configValue && !isEqual(dbValue, configValue);
152
+ } else {
153
+ hasChanged = !isEqual(dbValue, configValue);
154
+ }
155
+
156
+ if (hasChanged) {
157
+ differentAttributes.push({ name, configValue, dbValue });
158
+ }
159
+ });
160
+
161
+ return differentAttributes;
162
+ };
163
+
164
+ /**
165
+ * For security reasons, we only allow user to change domain and registry for now
166
+ */
167
+ const isConfigDifferentFromDB = (dbInfo, config) => {
168
+ if (!dbInfo) {
169
+ return false;
170
+ }
171
+
172
+ const differentAttributes = getDifferentAttributes(dbInfo, config);
173
+ debug('different attributes', differentAttributes);
174
+
175
+ const formatValue = (value) => {
176
+ if (Array.isArray(value)) {
177
+ return value.join('\n');
178
+ }
179
+
180
+ if (isObject(value)) {
181
+ return Object.keys(value)
182
+ .map((c) => `${c}: ${value[c]}`)
183
+ .join('\n');
184
+ }
185
+
186
+ return value;
187
+ };
188
+
189
+ if (differentAttributes.length > 0) {
190
+ const tableHead = ['Name', 'Configuration', 'Database'];
191
+
192
+ const table = new Table({
193
+ head: tableHead,
194
+ style: { 'padding-left': 1, head: ['cyan', 'bold'] },
195
+ });
196
+
197
+ differentAttributes.forEach(({ name, configValue, dbValue }) => {
198
+ table.push([name, formatValue(configValue), formatValue(dbValue)]);
199
+ });
200
+
201
+ util.printWarning('We found that there are some differences between config file and database');
202
+ util.print(table.toString());
203
+ return true;
204
+ }
205
+
206
+ return false;
207
+ };
208
+
209
+ const startService = async (logDir, maxMemoryRestart, environments) => {
210
+ const servicePort = getPort(PROCESS_NAME_SERVICE);
211
+ debug('start service', { servicePort });
212
+ if (!servicePort) {
213
+ printError('Can not get port for Blocklet Service');
214
+ shelljs.exec(`${ABT_NODE_COMMAND_NAME} stop --force`, { silent: true });
215
+ process.exit(1);
216
+ }
217
+
218
+ try {
219
+ await wrapSpinner('Starting blocklet service...', async () => {
220
+ await pm2.startAsync({
221
+ namespace: 'daemon',
222
+ name: PROCESS_NAME_SERVICE,
223
+ script: './lib/process/service.js',
224
+ max_memory_restart: maxMemoryRestart,
225
+ output: path.join(logDir, 'service.stdout.log'),
226
+ error: path.join(logDir, 'service.stderr.log'),
227
+ cwd: getCliCwd(),
228
+ wait_ready: true,
229
+ max_restarts: 3,
230
+ listen_timeout: 3000,
231
+ time: true,
232
+ env: {
233
+ ...environments,
234
+ ABT_NODE_SERVICE_PORT: servicePort,
235
+ },
236
+ execMode: 'cluster',
237
+ mergeLogs: true,
238
+ instances: getServiceInstanceNumber(),
239
+ });
240
+
241
+ await ensureEndpointHealthy({
242
+ port: servicePort,
243
+ timeout: 30 * 1000,
244
+ minConsecutiveTime: 2000,
245
+ });
246
+ }); // prettier-ignore
247
+ } catch (err) {
248
+ printError(`Blocklet Service start failed: ${err.message}`);
249
+ process.exit(1);
250
+ }
251
+
252
+ return servicePort;
253
+ };
254
+
255
+ const checkRunningDaemonInstance = async () => {
256
+ if (!(await checkRunning())) {
257
+ return { status: 'pass' };
258
+ }
259
+
260
+ const currentDirectoryFilePath = getConfigFileFromAncestors(process.cwd());
261
+ const runningInstanceConfigFile = await getRunningConfigFile();
262
+
263
+ debug('current directory file path', currentDirectoryFilePath);
264
+ debug('running instance config file', runningInstanceConfigFile);
265
+
266
+ if (currentDirectoryFilePath && runningInstanceConfigFile && currentDirectoryFilePath !== runningInstanceConfigFile) {
267
+ const newValidDataFolder = getDataDirectoryByConfigFile(currentDirectoryFilePath);
268
+ printWarning(
269
+ 'There is a running Blocklet Server instance from:',
270
+ chalk.yellow(getDataDirectoryByConfigFile(runningInstanceConfigFile))
271
+ );
272
+ printInfo('And a valid Blocklet Server config and data directory is found:', chalk.cyan(newValidDataFolder));
273
+
274
+ const answer = await inquirer.prompt({
275
+ type: 'confirm',
276
+ name: 'switch',
277
+ message: `Do you want to stop the running instance and start using ${chalk.cyan(newValidDataFolder)} folder?`,
278
+ default: false,
279
+ });
280
+
281
+ if (answer.switch === true) {
282
+ return { status: 'switch', from: runningInstanceConfigFile, to: currentDirectoryFilePath };
283
+ }
284
+
285
+ return { status: 'exit' };
286
+ }
287
+
288
+ return { status: 'pass' };
289
+ };
290
+
291
+ /**
292
+ *
293
+ *
294
+ * @param {string} configVersion
295
+ * @param {string} cliVersion
296
+ * @return {{
297
+ * matched: boolean,
298
+ * nextMajorVersion: string | null,
299
+ * configVersion: string,
300
+ * cliVersion: string,
301
+ * }}
302
+ */
303
+ const isVersionMatched = (configVersion, cliVersion) => {
304
+ if (!semver.valid(configVersion)) {
305
+ throw new Error(`invalid configuration version, got ${configVersion}`);
306
+ }
307
+
308
+ if (!semver.valid(cliVersion)) {
309
+ throw new Error(`invalid cli version, got ${cliVersion}`);
310
+ }
311
+
312
+ const coercedConfigVersion = semver.coerce(configVersion);
313
+ const coercedCliVersion = semver.coerce(cliVersion);
314
+
315
+ const nextMajorVersion = semver.inc(coercedConfigVersion, 'major');
316
+
317
+ if (semver.gte(coercedCliVersion, coercedConfigVersion) && semver.lt(cliVersion, nextMajorVersion)) {
318
+ return {
319
+ matched: true,
320
+ nextMajorVersion,
321
+ configVersion,
322
+ cliVersion,
323
+ };
324
+ }
325
+
326
+ return {
327
+ matched: false,
328
+ nextMajorVersion,
329
+ configVersion,
330
+ cliVersion,
331
+ };
332
+ };
333
+
334
+ const applyInitialBlocklets = async (node, config) => {
335
+ const tasks = config.initialize.blocklets.map(
336
+ ({ name, resolved }) =>
337
+ wrapSpinner(`Launching blocklet ${name}`, async () => {
338
+ const { meta } = await node.getBlockletMetaFromUrl({ url: resolved })
339
+ if (meta.name !== name) {
340
+ throw new Error(`The blocklet name does not match with config: expected ${name}, received ${meta.name}`);
341
+ }
342
+
343
+ const blocklet = await node.installBlocklet({ url: resolved, sync: true });
344
+ await node.handleBlockletEvent(BlockletEvents.installed, { blocklet });
345
+ await node.startBlocklet({ did: blocklet.meta.did, checkHealthImmediately: true, throwOnError: true });
346
+ })
347
+ ); // prettier-ignore
348
+
349
+ const results = await Promise.allSettled(tasks);
350
+ const errorResult = results.find((x) => x.status === 'rejected');
351
+ if (errorResult) {
352
+ throw errorResult.reason;
353
+ }
354
+ };
355
+
356
+ const prepareInitialData = async (node, config, dataDir) => {
357
+ // check
358
+ if (!config.initialize || !config.initialize.blocklets || !config.initialize.blocklets.length) {
359
+ return;
360
+ }
361
+
362
+ // begin
363
+ const dataDirBak = `${dataDir}~`;
364
+ debug('dataDirBak:', dataDirBak);
365
+ await fs.copy(dataDir, dataDirBak);
366
+ try {
367
+ await applyInitialBlocklets(node, config);
368
+ await fs.remove(dataDirBak);
369
+ } catch (error) {
370
+ await fs.move(dataDirBak, dataDir, { overwrite: true });
371
+ // FIXME: 这里可能会因错误导致节点无法启动, 进而导致节点在 ec2 上被无限重启
372
+ throw error;
373
+ }
374
+ };
375
+
376
+ const exec = async ({ workingDir, config, dataDir, mode, updateDb, forceIntranet }) => {
377
+ const configFile = await getConfigFile(workingDir);
378
+
379
+ const proxyMaxMemoryLimit = get(config, 'node.runtimeConfig.proxyMaxMemoryLimit', PROXY_MAX_MEM_LIMIT_IN_MB);
380
+
381
+ // 如果日志目录不存在,pm2 会启动可能会失败, 测试发现 docker 里的 Ubuntu 18.04.4 LTS 系统会失败,
382
+ // MAC OS 下正常,但是没搞明白为什么
383
+ const logDir = process.env.ABT_NODE_LOG_DIR || getDaemonLogDir(dataDir);
384
+ fs.ensureDirSync(logDir);
385
+
386
+ // We need to create node instance after db-proxy started
387
+ const { node, getBaseUrls, publishEvent, wallet } = await nodeLib.getNode({ dir: workingDir });
388
+
389
+ // start event-hub
390
+ const eventHubPort = await startEventHub(logDir, proxyMaxMemoryLimit);
391
+
392
+ return new Promise((resolve) => {
393
+ node.onReady(async () => {
394
+ const daemonErrorLogFile = path.join(logDir, 'daemon.stderr.log');
395
+ const startTimeMs = Date.now();
396
+ try {
397
+ // FIXME: @zhanghan 如果当前版本是 Beta 版(比如 1.16.33-beta-xxx),而 config.node.version 是 1.16.32 时,这里的逻辑不会执行,需要修复这个问题(这种情况会出现在本地执行 bn dev 时出现)
398
+ // 这个判断貌似没必要? 因为 runSchemaMigrations 会自动判断哪些 migration 需要执行
399
+ // Ensure all migrations are executed when config version is not latest
400
+ if (semver.gt(version, config.node.version)) {
401
+ // Migrate db schema first
402
+ await runSchemaMigrations({ dataDir, printInfo, printSuccess, blocklets: await node.getBlocklets() });
403
+
404
+ // Migrate application logic later
405
+ const migrationSucceeded = await runMigrationScripts({
406
+ node,
407
+ config,
408
+ configFile,
409
+ dataDir,
410
+ printInfo,
411
+ printError,
412
+ printSuccess,
413
+ });
414
+
415
+ if (migrationSucceeded === false) {
416
+ return resolve(1);
417
+ }
418
+
419
+ // NOTE: this is a workaround for the blocklet state revert issue after migration
420
+ if (process.env.ABORT_AFTER_MIGRATION) {
421
+ shelljs.exec(`${ABT_NODE_COMMAND_NAME} stop --force`, { silent: true });
422
+ return resolve(0);
423
+ }
424
+ }
425
+
426
+ // Ensure version is correct
427
+ const latest = await updateVersionInConfigFileAndState(config, configFile, node);
428
+
429
+ let info = await node.getNodeInfo();
430
+ getDockerStatusLog(printInfo, info);
431
+
432
+ const isRunning = await checkRunning();
433
+
434
+ // for backward compatibility: info.status may not exist
435
+ const startFromCrash = !isRunning && info.status && info.status !== SERVER_STATUS.STOPPED;
436
+ if (startFromCrash) {
437
+ await node.updateNodeStatus(SERVER_STATUS.START_FROM_CRASH);
438
+ }
439
+
440
+ const isProviderAvailable = await checkRoutingProvider(info.routing, { configDir: path.dirname(configFile) });
441
+ if (!isProviderAvailable) {
442
+ shelljs.exec(`${ABT_NODE_COMMAND_NAME} stop --force`, { silent: true });
443
+ return resolve(1);
444
+ }
445
+
446
+ const isConfigChanged = isConfigDifferentFromDB(info, latest);
447
+ if (isConfigChanged) {
448
+ if (updateDb) {
449
+ if (!isValidMode(latest.node.mode)) {
450
+ throw new Error(
451
+ `Invalid server mode, available modes:${prettyAvailableModes()}, current is ${latest.node.mode}.`
452
+ );
453
+ }
454
+
455
+ printWarning('And you enabled --update-db flag to update the database');
456
+ const data = {
457
+ port: latest.node.port,
458
+ routing: Object.assign({}, info.routing || {}, {
459
+ https: latest.node.routing.https,
460
+ provider: latest.node.routing.provider,
461
+ adminPath: latest.node.routing.adminPath,
462
+ headers: latest.node.routing.headers,
463
+ ipWildcardDomain: latest.node.routing.ipWildcardDomain,
464
+ wildcardCertHost: latest.node.routing.wildcardCertHost,
465
+ maxUploadFileSize: latest.node.routing.maxUploadFileSize,
466
+ httpPort: latest.node.routing.httpPort,
467
+ httpsPort: latest.node.routing.httpsPort,
468
+ }),
469
+ mode: latest.node.mode,
470
+ didRegistry: latest.node.didRegistry,
471
+ didDomain: latest.node.didDomain,
472
+ runtimeConfig: {
473
+ daemonMaxMemoryLimit: get(
474
+ latest,
475
+ 'node.runtimeConfig.daemonMaxMemoryLimit',
476
+ DAEMON_MAX_MEM_LIMIT_IN_MB
477
+ ),
478
+ blockletMaxMemoryLimit: get(
479
+ latest,
480
+ 'node.runtimeConfig.blockletMaxMemoryLimit',
481
+ BLOCKLET_MAX_MEM_LIMIT_IN_MB
482
+ ),
483
+ proxyMaxMemoryLimit: get(latest, 'node.runtimeConfig.proxyMaxMemoryLimit', DAEMON_MAX_MEM_LIMIT_IN_MB),
484
+ },
485
+ };
486
+
487
+ if (isInServerlessMode(latest.node)) {
488
+ data.slpDomain = latest.node.slpDomain;
489
+ }
490
+
491
+ info = await node.states.node.updateNodeInfo(data);
492
+ printSuccess('Blocklet Server config was updated to database');
493
+ } else {
494
+ printInfo(`If you want to update the database, restart node with ${chalk.cyan('--update-db')} flag`);
495
+ }
496
+ }
497
+
498
+ if (typeof mode !== 'undefined' && info.mode !== mode) {
499
+ if (mode !== NODE_MODES.MAINTENANCE) {
500
+ await node.states.node.updateNodeInfo({ nextVersion: '', upgradeSessionId: '' });
501
+ printSuccess('Cleaned upgrade information');
502
+ }
503
+
504
+ await node.states.node.setMode(mode);
505
+ printSuccess(`Blocklet Server mode has been set to ${chalk.cyan(mode)}`);
506
+ }
507
+
508
+ if (isInServerlessMode(info)) {
509
+ info = await node.states.node.updateNodeInfo({ enableWelcomePage: false });
510
+ debug(`disable welcome page in ${NODE_MODES.SERVERLESS} mode`);
511
+ }
512
+
513
+ await node.ensureDashboardRouting();
514
+
515
+ await wrapSpinner(
516
+ 'Updating wildcard certificates...',
517
+ async () => {
518
+ await node.ensureWildcardCerts();
519
+ },
520
+ { throwOnError: false }
521
+ );
522
+
523
+ // install initial blocklets
524
+ const initialized = await node.isInitialized();
525
+ if (!initialized) {
526
+ await prepareInitialData(node, latest, dataDir);
527
+ }
528
+
529
+ // NOTE: following configurations are required to start the daemon dashboard
530
+ const environments = {
531
+ ABT_NODE_SK: config.node.sk,
532
+ ABT_NODE_DID: config.node.did,
533
+ ABT_NODE_PORT: config.node.port,
534
+ ABT_NODE_SESSION_SECRET: config.node.secret,
535
+ ABT_NODE_DATA_DIR: dataDir,
536
+ ABT_NODE_CONFIG_FILE: configFile,
537
+ ABT_NODE_BLOCKLET_PORT: config.blocklet.port,
538
+ ABT_NODE_UPDATER_PORT: getPort(PROCESS_NAME_UPDATER),
539
+ ABT_NODE_TEST_DOCKER: process.env.ABT_NODE_TEST_DOCKER,
540
+ MAX_UPLOAD_FILE_SIZE: config.node.routing.maxUploadFileSize || MAX_UPLOAD_FILE_SIZE,
541
+ MAX_NGINX_WORKER_CONNECTIONS: config.node.routing.maxNginxWorkerConnections || MAX_NGINX_WORKER_CONNECTIONS,
542
+ NGINX_MODULES_PATH: process.env.NGINX_MODULES_PATH,
543
+ ABT_NODE_EVENT_PORT: eventHubPort,
544
+ ABT_NODE_PACKAGE_NAME: process.env.ABT_NODE_PACKAGE_NAME,
545
+ ABT_NODE_JOB_NAME: process.env.ABT_NODE_JOB_NAME || '', // used to trigger log analyzing
546
+ ABT_NODE_BINARY_NAME,
547
+ ABT_NODE_COMMAND_NAME,
548
+ BLOCKLET_SERVER_RUNNING_BEFORE: isRunning ? '1' : '',
549
+ ...(config.env || {}),
550
+ };
551
+ if (process.env.DEBUG) {
552
+ environments.DEBUG = process.env.DEBUG;
553
+ }
554
+ debug('environments', omit(environments, ['ABT_NODE_SK']));
555
+
556
+ const maxMemoryRestart = `${get(info, 'runtimeConfig.daemonMaxMemoryLimit', DAEMON_MAX_MEM_LIMIT_IN_MB)}M`;
557
+ debug('pm2 daemon max memory restart', maxMemoryRestart);
558
+
559
+ // start service
560
+ const servicePort = await startService(logDir, maxMemoryRestart, environments);
561
+ environments.ABT_NODE_SERVICE_PORT = servicePort;
562
+
563
+ // check localhost listening
564
+ await wrapSpinner('Starting server daemon...', async () => {
565
+ // start daemon
566
+ await pm2.startAsync({
567
+ namespace: 'daemon',
568
+ name: PROCESS_NAME_DAEMON,
569
+ script: './lib/process/daemon.js',
570
+ max_memory_restart: maxMemoryRestart,
571
+ output: path.join(logDir, 'daemon.stdout.log'),
572
+ error: daemonErrorLogFile,
573
+ cwd: getCliCwd(),
574
+ max_restarts: 3,
575
+ wait_ready: true,
576
+ listen_timeout: 3000,
577
+ env: environments,
578
+ time: true,
579
+ });
580
+
581
+ // check daemon
582
+ await ensureEndpointHealthy({
583
+ port: info.port,
584
+ timeout: 30 * 1000, // wait up to 30 seconds
585
+ minConsecutiveTime: 2000,
586
+ });
587
+ });
588
+
589
+ try {
590
+ await publishEvent(EVENTS.NODE_STARTED, {});
591
+ } catch (err) {
592
+ printError(`Failed to publish start event to socket server: ${err.message}`);
593
+ }
594
+
595
+ const accessUrls = await util.getDaemonAccessUrls({ info, wallet, getBaseUrls, forceIntranet });
596
+
597
+ printAccessUrls(accessUrls);
598
+
599
+ return resolve(0);
600
+ } catch (err) {
601
+ debug('start error', err);
602
+ printError(`Blocklet Server start failed due to: ${chalk.red(err.message)}`);
603
+ if (fs.existsSync(daemonErrorLogFile) && fs.statSync(daemonErrorLogFile).ctimeMs > startTimeMs) {
604
+ print(chalk.gray(`${daemonErrorLogFile} last 15 lines:`));
605
+ const lastLogs = await readLastLines.read(daemonErrorLogFile, 15);
606
+ print(lastLogs);
607
+ printWarning(`Check ${daemonErrorLogFile} file for more error messages`);
608
+ }
609
+ printInfo(
610
+ `You can run ${chalk.cyan(`${ABT_NODE_COMMAND_NAME} start`)} again to fix error if it's router related.`
611
+ );
612
+ shelljs.exec(`${ABT_NODE_COMMAND_NAME} stop --force`, { silent: true });
613
+
614
+ return resolve(1);
615
+ } finally {
616
+ // ensure log rotator and updater is not global anymore
617
+ await cleanupProcessByName(PROCESS_NAME_PROXY);
618
+ await cleanupProcessByName(PROCESS_NAME_UPDATER);
619
+ await cleanupProcessByName(PROCESS_NAME_LOG_ROTATE);
620
+ }
621
+ });
622
+ });
623
+ };
624
+
625
+ const createStartLock = ({ lockFile, pid }) => fs.writeFileSync(lockFile, pid.toString());
626
+
627
+ const clearStartLock = ({ pid, lockFile, enforceLock }) => {
628
+ try {
629
+ // 如果使用了 autoInit, 只要存在 lock 文件则直接删除改文件
630
+ if (!enforceLock && fs.existsSync(lockFile)) {
631
+ fs.removeSync(lockFile);
632
+ return;
633
+ }
634
+
635
+ if (!enforceLock || !fs.existsSync(lockFile)) {
636
+ return;
637
+ }
638
+
639
+ const lockPid = fs.readFileSync(lockFile).toString();
640
+ if (lockPid !== pid.toString()) {
641
+ return;
642
+ }
643
+
644
+ fs.removeSync(lockFile);
645
+ } catch (error) {
646
+ debug('clear start log failed', error);
647
+ util.printError(`Clear start lock failed, please delete this file manually ${lockFile}`);
648
+ }
649
+ };
650
+
651
+ /**
652
+ * start blocklet server
653
+ * @param {object} params
654
+ * @param {boolean} params.updateDb the database from the configuration file or not
655
+ * @param {boolean} params.enforceLock should we enforce start lock
656
+ * @param {string} params.workingDir working directory when start node
657
+ * @param {boolean} params.forceIntranet force server to run in intranet, ignore any external IP
658
+ * @param {string} params.mode server mode
659
+ * @returns {Promise<number>} 0: success, others: failed
660
+ */
661
+ const start = async ({ updateDb, enforceLock, forceIntranet, workingDir, mode }) => {
662
+ const { pid } = process;
663
+ const { config, dataDir } = await nodeLib.readNodeConfig(workingDir);
664
+ // Ensure only one start is running at the same time
665
+ const lockFile = path.join(dataDir, 'start.lock');
666
+
667
+ const onExit = (_, code) => {
668
+ try {
669
+ clearStartLock({ pid, enforceLock, lockFile });
670
+ } catch (error) {
671
+ debug('clear start lock failed', error);
672
+ }
673
+
674
+ process.exit(code);
675
+ };
676
+
677
+ [
678
+ 'SIGINT',
679
+ 'SIGTERM',
680
+ 'SIGHUP', // the console window is closed
681
+ 'SIGBREAK', // <Ctrl>+<Break> (in Windows)
682
+ ].forEach((sig) => {
683
+ process.on(sig, onExit);
684
+ });
685
+
686
+ // uncaughtException is handled outside
687
+ process.on('exit', (code) => {
688
+ if (code > 0) {
689
+ clearStartLock({ pid, enforceLock, lockFile });
690
+ }
691
+ });
692
+
693
+ util.ensurePermission(dataDir);
694
+
695
+ if (!canUseFileSystemIsolateApi()) {
696
+ util.printWarning(
697
+ `The current Node.js version is below ${SAFE_NODE_VERSION}. To enable strict mode, where blocklets are isolated, it is recommended to install Node.js version ${SAFE_NODE_VERSION} or higher.`
698
+ );
699
+ }
700
+
701
+ const cliVersion = util.getCLIVersion();
702
+ const matchResult = isVersionMatched(config.node.version, cliVersion);
703
+ if (matchResult.matched === false) {
704
+ util.printError(
705
+ // eslint-disable-next-line max-len
706
+ `Your CLI version ${matchResult.cliVersion} does not match the version ${matchResult.configVersion} in server config, please install a version >= ${matchResult.configVersion} and < ${matchResult.nextMajorVersion} `
707
+ );
708
+
709
+ return 1;
710
+ }
711
+
712
+ // Ensure db schema and data migrated: only need to run once
713
+ await runSchemaMigrations({ dataDir, printInfo, printSuccess });
714
+
715
+ try {
716
+ if (enforceLock) {
717
+ if (fs.existsSync(lockFile)) {
718
+ util.printWarning('Server is starting, please try again later');
719
+ util.printInfo(`Please cleanup with ${chalk.cyan(`rm -f ${dataDir}/start.lock`)} and then try again`);
720
+ return 1;
721
+ }
722
+
723
+ createStartLock({ lockFile, pid });
724
+ }
725
+
726
+ const code = await exec({ config, dataDir, updateDb, mode, workingDir, forceIntranet });
727
+ return code;
728
+ } catch (error) {
729
+ debug('start error', error);
730
+ printError(`Blocklet Server start failed due to: ${chalk.red(error.message)}`);
731
+
732
+ printInfo(`You can run ${chalk.cyan(`${ABT_NODE_COMMAND_NAME} start`)} again to fix error if it's router related.`);
733
+ shelljs.exec(`${ABT_NODE_COMMAND_NAME} stop --force`, { silent: true });
734
+
735
+ return 1;
736
+ } finally {
737
+ clearStartLock({ pid, enforceLock, lockFile });
738
+ }
739
+ };
740
+
741
+ const stopDaemon = async () => {
742
+ try {
743
+ await pm2.killDaemonAsync();
744
+ printSuccess('Blocklet Server related processes stopped successfully');
745
+ } catch (err) {
746
+ printError(`Blocklet Server related processes stop failed: ${err.message}`);
747
+ }
748
+ };
749
+
750
+ const stopOriginNode = async () => {
751
+ try {
752
+ await stopDaemon();
753
+ const results = await Promise.all([
754
+ clearRouterByConfigKeyword(CONFIG_FOLDER_NAME),
755
+ clearRouterByConfigKeyword(CONFIG_FOLDER_NAME_OLD),
756
+ ]);
757
+ if (results.some(Boolean)) {
758
+ printSuccess('Routing engine was successfully stopped');
759
+ }
760
+
761
+ printSuccess('Stop original node successfully');
762
+ } catch (error) {
763
+ printError('Stop original node failed:', error.message);
764
+ }
765
+ };
766
+
767
+ exports.run = async ({ updateDb, autoInit = false, forceIntranet = false, forceMode: mode }) => {
768
+ if (typeof mode !== 'undefined' && !isValidMode(mode)) {
769
+ printInvalidModeInfo();
770
+ process.exit(1);
771
+ }
772
+
773
+ const workingDir = process.cwd();
774
+ if (autoInit) {
775
+ const baseConfigDirectory = getBaseConfigDataDirectory(workingDir);
776
+ if (baseConfigDirectory) {
777
+ print(`Can't initialize Blocklet Server instance inside another instance: ${baseConfigDirectory}`);
778
+ process.exit(1);
779
+ }
780
+
781
+ const configDir = getConfigDirFromCurrentDir(workingDir);
782
+
783
+ if (configDir) {
784
+ print(`Blocklet Server instance already exists in ${workingDir}, now starting...`);
785
+ } else {
786
+ printInfo(`Initialize new Blocklet Server instance config in ${workingDir}...`);
787
+ const { stderr, code } = shelljs.exec(`${ABT_NODE_COMMAND_NAME} init -f`, { silent: true });
788
+ if (code !== 0) {
789
+ printError(`Failed to initialize Blocklet Server configuration in ${workingDir}: ${stderr}`);
790
+ process.exit(1);
791
+ }
792
+ }
793
+ }
794
+
795
+ const result = await checkRunningDaemonInstance();
796
+ if (result.status === 'exit') {
797
+ process.exit(1);
798
+ }
799
+
800
+ if (result.status === 'switch') {
801
+ await stopOriginNode();
802
+ }
803
+
804
+ util.checkTerminalProxy('Blocklet Server');
805
+
806
+ // Skip start lock if we are started using `--auto-init` flag
807
+ const startResult = await start({ updateDb, workingDir, enforceLock: !autoInit, forceIntranet, mode });
808
+
809
+ if (startResult !== 0) {
810
+ process.exit(startResult);
811
+ }
812
+
813
+ process.exit(0);
814
+ };
815
+
816
+ exports.start = start;
817
+ exports.isVersionMatched = isVersionMatched;
818
+ exports.clearStartLock = clearStartLock;
819
+ exports.createStartLock = createStartLock;
820
+ exports.isConfigDifferentFromDB = isConfigDifferentFromDB;
821
+ exports.getDifferentAttributes = getDifferentAttributes;