@abtnode/core 1.16.52-beta-20251002-030549-0f91dab2 → 1.16.52-beta-20251005-235515-42ad5caf

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.
@@ -116,7 +116,6 @@ const fetchPm2 = require('@abtnode/util/lib/pm2/fetch-pm2');
116
116
 
117
117
  const promiseSpawn = require('@abtnode/util/lib/promise-spawn');
118
118
  const { getAbtNodeRedisAndSQLiteUrl } = require('@abtnode/db-cache');
119
- const xbytes = require('xbytes');
120
119
  const { validate: validateEngine, get: getEngine } = require('../blocklet/manager/engine');
121
120
 
122
121
  const isRequirementsSatisfied = require('./requirement');
@@ -184,6 +183,7 @@ const statusMap = {
184
183
  errored: BlockletStatus.error,
185
184
  stopping: BlockletStatus.stopping,
186
185
  stopped: BlockletStatus.stopped,
186
+ 'waiting restart': BlockletStatus.restarting,
187
187
  };
188
188
 
189
189
  const PRIVATE_NODE_ENVS = [
@@ -449,6 +449,7 @@ const getComponentSystemEnvironments = (blocklet) => {
449
449
  if (port) {
450
450
  portEnvironments[BLOCKLET_DEFAULT_PORT_NAME] = port;
451
451
  }
452
+
452
453
  if (ports) {
453
454
  Object.assign(portEnvironments, ports);
454
455
  }
@@ -466,11 +467,24 @@ const getComponentSystemEnvironments = (blocklet) => {
466
467
  };
467
468
  };
468
469
 
469
- const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
470
+ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors, isGreen = false) => {
470
471
  const root = (ancestors || [])[0] || blocklet;
471
472
 
472
473
  const initialized = root?.settings?.initialized;
473
474
 
475
+ const environmentObj = { ...(blocklet.environmentObj || {}) };
476
+ if (isGreen && blocklet.greenPorts) {
477
+ Object.entries(blocklet.greenPorts).forEach(([key, value]) => {
478
+ if (!value) {
479
+ return;
480
+ }
481
+ environmentObj[key] = value;
482
+ if (key === BLOCKLET_DEFAULT_PORT_NAME || key === 'BLOCKLET_PORT') {
483
+ environmentObj[BLOCKLET_DEFAULT_PORT_NAME] = value;
484
+ }
485
+ });
486
+ }
487
+
474
488
  // pm2 will force inject env variables of daemon process to blocklet process
475
489
  // we can only rewrite these private env variables to empty
476
490
  const safeNodeEnvironments = PRIVATE_NODE_ENVS.reduce((o, x) => {
@@ -495,8 +509,9 @@ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
495
509
  const ports = {};
496
510
  forEachBlockletSync(root, (x) => {
497
511
  const webInterface = findWebInterface(x);
498
- if (webInterface && x.environmentObj[webInterface.port]) {
499
- ports[x.environmentObj.BLOCKLET_REAL_NAME] = x.environmentObj[webInterface.port];
512
+ const envObj = x.meta?.did === blocklet.meta?.did ? environmentObj : x.environmentObj;
513
+ if (webInterface && envObj?.[webInterface.port]) {
514
+ ports[envObj.BLOCKLET_REAL_NAME] = envObj[webInterface.port];
500
515
  }
501
516
  });
502
517
 
@@ -518,7 +533,7 @@ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
518
533
  const env = {
519
534
  ...blocklet.configObj,
520
535
  ...getSharedConfigObj((ancestors || [])[0], blocklet, true),
521
- ...blocklet.environmentObj,
536
+ ...environmentObj,
522
537
  ...devEnvironments,
523
538
  BLOCKLET_MOUNT_POINTS: JSON.stringify(componentsInternalInfo),
524
539
  BLOCKLET_MODE: blocklet.mode || BLOCKLET_MODES.PRODUCTION,
@@ -559,6 +574,10 @@ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
559
574
  env.initialized = initialized;
560
575
  }
561
576
 
577
+ if (isGreen && blocklet.greenPorts?.[BLOCKLET_DEFAULT_PORT_NAME]) {
578
+ env[BLOCKLET_DEFAULT_PORT_NAME] = blocklet.greenPorts[BLOCKLET_DEFAULT_PORT_NAME];
579
+ }
580
+
562
581
  // ensure all envs are literals and do not contain line breaks
563
582
  Object.keys(env).forEach((key) => {
564
583
  env[key] = formatEnv(env[key]);
@@ -573,7 +592,7 @@ const isUsefulError = (err) =>
573
592
  !/id unknown/.test(err.message) &&
574
593
  !/^Process \d+ not found$/.test(err.message);
575
594
 
576
- const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately } = {}) => {
595
+ const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately, componentDids } = {}) => {
577
596
  let minConsecutiveTime = 3000;
578
597
  if (process.env.NODE_ENV === 'test' && process.env.ABT_NODE_TEST_MIN_CONSECUTIVE_TIME !== undefined) {
579
598
  minConsecutiveTime = +process.env.ABT_NODE_TEST_MIN_CONSECUTIVE_TIME;
@@ -594,11 +613,15 @@ const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately } = {}) => {
594
613
  };
595
614
  }
596
615
 
616
+ const children = componentDids?.length
617
+ ? blocklet.children.filter((child) => componentDids.includes(child.meta.did))
618
+ : blocklet.children;
619
+
597
620
  // Let's wait for at least 1 minute for the blocklet to go live
598
621
  let startTimeout =
599
622
  Math.max(
600
623
  get(blocklet, 'meta.timeout.start', 60),
601
- ...(blocklet?.children || []).map((child) => child.meta?.timeout?.start || 0)
624
+ ...(children || []).map((child) => child.meta?.timeout?.start || 0)
602
625
  ) * 1000;
603
626
 
604
627
  if (process.env.NODE_ENV === 'test') {
@@ -628,6 +651,7 @@ const startBlockletProcess = async (
628
651
  componentDids,
629
652
  configSynchronizer,
630
653
  onlyStart = false,
654
+ isGreen = false,
631
655
  } = {}
632
656
  ) => {
633
657
  if (!blocklet) {
@@ -637,6 +661,25 @@ const startBlockletProcess = async (
637
661
  const dockerNetworkName = parseDockerName(blocklet?.meta?.did, 'docker-network');
638
662
  await createDockerNetwork(dockerNetworkName);
639
663
 
664
+ blocklet.children.forEach((component) => {
665
+ if (!componentDids.includes(component.meta.did)) {
666
+ return;
667
+ }
668
+ for (const envItem of component.environments) {
669
+ const envKey = envItem.key || envItem.name;
670
+ if (envKey !== 'BLOCKLET_PORT') {
671
+ continue;
672
+ }
673
+ if (isGreen) {
674
+ if (component.greenPorts?.BLOCKLET_PORT) {
675
+ envItem.value = component.greenPorts.BLOCKLET_PORT;
676
+ }
677
+ } else if (component.ports?.BLOCKLET_PORT) {
678
+ envItem.value = component.ports.BLOCKLET_PORT;
679
+ }
680
+ }
681
+ });
682
+
640
683
  const startBlockletTask = async (b, { ancestors }) => {
641
684
  // 需要在在这里传入字符串类型,否则进程中如法转化成 Date 对象
642
685
  const now = `${new Date()}`;
@@ -674,7 +717,7 @@ const startBlockletProcess = async (
674
717
  }
675
718
 
676
719
  // get env
677
- const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors);
720
+ const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors, isGreen);
678
721
  const startedAt = Date.now();
679
722
 
680
723
  await installExternalDependencies({ appDir: env?.BLOCKLET_APP_DIR, nodeInfo });
@@ -683,8 +726,12 @@ const startBlockletProcess = async (
683
726
 
684
727
  // kill process if port is occupied
685
728
  try {
686
- const { ports } = b;
687
- await killProcessOccupiedPorts({ ports, pm2ProcessId: processId, printError: logger.error.bind(logger) });
729
+ const { ports, greenPorts } = b;
730
+ await killProcessOccupiedPorts({
731
+ ports: isGreen ? greenPorts : ports,
732
+ pm2ProcessId: processId,
733
+ printError: logger.error.bind(logger),
734
+ });
688
735
  } catch (error) {
689
736
  logger.error('Failed to killProcessOccupiedPorts', { error });
690
737
  }
@@ -692,12 +739,13 @@ const startBlockletProcess = async (
692
739
  // start process
693
740
  const maxMemoryRestart = get(nodeInfo, 'runtimeConfig.blockletMaxMemoryLimit', BLOCKLET_MAX_MEM_LIMIT_IN_MB);
694
741
 
742
+ const processIdName = isGreen ? `${processId}-green` : processId;
695
743
  /**
696
744
  * @type {pm2.StartOptions}
697
745
  */
698
746
  const options = {
699
747
  namespace: 'blocklets',
700
- name: processId,
748
+ name: processIdName,
701
749
  cwd,
702
750
  // FIXME @linchen [] does not work, so use () here
703
751
  log_date_format: '(YYYY-MM-DD HH:mm:ss)',
@@ -772,34 +820,36 @@ const startBlockletProcess = async (
772
820
  delete options.mergeLogs;
773
821
  }
774
822
 
775
- const nextOptions =
776
- b.mode === BLOCKLET_MODES.DEVELOPMENT
777
- ? options
778
- : await parseDockerOptionsFromPm2({
779
- options,
780
- nodeInfo,
781
- isExternal: isExternalBlocklet(b),
782
- meta: b.meta,
783
- ports: b.ports,
784
- onlyStart,
785
- dockerNetworkName,
786
- rootBlocklet: blocklet,
787
- });
823
+ let nextOptions;
824
+ if (b.mode === BLOCKLET_MODES.DEVELOPMENT) {
825
+ nextOptions = options;
826
+ } else {
827
+ nextOptions = await parseDockerOptionsFromPm2({
828
+ options,
829
+ nodeInfo,
830
+ isExternal: isExternalBlocklet(b),
831
+ meta: b.meta,
832
+ ports: isGreen ? b.greenPorts : b.ports,
833
+ onlyStart,
834
+ dockerNetworkName,
835
+ rootBlocklet: blocklet,
836
+ });
837
+ }
788
838
 
789
839
  await fetchPm2({ ...nextOptions, pmx: false }, nodeEnvironments.ABT_NODE_SK);
790
840
 
791
841
  // eslint-disable-next-line no-use-before-define
792
- const status = await getProcessState(processId);
842
+ const status = await getProcessState(processIdName);
793
843
  if (status === BlockletStatus.error) {
794
- throw new Error(`process ${processId} is not running within 3 seconds`);
844
+ throw new Error(`process ${processIdName} is not running within 3 seconds`);
795
845
  }
796
- logger.info('done start blocklet', { processId, status, time: Date.now() - startedAt });
846
+ logger.info('done start blocklet', { processId: processIdName, status, time: Date.now() - startedAt });
797
847
 
798
848
  if (nextOptions.env.connectInternalDockerNetwork) {
799
849
  try {
800
850
  await promiseSpawn(nextOptions.env.connectInternalDockerNetwork, { mute: true });
801
851
  } catch (err) {
802
- logger.error('blocklet connect internal docker network failed', { processId, error: err });
852
+ logger.error('blocklet connect internal docker network failed', { processId: processIdName, error: err });
803
853
  }
804
854
  }
805
855
 
@@ -807,7 +857,7 @@ const startBlockletProcess = async (
807
857
  try {
808
858
  await postStart(b, { env });
809
859
  } catch (err) {
810
- logger.error('blocklet post start failed', { processId, error: err });
860
+ logger.error('blocklet post start failed', { processId: processIdName, error: err });
811
861
  }
812
862
  };
813
863
 
@@ -842,16 +892,28 @@ const startBlockletProcess = async (
842
892
  * Stop all precesses of a blocklet
843
893
  * @param {*} blocklet should contain env props
844
894
  */
845
- const stopBlockletProcess = (blocklet, { preStop = noop, skippedProcessIds = [], componentDids } = {}) => {
895
+ const stopBlockletProcess = (
896
+ blocklet,
897
+ { preStop = noop, skippedProcessIds = [], componentDids, isGreen = false, isStopGreenAndBlue = false } = {}
898
+ ) => {
846
899
  // eslint-disable-next-line no-use-before-define
847
- return deleteBlockletProcess(blocklet, { preDelete: preStop, skippedProcessIds, componentDids });
900
+ return deleteBlockletProcess(blocklet, {
901
+ preDelete: preStop,
902
+ skippedProcessIds,
903
+ componentDids,
904
+ isGreen,
905
+ isStopGreenAndBlue,
906
+ });
848
907
  };
849
908
 
850
909
  /**
851
910
  * Delete all precesses of a blocklet
852
911
  * @param {*} blocklet should contain env props
853
912
  */
854
- const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProcessIds = [], componentDids } = {}) => {
913
+ const deleteBlockletProcess = async (
914
+ blocklet,
915
+ { preDelete = noop, skippedProcessIds = [], componentDids, isGreen = false, isStopGreenAndBlue = false } = {}
916
+ ) => {
855
917
  await forEachBlocklet(
856
918
  blocklet,
857
919
  async (b, { ancestors }) => {
@@ -875,8 +937,16 @@ const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProces
875
937
  return;
876
938
  }
877
939
  await preDelete(b, { ancestors });
940
+ if (isStopGreenAndBlue) {
941
+ // eslint-disable-next-line no-use-before-define
942
+ await deleteProcess(`${b.env.processId}-green`);
943
+ // eslint-disable-next-line no-use-before-define
944
+ await deleteProcess(b.env.processId);
945
+ return;
946
+ }
947
+ const processId = isGreen ? `${b.env.processId}-green` : b.env.processId;
878
948
  // eslint-disable-next-line no-use-before-define
879
- await deleteProcess(b.env.processId);
949
+ await deleteProcess(processId);
880
950
  },
881
951
  { parallel: true }
882
952
  );
@@ -975,57 +1045,10 @@ const validateBlockletChainInfo = (blocklet) => {
975
1045
  return chainInfo;
976
1046
  };
977
1047
 
978
- const checkBlockletProcessHealthy = async (
979
- blocklet,
980
- { minConsecutiveTime, timeout, componentDids, setBlockletRunning } = {}
981
- ) => {
982
- await forEachBlocklet(
983
- blocklet,
984
- async (b) => {
985
- if (b.meta.group === BlockletGroup.gateway) {
986
- return;
987
- }
988
-
989
- // components that relies on another engine component should not be checked
990
- const engine = getBlockletEngine(b.meta);
991
- if (engine.interpreter === 'blocklet') {
992
- return;
993
- }
994
-
995
- if (!hasStartEngine(b.meta)) {
996
- return;
997
- }
998
-
999
- // eslint-disable-next-line no-use-before-define
1000
- if (shouldSkipComponent(b.meta.did, componentDids)) {
1001
- logger.info('skip check component healthy', { processId: b.env.processId });
1002
- return;
1003
- }
1004
-
1005
- const logToTerminal = [blocklet.mode, b.mode].includes(BLOCKLET_MODES.DEVELOPMENT);
1006
-
1007
- const startedAt = Date.now();
1008
-
1009
- // eslint-disable-next-line no-use-before-define
1010
- await _checkProcessHealthy(b, { minConsecutiveTime, timeout, logToTerminal });
1011
- logger.info('done check component healthy', { processId: b.env.processId, time: Date.now() - startedAt });
1012
-
1013
- if (setBlockletRunning) {
1014
- try {
1015
- await setBlockletRunning(b.meta.did);
1016
- } catch (error) {
1017
- logger.error(`Failed to set blocklet as running for DID: ${b.meta.name || b.meta.did}`, { error });
1018
- }
1019
- }
1020
- },
1021
- { parallel: true }
1022
- );
1023
- };
1024
-
1025
- const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, logToTerminal }) => {
1026
- const { meta, ports, env } = blocklet;
1048
+ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, logToTerminal, isGreen = false }) => {
1049
+ const { meta, ports, greenPorts, env } = blocklet;
1027
1050
  const { name } = meta;
1028
- const { processId } = env;
1051
+ const processId = isGreen ? `${env.processId}-green` : env.processId;
1029
1052
 
1030
1053
  const webInterface = (meta.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
1031
1054
  const dockerInterface = (meta.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_DOCKER);
@@ -1048,6 +1071,7 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, log
1048
1071
  };
1049
1072
 
1050
1073
  let status = await getStatus();
1074
+
1051
1075
  for (let i = 0; i < 20 && status !== 'online'; i++) {
1052
1076
  const t = process.env.NODE_ENV !== 'test' ? 500 : 30;
1053
1077
  await sleep(t);
@@ -1058,7 +1082,10 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, log
1058
1082
  throw new Error('process not start within 10s');
1059
1083
  }
1060
1084
 
1061
- const port = findInterfacePortByName({ meta, ports }, (webInterface || dockerInterface).name);
1085
+ const port = findInterfacePortByName(
1086
+ { meta, ports: isGreen ? greenPorts : ports },
1087
+ (webInterface || dockerInterface).name
1088
+ );
1062
1089
  if (logToTerminal) {
1063
1090
  // eslint-disable-next-line no-console
1064
1091
  console.log(
@@ -1087,6 +1114,52 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, log
1087
1114
  }
1088
1115
  };
1089
1116
 
1117
+ const checkBlockletProcessHealthy = async (
1118
+ blocklet,
1119
+ { minConsecutiveTime, timeout, componentDids, setBlockletRunning, isGreen = false } = {}
1120
+ ) => {
1121
+ await forEachBlocklet(
1122
+ blocklet,
1123
+ async (b) => {
1124
+ if (b.meta.group === BlockletGroup.gateway) {
1125
+ return;
1126
+ }
1127
+
1128
+ // components that relies on another engine component should not be checked
1129
+ const engine = getBlockletEngine(b.meta);
1130
+ if (engine.interpreter === 'blocklet') {
1131
+ return;
1132
+ }
1133
+
1134
+ if (!hasStartEngine(b.meta)) {
1135
+ return;
1136
+ }
1137
+
1138
+ // eslint-disable-next-line no-use-before-define
1139
+ if (shouldSkipComponent(b.meta.did, componentDids)) {
1140
+ logger.info('skip check component healthy', { processId: b.env.processId });
1141
+ return;
1142
+ }
1143
+
1144
+ const logToTerminal = [blocklet.mode, b.mode].includes(BLOCKLET_MODES.DEVELOPMENT);
1145
+
1146
+ const startedAt = Date.now();
1147
+
1148
+ await _checkProcessHealthy(b, { minConsecutiveTime, timeout, logToTerminal, isGreen });
1149
+ logger.info('done check component healthy', { processId: b.env.processId, time: Date.now() - startedAt });
1150
+
1151
+ if (setBlockletRunning) {
1152
+ try {
1153
+ await setBlockletRunning(b.meta.did);
1154
+ } catch (error) {
1155
+ logger.error(`Failed to set blocklet as running for DID: ${b.meta.name || b.meta.did}`, { error });
1156
+ }
1157
+ }
1158
+ },
1159
+ { parallel: true }
1160
+ );
1161
+ };
1162
+
1090
1163
  const shouldCheckHealthy = (blocklet) => {
1091
1164
  if (blocklet.meta.group === BlockletGroup.gateway) {
1092
1165
  return false;
@@ -1108,8 +1181,9 @@ const isBlockletPortHealthy = async (blocklet, { minConsecutiveTime = 3000, time
1108
1181
  const { environments } = blocklet;
1109
1182
  const webInterface = (blocklet.meta?.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
1110
1183
  const dockerInterface = (blocklet.meta?.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_DOCKER);
1184
+ const key = webInterface?.port || dockerInterface?.port || 'BLOCKLET_PORT';
1111
1185
 
1112
- let port = environments?.find((e) => e.key === 'BLOCKLET_PORT')?.value;
1186
+ let port = blocklet.greenStatus === BlockletStatus.running ? blocklet.greenPorts?.[key] : blocklet.ports?.[key];
1113
1187
 
1114
1188
  if (!port) {
1115
1189
  const keyPort = webInterface?.port || dockerInterface?.port;
@@ -1354,26 +1428,6 @@ const getRuntimeInfo = async (processId) => {
1354
1428
  };
1355
1429
  };
1356
1430
 
1357
- /**
1358
- *
1359
- *
1360
- * @param {string} dockerName
1361
- * @return {Promise<{ memoryUsage?: number }>}
1362
- */
1363
- const getDockerRuntimeInfoByDockerName = async (dockerName) => {
1364
- try {
1365
- const proc = await promiseSpawn(`docker stats --no-stream --format "{{json .}}" ${dockerName}`, { mute: true });
1366
- const stats = JSON.parse(proc);
1367
- return {
1368
- cpuUsage: stats.CPUPerc ? parseFloat(stats.CPUPerc.replace('%', '').trim()) : 0,
1369
- memoryUsage: stats.MemUsage ? xbytes.parseSize(stats.MemUsage.split('/')[0].trim()) : 0,
1370
- };
1371
- } catch (error) {
1372
- logger.error('Get docker runtime info by docker name failed', { dockerName, error });
1373
- return {};
1374
- }
1375
- };
1376
-
1377
1431
  /**
1378
1432
  * merge services
1379
1433
  * from meta.children[].mountPoints[].services, meta.children[].services
@@ -1449,7 +1503,7 @@ const getUpdateMetaList = (oldBlocklet = {}, newBlocklet = {}) => {
1449
1503
  const res = [];
1450
1504
 
1451
1505
  forEachChildSync(newBlocklet, (b, { id }) => {
1452
- if (b.bundleSource && semver.gt(b.meta.version, oldMap[id])) {
1506
+ if ((b.bundleSource && semver.gt(b.meta.version, oldMap[id])) || process.env.TEST_UPDATE_ALL_BLOCKLET === 'true') {
1453
1507
  res.push({ id, meta: b.meta });
1454
1508
  }
1455
1509
  });
@@ -2254,8 +2308,8 @@ const getBlockletStatus = (blocklet) => {
2254
2308
  return;
2255
2309
  }
2256
2310
 
2257
- if (isRunning(component.status)) {
2258
- runningStatus = component.status;
2311
+ if (isRunning(component.status) || isRunning(component.greenStatus)) {
2312
+ runningStatus = BlockletStatus.running;
2259
2313
  return;
2260
2314
  }
2261
2315
 
@@ -2289,25 +2343,35 @@ const ensureAppPortsNotOccupied = async ({
2289
2343
  states,
2290
2344
  manager,
2291
2345
  checkPortsFn = isPortsOccupiedByOtherProcess,
2346
+ isGreen = false,
2292
2347
  }) => {
2293
2348
  const { did } = blocklet.meta;
2294
- const componentDids = [];
2349
+ const dids = new Set();
2295
2350
  await forEachComponentV2(blocklet, async (b) => {
2296
2351
  try {
2297
2352
  if (shouldSkipComponent(b.meta.did, inputDids)) {
2298
2353
  return;
2299
2354
  }
2300
-
2301
2355
  const { processId } = b.env;
2302
- const { ports } = b;
2356
+ const { ports, greenPorts } = b;
2357
+
2358
+ if (
2359
+ await checkPortsFn({
2360
+ ports: greenPorts || ports,
2361
+ pm2ProcessId: isGreen ? `${processId}-green` : processId,
2362
+ printError: logger.error.bind(logger),
2363
+ })
2364
+ ) {
2365
+ dids.add(b.meta.did);
2366
+ }
2303
2367
  if (
2304
2368
  await checkPortsFn({
2305
2369
  ports,
2306
- pm2ProcessId: processId,
2370
+ pm2ProcessId: !isGreen ? `${processId}-green` : processId,
2307
2371
  printError: logger.error.bind(logger),
2308
2372
  })
2309
2373
  ) {
2310
- componentDids.push(b.meta.did);
2374
+ dids.add(b.meta.did);
2311
2375
  }
2312
2376
  } catch (error) {
2313
2377
  logger.error('Failed to check ports occupied', { error });
@@ -2315,8 +2379,9 @@ const ensureAppPortsNotOccupied = async ({
2315
2379
  });
2316
2380
 
2317
2381
  let newBlocklet = blocklet;
2382
+ const componentDids = Array.from(dids);
2318
2383
  if (componentDids.length) {
2319
- await states.blocklet.refreshBlockletPorts(did, componentDids);
2384
+ await states.blocklet.refreshBlockletPorts(did, componentDids, isGreen);
2320
2385
  logger.info('refresh component ports', { did, componentDids });
2321
2386
  await manager._updateBlockletEnvironment(did);
2322
2387
  newBlocklet = await manager.ensureBlocklet(did);
@@ -2521,6 +2586,16 @@ const copyPackImages = async ({ appDataDir, packDir, packConfig = {} }) => {
2521
2586
  */
2522
2587
  const isDevelopmentMode = (blocklet) => blocklet?.mode === BLOCKLET_MODES.DEVELOPMENT;
2523
2588
 
2589
+ const getHookArgs = (blocklet) => ({
2590
+ output: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? '' : path.join(blocklet.env.logsDir, 'output.log'),
2591
+ error: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? '' : path.join(blocklet.env.logsDir, 'error.log'),
2592
+ timeout:
2593
+ Math.max(
2594
+ get(blocklet, 'meta.timeout.script', 120),
2595
+ ...(blocklet?.children || []).map((child) => child.meta?.timeout?.script || 0)
2596
+ ) * 1000,
2597
+ });
2598
+
2524
2599
  module.exports = {
2525
2600
  updateBlockletFallbackLogo,
2526
2601
  forEachBlocklet,
@@ -2532,6 +2607,7 @@ module.exports = {
2532
2607
  getAppOverwrittenEnvironments,
2533
2608
  getComponentSystemEnvironments,
2534
2609
  getRuntimeEnvironments,
2610
+ getHookArgs,
2535
2611
  validateBlocklet,
2536
2612
  validateBlockletChainInfo,
2537
2613
  fillBlockletConfigs,
@@ -2551,7 +2627,6 @@ module.exports = {
2551
2627
  pruneBlockletBundle,
2552
2628
  getDiskInfo,
2553
2629
  getRuntimeInfo,
2554
- getDockerRuntimeInfoByDockerName,
2555
2630
  mergeMeta,
2556
2631
  getUpdateMetaList,
2557
2632
  getTypeFromInstallParams,
@@ -0,0 +1,17 @@
1
+ // 是否是自定义 docker 镜像, 并且是否挂载了数据卷
2
+ function isDockerOnlySingleInstance(meta = {}) {
3
+ if (!meta.docker) {
4
+ return false;
5
+ }
6
+ // 保留字段, 用于描述 docker 镜像支持多实例运行
7
+ if (meta.docker.allowMultipleInstances) {
8
+ return false;
9
+ }
10
+ if (meta.docker.RunBaseScript && meta.docker.installNodeModules) {
11
+ return false;
12
+ }
13
+
14
+ return !!meta.docker.volume;
15
+ }
16
+
17
+ module.exports = { isDockerOnlySingleInstance };
@@ -0,0 +1,99 @@
1
+ const { ROLES, SERVER_ROLES } = require('@abtnode/constant');
2
+ const { getAppUrl } = require('@blocklet/meta/lib/util');
3
+ const { CustomError } = require('@blocklet/error');
4
+ const { joinURL, withQuery } = require('ufo');
5
+ const { WELLKNOWN_SERVICE_PATH_PREFIX } = require('@abtnode/constant');
6
+ const get = require('lodash/get');
7
+
8
+ const { isAdminPath, getEndpoint } = require('./verify-access-key-user');
9
+
10
+ const MAX_PAGE_SIZE = 100;
11
+
12
+ const formatPaging = (paging) => {
13
+ const { pageSize = 20, page = 1 } = paging || {};
14
+ const newPageSize = Math.max(1, Math.min(MAX_PAGE_SIZE, parseInt(pageSize, 10)));
15
+ const newPage = Math.max(1, parseInt(page, 10));
16
+ return { pageSize: newPageSize, page: newPage };
17
+ };
18
+
19
+ const isAdminUser = (user) => {
20
+ const { role = '' } = user || {};
21
+ if (!role) {
22
+ return false;
23
+ }
24
+ return [ROLES.ADMIN, ROLES.OWNER, SERVER_ROLES.BLOCKLET_ADMIN, SERVER_ROLES.BLOCKLET_OWNER].includes(role);
25
+ };
26
+
27
+ const isOrgOwner = (user, org) => {
28
+ const { did } = user || {};
29
+ if (!org) {
30
+ throw new CustomError(403, 'Org not found');
31
+ }
32
+ return org.ownerDid === did;
33
+ };
34
+
35
+ const isAdmingPath = (context = {}) => {
36
+ try {
37
+ const pathname = getEndpoint(context);
38
+ return isAdminPath(pathname);
39
+ } catch (error) {
40
+ return false;
41
+ }
42
+ };
43
+
44
+ const getOrgInviteLink = (inviteInfo, blocklet) => {
45
+ const { inviteId } = inviteInfo || {};
46
+ const appUrl = getAppUrl(blocklet);
47
+ if (!inviteId || !appUrl) {
48
+ throw new CustomError(500, 'Invitation link creation failed, please try again later');
49
+ }
50
+ return withQuery(joinURL(appUrl, WELLKNOWN_SERVICE_PATH_PREFIX, 'invite'), { inviteId });
51
+ };
52
+
53
+ const createOrgValidators = (blocklet = {}) => {
54
+ function getOrgSettings() {
55
+ const org = get(blocklet, 'settings.org', { enabled: false, maxMemberPerOrg: 100, maxOrgPerUser: 10 });
56
+ return org || { enabled: false, maxMemberPerOrg: 100, maxOrgPerUser: 10 };
57
+ }
58
+
59
+ const org = getOrgSettings();
60
+
61
+ function verifyOrgEnabled() {
62
+ if (!org?.enabled) {
63
+ throw new CustomError(403, 'Org is not enabled');
64
+ }
65
+ return true;
66
+ }
67
+
68
+ function veriftMaxMemberPerOrg(number) {
69
+ verifyOrgEnabled();
70
+ if (number >= org.maxMemberPerOrg) {
71
+ throw new CustomError(403, `The current org can invite up to ${org.maxMemberPerOrg} members`);
72
+ }
73
+ return true;
74
+ }
75
+
76
+ function veriftMaxOrgPerUser(number) {
77
+ verifyOrgEnabled();
78
+ if (number >= org.maxOrgPerUser) {
79
+ throw new CustomError(403, `You can create up to ${org.maxOrgPerUser} orgs`);
80
+ }
81
+ return true;
82
+ }
83
+
84
+ return {
85
+ verifyOrgEnabled,
86
+ veriftMaxMemberPerOrg,
87
+ veriftMaxOrgPerUser,
88
+ getOrgSettings,
89
+ };
90
+ };
91
+
92
+ module.exports = {
93
+ formatPaging,
94
+ isAdminUser,
95
+ isOrgOwner,
96
+ isAdmingPath,
97
+ getOrgInviteLink,
98
+ createOrgValidators,
99
+ };
@@ -0,0 +1,19 @@
1
+ const { Joi } = require('@arcblock/validator');
2
+
3
+ const createOrgInputSchema = Joi.object({
4
+ name: Joi.string().required().trim().min(1).max(20),
5
+ description: Joi.string().optional().allow('').trim().min(1).max(255),
6
+ ownerDid: Joi.DID().optional().allow('').allow(null),
7
+ });
8
+
9
+ /**
10
+ * 只允许更新 name 和 description
11
+ */
12
+ const updateOrgInputSchema = Joi.object({
13
+ id: Joi.string().required(),
14
+ name: Joi.string().required().trim().min(1).max(20),
15
+ description: Joi.string().optional().allow('').trim().min(1).max(255),
16
+ });
17
+
18
+ exports.createOrgInputSchema = createOrgInputSchema;
19
+ exports.updateOrgInputSchema = updateOrgInputSchema;