@elementspanel/epanel-plugin-instance 1.0.2 → 1.0.4

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.
@@ -2142,6 +2142,10 @@ class Instance extends events_1.EventEmitter {
2142
2142
  outputBuffer;
2143
2143
  stopping;
2144
2144
  resourceCleanup;
2145
+ dispatcher;
2146
+ reloading = false;
2147
+ restartPending = false;
2148
+ shuttingDown = false;
2145
2149
  // Track whether the current stop was initiated by a user action (stop/kill command)
2146
2150
  userRequestedStop = false;
2147
2151
  // When initializing an instance, the instance must be initialized through uuid and configuration class, otherwise the instance will be unavailable
@@ -2169,19 +2173,19 @@ class Instance extends events_1.EventEmitter {
2169
2173
  if (!this.isStoppedOrBusy())
2170
2174
  throw new Error((0, i18n_1.$t)("TXT_CODE_instanceConf.cantModifyInstanceType"));
2171
2175
  (0, mcsmanager_common_1.configureEntityParams)(this.config, cfg, "type", String);
2172
- this.forceExec(new dispatcher_1.default());
2176
+ this.forceExec(this.dispatcher ?? new dispatcher_1.default());
2173
2177
  }
2174
2178
  if (cfg?.enableRcon != null && cfg?.enableRcon !== this.config.enableRcon) {
2175
2179
  if (!this.isStoppedOrBusy())
2176
2180
  throw new Error((0, i18n_1.$t)("TXT_CODE_bdfa3457"));
2177
2181
  (0, mcsmanager_common_1.configureEntityParams)(this.config, cfg, "enableRcon", Boolean);
2178
- this.forceExec(new dispatcher_1.default());
2182
+ this.forceExec(this.dispatcher ?? new dispatcher_1.default());
2179
2183
  }
2180
2184
  if (cfg?.processType && cfg?.processType !== this.config.processType) {
2181
2185
  if (!this.isStoppedOrBusy())
2182
2186
  throw new Error((0, i18n_1.$t)("TXT_CODE_instanceConf.cantModifyProcessType"));
2183
2187
  (0, mcsmanager_common_1.configureEntityParams)(this.config, cfg, "processType", String);
2184
- this.forceExec(new dispatcher_1.default());
2188
+ this.forceExec(this.dispatcher ?? new dispatcher_1.default());
2185
2189
  }
2186
2190
  // If the terminal type is changed, the default command must be reset
2187
2191
  if (cfg?.terminalOption?.pty != null &&
@@ -2189,7 +2193,7 @@ class Instance extends events_1.EventEmitter {
2189
2193
  if (!this.isStoppedOrBusy())
2190
2194
  throw new Error((0, i18n_1.$t)("TXT_CODE_instanceConf.cantModifyPtyModel"));
2191
2195
  (0, mcsmanager_common_1.configureEntityParams)(this.config.terminalOption, cfg.terminalOption, "pty", Boolean);
2192
- this.forceExec(new dispatcher_1.default());
2196
+ this.forceExec(this.dispatcher ?? new dispatcher_1.default());
2193
2197
  }
2194
2198
  // Only allow some configuration items to be modified when the server is stopped
2195
2199
  if (this.isStoppedOrBusy() && cfg.terminalOption) {
@@ -2350,10 +2354,19 @@ class Instance extends events_1.EventEmitter {
2350
2354
  });
2351
2355
  this.process = process;
2352
2356
  this.instanceStatus = Instance.STATUS_RUNNING;
2357
+ if (this.shuttingDown) {
2358
+ this.userRequestedStop = true;
2359
+ void Promise.resolve()
2360
+ .then(() => process.kill("SIGKILL"))
2361
+ .catch((error) => log_1.default.error(`Instance ${this.instanceUuid} failed to stop:`, error));
2362
+ return;
2363
+ }
2353
2364
  this.emit("open", this);
2354
2365
  // start all lifecycle tasks
2355
- void this.lifeCycleTaskManager.execLifeCycleTask(1);
2356
- this.startOutputLoop();
2366
+ if (!this.reloading) {
2367
+ void this.lifeCycleTaskManager.execLifeCycleTask(1);
2368
+ this.startOutputLoop();
2369
+ }
2357
2370
  }
2358
2371
  // If the instance performs any operation exception, it must throw an exception through this function
2359
2372
  // trigger failure event
@@ -2393,22 +2406,11 @@ class Instance extends events_1.EventEmitter {
2393
2406
  this.println("ERROR", (0, i18n_1.$t)("TXT_CODE_aae2918f"));
2394
2407
  // Release this stop before a restarted process can emit its own exit.
2395
2408
  this.stopping = undefined;
2396
- if (shouldRestart) {
2397
- const maxAutoRestartCount = this.config.eventTask.autoRestartMaxTimes;
2398
- if (maxAutoRestartCount === -1 || this.autoRestartCount < maxAutoRestartCount) {
2399
- this.execPreset("start")
2400
- .then(() => {
2401
- this.autoRestartCount++;
2402
- this.println((0, i18n_1.$t)("TXT_CODE_instanceConf.info"), (0, i18n_1.$t)("TXT_CODE_instanceConf.autoRestart"));
2403
- this.emit("autoRestarted", { count: this.autoRestartCount });
2404
- })
2405
- .catch((err) => {
2406
- this.println((0, i18n_1.$t)("TXT_CODE_instanceConf.error"), (0, i18n_1.$t)("TXT_CODE_instanceConf.autoRestartErr", { err }));
2407
- });
2408
- }
2409
- else {
2410
- this.println((0, i18n_1.$t)("TXT_CODE_instanceConf.error"), (0, i18n_1.$t)("TXT_CODE_894b8e52"));
2411
- }
2409
+ if (shouldRestart && !this.shuttingDown) {
2410
+ if (this.reloading)
2411
+ this.restartPending = true;
2412
+ else
2413
+ this.restartAfterCrash();
2412
2414
  }
2413
2415
  }).finally(() => {
2414
2416
  if (this.stopping === operation)
@@ -2417,6 +2419,60 @@ class Instance extends events_1.EventEmitter {
2417
2419
  this.stopping = operation;
2418
2420
  return operation;
2419
2421
  }
2422
+ restartAfterCrash() {
2423
+ if (this.shuttingDown)
2424
+ return;
2425
+ const maxAutoRestartCount = this.config.eventTask.autoRestartMaxTimes;
2426
+ if (maxAutoRestartCount !== -1 && this.autoRestartCount >= maxAutoRestartCount) {
2427
+ this.println((0, i18n_1.$t)("TXT_CODE_instanceConf.error"), (0, i18n_1.$t)("TXT_CODE_894b8e52"));
2428
+ return;
2429
+ }
2430
+ void this.execPreset("start")
2431
+ .then(() => {
2432
+ this.autoRestartCount++;
2433
+ this.println((0, i18n_1.$t)("TXT_CODE_instanceConf.info"), (0, i18n_1.$t)("TXT_CODE_instanceConf.autoRestart"));
2434
+ this.emit("autoRestarted", { count: this.autoRestartCount });
2435
+ })
2436
+ .catch((err) => {
2437
+ this.println((0, i18n_1.$t)("TXT_CODE_instanceConf.error"), (0, i18n_1.$t)("TXT_CODE_instanceConf.autoRestartErr", { err }));
2438
+ });
2439
+ }
2440
+ /** Keep the process and its pipe readers alive while plugin services change. */
2441
+ async suspendForReload() {
2442
+ this.stopOutputLoop();
2443
+ this.reloading = true;
2444
+ this.watchers.clear();
2445
+ await this.lifeCycleTaskManager.clearLifeCycleTask();
2446
+ }
2447
+ async resumeAfterReload(dispatcher) {
2448
+ if (this.shuttingDown)
2449
+ return;
2450
+ await this.stopping;
2451
+ if (this.shuttingDown)
2452
+ return;
2453
+ this.dispatcher = dispatcher;
2454
+ await this.forceExec(dispatcher);
2455
+ if (this.shuttingDown)
2456
+ return;
2457
+ this.reloading = false;
2458
+ this.flushOutputBuffer();
2459
+ if (this.status() === Instance.STATUS_RUNNING) {
2460
+ await this.lifeCycleTaskManager.execLifeCycleTask(1);
2461
+ if (!this.shuttingDown && this.status() === Instance.STATUS_RUNNING) {
2462
+ this.startOutputLoop(false);
2463
+ }
2464
+ }
2465
+ else if (this.status() === Instance.STATUS_STOP && this.restartPending) {
2466
+ this.restartPending = false;
2467
+ this.restartAfterCrash();
2468
+ }
2469
+ }
2470
+ beginShutdown() {
2471
+ this.restartPending = false;
2472
+ this.reloading = false;
2473
+ this.shuttingDown = true;
2474
+ this.ignoreEventTaskOnce();
2475
+ }
2420
2476
  ignoreEventTaskOnce() {
2421
2477
  if (this.config.eventTask)
2422
2478
  this.config.eventTask.ignore = true;
@@ -2430,11 +2486,14 @@ class Instance extends events_1.EventEmitter {
2430
2486
  // custom output method, formatting
2431
2487
  println(level, text) {
2432
2488
  const str = `[${level}] ${text}\n`;
2433
- this.emit("data", str);
2489
+ this.print(str);
2434
2490
  }
2435
2491
  // custom output method
2436
2492
  print(data) {
2437
- this.emit("data", data);
2493
+ if (this.reloading)
2494
+ this.pushOutput(String(data));
2495
+ else
2496
+ this.emit("data", data);
2438
2497
  }
2439
2498
  // Release resources (mainly release process-related resources)
2440
2499
  releaseResources() {
@@ -2572,6 +2631,8 @@ class Instance extends events_1.EventEmitter {
2572
2631
  this.outputBuffer.pushLog(data);
2573
2632
  }
2574
2633
  flushOutputBuffer(shouldClear = false) {
2634
+ if (this.reloading)
2635
+ return;
2575
2636
  const { items, wasDeleted } = this.outputBuffer.getCache();
2576
2637
  if (!items.length)
2577
2638
  return;
@@ -2581,8 +2642,11 @@ class Instance extends events_1.EventEmitter {
2581
2642
  this.outputBuffer.clear();
2582
2643
  this.emit("data", items.join(""));
2583
2644
  }
2584
- startOutputLoop() {
2585
- this.outputBuffer = new string_cache_1.CircularBuffer(config_1.globalConfiguration.config.outputBufferSize || 256);
2645
+ startOutputLoop(resetBuffer = true) {
2646
+ if (this.outputLoopTask)
2647
+ return;
2648
+ if (resetBuffer)
2649
+ this.outputBuffer = new string_cache_1.CircularBuffer(config_1.globalConfiguration.config.outputBufferSize || 256);
2586
2650
  this.outputLoopTask = setInterval(() => {
2587
2651
  this.flushOutputBuffer();
2588
2652
  }, 50);
@@ -2927,7 +2991,18 @@ function apply(ctx) {
2927
2991
  ctx.plugin(registries_1.LifecycleService);
2928
2992
  ctx.plugin(registries_1.PresetsService);
2929
2993
  ctx.plugin(registries_1.SchedulesService);
2930
- (0, runtime_1.setPluginContext)(ctx);
2994
+ const runtime = (0, runtime_1.setPluginContext)(ctx);
2995
+ let preserving = false;
2996
+ let subsystem;
2997
+ let disposeSchedules;
2998
+ ctx.on("plugins/reloading", async () => {
2999
+ const inventory = ctx.get("plugins")?.inventory();
3000
+ preserving = inventory?.some((plugin) => plugin.id === "instance" && plugin.enabled) ?? true;
3001
+ if (preserving) {
3002
+ disposeSchedules?.();
3003
+ await subsystem?.suspendForReload();
3004
+ }
3005
+ });
2931
3006
  (0, version_adapter_1.migrateConfig)(ctx);
2932
3007
  void (0, steam_cmd_1.initSteamCmd)(ctx);
2933
3008
  // The registry services above are created as child plugins, so their values
@@ -2946,6 +3021,57 @@ function apply(ctx) {
2946
3021
  const { commandStringToArray } = __webpack_require__("../../plugins/instance/daemon/src/backend/entity/commands/base/command_parser.ts");
2947
3022
  const { default: FunctionDispatcher } = __webpack_require__("../../plugins/instance/daemon/src/backend/entity/commands/dispatcher.ts");
2948
3023
  const { DockerManager } = __webpack_require__("../../plugins/instance/daemon/src/backend/service/docker_service.ts");
3024
+ const { default: InstanceControlSubsystem } = __webpack_require__("../../plugins/instance/daemon/src/backend/service/system_instance_control.ts");
3025
+ subsystem = InstanceSubsystem;
3026
+ disposeSchedules = () => InstanceControlSubsystem.dispose();
3027
+ const previous = runtime.handover;
3028
+ let recovering = Boolean(previous);
3029
+ let disposeInstanceEvents;
3030
+ let shuttingDown;
3031
+ const config = instanceCtx.settings.config;
3032
+ const shutdown = () => (shuttingDown ??= (async () => {
3033
+ for (const instance of InstanceSubsystem.instances.values())
3034
+ instance.beginShutdown();
3035
+ try {
3036
+ if (config.enableSoftShutdown) {
3037
+ await InstanceSubsystem.softExit(Boolean(config.softShutdownSkipDocker), Number(config.softShutdownWaitSeconds) || 10);
3038
+ }
3039
+ else {
3040
+ await InstanceSubsystem.exit(true);
3041
+ }
3042
+ }
3043
+ catch (error) {
3044
+ instanceCtx.logger.error("Failed to stop application instances:", error);
3045
+ }
3046
+ })());
3047
+ const retain = () => {
3048
+ runtime.handover = {
3049
+ instances: [...InstanceSubsystem.instances.values()],
3050
+ shutdown,
3051
+ release: () => InstanceSubsystem.releaseAfterReload()
3052
+ };
3053
+ };
3054
+ // Own the processes before any asynchronous initialization. A failed Java
3055
+ // manager or replacement must leave them available for another attempt.
3056
+ if (previous) {
3057
+ InstanceControlSubsystem.dispose();
3058
+ InstanceSubsystem.adoptInstances(previous.instances);
3059
+ retain();
3060
+ previous.release();
3061
+ }
3062
+ instanceCtx.on("dispose", async () => {
3063
+ await disposeInstanceEvents?.();
3064
+ InstanceControlSubsystem.dispose();
3065
+ router_1.routerApp.dispose();
3066
+ if ((preserving || recovering) && !runtime.stopping && ctx.root.scope.isActive) {
3067
+ await InstanceSubsystem.suspendForReload();
3068
+ retain();
3069
+ }
3070
+ else {
3071
+ await shutdown();
3072
+ }
3073
+ InstanceSubsystem.detachListeners();
3074
+ });
2949
3075
  const services = {
2950
3076
  subsystem: InstanceSubsystem,
2951
3077
  Instance,
@@ -2987,11 +3113,10 @@ function apply(ctx) {
2987
3113
  const { registerInstanceEvents } = __webpack_require__("../../plugins/instance/daemon/src/backend/routers/instance_event_router.ts");
2988
3114
  __webpack_require__("../../plugins/instance/daemon/src/backend/routers/schedule_router.ts");
2989
3115
  __webpack_require__("../../plugins/instance/daemon/src/backend/routers/environment_router.ts");
2990
- const disposeInstanceEvents = registerInstanceEvents();
2991
- const { default: InstanceControlSubsystem } = __webpack_require__("../../plugins/instance/daemon/src/backend/service/system_instance_control.ts");
2992
- instanceCtx.on("dispose", () => router_1.routerApp.dispose());
3116
+ disposeInstanceEvents = registerInstanceEvents();
2993
3117
  try {
2994
- InstanceSubsystem.loadInstances();
3118
+ if (!previous)
3119
+ InstanceSubsystem.loadInstances();
2995
3120
  instanceCtx.logger.info(`Loaded ${InstanceSubsystem.getInstances().length} application instances.`);
2996
3121
  }
2997
3122
  catch (error) {
@@ -3018,22 +3143,24 @@ function apply(ctx) {
3018
3143
  dockerPlatforms
3019
3144
  };
3020
3145
  });
3021
- instanceCtx.on("dispose", async () => {
3022
- disposeInstanceEvents();
3023
- InstanceControlSubsystem.dispose();
3024
- try {
3025
- const config = instanceCtx.settings.config;
3026
- if (config.enableSoftShutdown) {
3027
- await InstanceSubsystem.softExit(Boolean(config.softShutdownSkipDocker), Number(config.softShutdownWaitSeconds) || 10);
3028
- }
3029
- else {
3030
- await InstanceSubsystem.exit(true);
3031
- }
3032
- }
3033
- catch (error) {
3034
- instanceCtx.logger.error("Failed to stop application instances:", error);
3035
- }
3036
- });
3146
+ if (previous) {
3147
+ let resuming;
3148
+ const resume = () => (resuming ??= (async () => {
3149
+ if (runtime.stopping || !instanceCtx.scope.isActive || !ctx.root.scope.isActive)
3150
+ return;
3151
+ await InstanceSubsystem.resumeAfterReload();
3152
+ if (runtime.stopping || !instanceCtx.scope.isActive || !ctx.root.scope.isActive)
3153
+ return;
3154
+ InstanceControlSubsystem.loadSchedules();
3155
+ recovering = false;
3156
+ runtime.handover = undefined;
3157
+ })());
3158
+ // Rebuild commands after other plugins have registered their presets and
3159
+ // lifecycle tasks, and before the replacement server starts listening.
3160
+ instanceCtx.on("plugins/reloaded", resume);
3161
+ // Also allow recovery by enabling/retrying only the instance plugin.
3162
+ instanceCtx.on("ready", resume);
3163
+ }
3037
3164
  });
3038
3165
  }
3039
3166
 
@@ -5218,29 +5345,41 @@ const fs_extra_1 = __importDefault(__webpack_require__("fs-extra"));
5218
5345
  const ctx_1 = __importDefault(__webpack_require__("../../plugins/instance/daemon/src/backend/entity/ctx.ts"));
5219
5346
  const protocol = __importStar(__webpack_require__("../../plugins/instance/daemon/src/backend/service/protocol.ts"));
5220
5347
  const system_instance_1 = __importDefault(__webpack_require__("../../plugins/instance/daemon/src/backend/service/system_instance.ts"));
5348
+ const log_1 = __importDefault(__webpack_require__("../../plugins/instance/daemon/src/backend/service/log.ts"));
5221
5349
  const MAX_LOG_SIZE = 512;
5222
- const buffer = new Map();
5223
5350
  function registerInstanceEvents() {
5224
- const flushTimer = setInterval(() => {
5351
+ const buffer = new Map();
5352
+ const pending = new Map();
5353
+ const flush = async () => {
5225
5354
  buffer.forEach((buf, instanceUuid) => {
5226
5355
  if (!buf || !instanceUuid)
5227
5356
  return;
5228
- const logFilePath = path_1.default.join(system_instance_1.default.LOG_DIR, `${instanceUuid}.log`);
5229
- if (!fs_extra_1.default.existsSync(system_instance_1.default.LOG_DIR))
5230
- fs_extra_1.default.mkdirsSync(system_instance_1.default.LOG_DIR);
5231
- try {
5232
- const fileInfo = fs_extra_1.default.statSync(logFilePath);
5233
- if (fileInfo && fileInfo.size > 1024 * MAX_LOG_SIZE)
5234
- fs_extra_1.default.removeSync(logFilePath);
5235
- }
5236
- catch (err) {
5237
- // A concurrently removed log file can be recreated on the next flush.
5238
- }
5239
- fs_extra_1.default.writeFile(logFilePath, buf, { encoding: "utf-8", flag: "a" }, () => {
5240
- buffer.set(instanceUuid, "");
5357
+ buffer.delete(instanceUuid);
5358
+ const work = (pending.get(instanceUuid) ?? Promise.resolve())
5359
+ .then(async () => {
5360
+ const logFilePath = path_1.default.join(system_instance_1.default.LOG_DIR, `${instanceUuid}.log`);
5361
+ await fs_extra_1.default.ensureDir(system_instance_1.default.LOG_DIR);
5362
+ try {
5363
+ const fileInfo = await fs_extra_1.default.stat(logFilePath);
5364
+ if (fileInfo.size > 1024 * MAX_LOG_SIZE)
5365
+ await fs_extra_1.default.remove(logFilePath);
5366
+ }
5367
+ catch (err) {
5368
+ if (err.code !== "ENOENT")
5369
+ throw err;
5370
+ }
5371
+ await fs_extra_1.default.writeFile(logFilePath, buf, { encoding: "utf-8", flag: "a" });
5372
+ })
5373
+ .catch((error) => log_1.default.error(`Failed to write instance ${instanceUuid} output:`, error))
5374
+ .finally(() => {
5375
+ if (pending.get(instanceUuid) === work)
5376
+ pending.delete(instanceUuid);
5241
5377
  });
5378
+ pending.set(instanceUuid, work);
5242
5379
  });
5243
- }, 500);
5380
+ await Promise.all(pending.values());
5381
+ };
5382
+ const flushTimer = setInterval(() => void flush(), 500);
5244
5383
  const outputLog = (instanceUuid, text) => {
5245
5384
  const buf = (buffer.get(instanceUuid) ?? "") + text;
5246
5385
  if (buf.length > 1024 * 1024)
@@ -5301,13 +5440,14 @@ function registerInstanceEvents() {
5301
5440
  });
5302
5441
  };
5303
5442
  system_instance_1.default.on("autoRestarted", onAutoRestarted);
5304
- return () => {
5443
+ return async () => {
5305
5444
  clearInterval(flushTimer);
5306
5445
  system_instance_1.default.off("data", onData);
5307
5446
  system_instance_1.default.off("exit", onExit);
5308
5447
  system_instance_1.default.off("open", onOpen);
5309
5448
  system_instance_1.default.off("failure", onFailure);
5310
5449
  system_instance_1.default.off("autoRestarted", onAutoRestarted);
5450
+ await flush();
5311
5451
  buffer.clear();
5312
5452
  };
5313
5453
  }
@@ -5399,17 +5539,45 @@ exports.core = core;
5399
5539
  const os_1 = __importDefault(__webpack_require__("os"));
5400
5540
  const path_1 = __importDefault(__webpack_require__("path"));
5401
5541
  const mcsmanager_common_1 = __webpack_require__("../common/src/index.ts");
5402
- let context;
5542
+ const INSTANCE_RUNTIME = Symbol.for("elementspanel.daemon.instance-runtime");
5543
+ let runtime;
5403
5544
  function setPluginContext(value) {
5404
- context = value;
5545
+ // Retained process callbacks can come from an evicted module. Give every
5546
+ // generation the same bridge, owned by this daemon rather than the module.
5547
+ const host = value.root;
5548
+ runtime = host[INSTANCE_RUNTIME];
5549
+ if (!runtime) {
5550
+ const state = {
5551
+ context: value,
5552
+ settings: value.settings,
5553
+ storage: value.storage,
5554
+ stopping: false
5555
+ };
5556
+ host[INSTANCE_RUNTIME] = runtime = state;
5557
+ host.on("dispose", async () => {
5558
+ state.stopping = true;
5559
+ // A replacement may fail before it can claim the retained processes.
5560
+ // The daemon still owns them and must stop them during real shutdown.
5561
+ await state.handover?.shutdown();
5562
+ state.handover = undefined;
5563
+ });
5564
+ }
5565
+ if (runtime.stopping)
5566
+ throw new Error("The daemon is shutting down.");
5567
+ runtime.context = value;
5568
+ runtime.settings = value.settings;
5569
+ runtime.storage = value.storage;
5570
+ return runtime;
5405
5571
  }
5406
5572
  function core() {
5407
- if (!context)
5573
+ if (!runtime)
5408
5574
  throw new Error("The instance plugin has not been initialized yet.");
5409
- return context;
5575
+ return runtime.context;
5410
5576
  }
5411
5577
  function required(name) {
5412
- const value = core().get(name);
5578
+ const value = core().get(name) ??
5579
+ // Process exit and shutdown can happen between foundation generations.
5580
+ (name === "settings" ? runtime?.settings : name === "storage" ? runtime?.storage : undefined);
5413
5581
  if (value === undefined)
5414
5582
  throw new Error(`Daemon service is unavailable: ${name}`);
5415
5583
  return value;
@@ -7195,6 +7363,9 @@ class InstanceSubsystem extends events_1.default {
7195
7363
  instances = new Map();
7196
7364
  instanceStream = new mcsmanager_common_1.InstanceStreamListener();
7197
7365
  instanceDataDir = "";
7366
+ startupCancelled = false;
7367
+ takeover;
7368
+ instanceListeners = new Map();
7198
7369
  constructor() {
7199
7370
  super();
7200
7371
  // init instance default install path
@@ -7214,6 +7385,8 @@ class InstanceSubsystem extends events_1.default {
7214
7385
  async autoStart() {
7215
7386
  await (0, sleep_1.sleep)(1000 * 5);
7216
7387
  for (const instance of this.instances.values()) {
7388
+ if (this.startupCancelled)
7389
+ return;
7217
7390
  if (instance.config.eventTask.autoStart && instance.status() == instance_1.default.STATUS_STOP) {
7218
7391
  instance
7219
7392
  .execPreset("start")
@@ -7236,6 +7409,7 @@ class InstanceSubsystem extends events_1.default {
7236
7409
  }
7237
7410
  // init all instances from local files
7238
7411
  loadInstances() {
7412
+ this.startupCancelled = false;
7239
7413
  const instanceConfigs = system_storage_1.default.list("InstanceConfig");
7240
7414
  instanceConfigs.forEach((uuid) => {
7241
7415
  if (uuid === this.GLOBAL_INSTANCE_UUID)
@@ -7290,15 +7464,14 @@ class InstanceSubsystem extends events_1.default {
7290
7464
  processType: globalConfig.processType
7291
7465
  }, true, // allow persistence
7292
7466
  this.GLOBAL_INSTANCE_UUID);
7293
- (0, takeover_container_1.default)()
7294
- .catch((error) => {
7467
+ this.takeover = (0, takeover_container_1.default)(() => !this.startupCancelled).catch((error) => {
7295
7468
  const reason = error.message || error;
7296
7469
  if (typeof reason == "string" && reason.includes("connect ENOENT")) {
7297
7470
  return;
7298
7471
  }
7299
7472
  log_1.default.error(`${(0, i18n_1.$t)("TXT_CODE_8d4c8f7e")}: ${reason}`);
7300
- })
7301
- .finally(() => this.autoStart());
7473
+ });
7474
+ void this.takeover.then(() => this.autoStart());
7302
7475
  }
7303
7476
  createInstance(cfg, persistence = true, uuid) {
7304
7477
  const newUuid = uuid || (0, uuid_1.v4)().replace(/-/gim, "");
@@ -7325,37 +7498,74 @@ class InstanceSubsystem extends events_1.default {
7325
7498
  }
7326
7499
  this.instances.set(instance.instanceUuid, instance);
7327
7500
  // Dynamically monitor the newly added instance output stream and pass it to its own event stream
7328
- instance.on("data", (...arr) => {
7501
+ const onData = (...arr) => {
7329
7502
  this.emit("data", instance.instanceUuid, ...arr);
7330
- });
7331
- instance.on("exit", (exitData) => {
7503
+ };
7504
+ const onExit = (exitData) => {
7332
7505
  this.emit("exit", {
7333
7506
  instanceUuid: instance.instanceUuid,
7334
7507
  instanceName: instance.config.nickname,
7335
7508
  exitCode: exitData.code,
7336
7509
  isCrash: exitData.isCrash
7337
7510
  });
7338
- });
7339
- instance.on("open", (...arr) => {
7511
+ };
7512
+ const onOpen = (...arr) => {
7340
7513
  this.emit("open", {
7341
7514
  instanceUuid: instance.instanceUuid,
7342
7515
  instanceName: instance.config.nickname
7343
7516
  }, ...arr);
7344
- });
7345
- instance.on("failure", (...arr) => {
7517
+ };
7518
+ const onFailure = (...arr) => {
7346
7519
  this.emit("failure", {
7347
7520
  instanceUuid: instance.instanceUuid,
7348
7521
  instanceName: instance.config.nickname
7349
7522
  }, ...arr);
7350
- });
7351
- instance.on("autoRestarted", (data) => {
7523
+ };
7524
+ const onAutoRestarted = (data) => {
7352
7525
  this.emit("autoRestarted", {
7353
7526
  instanceUuid: instance.instanceUuid,
7354
7527
  instanceName: instance.config.nickname,
7355
7528
  restartCount: data.count
7356
7529
  });
7530
+ };
7531
+ instance.on("data", onData);
7532
+ instance.on("exit", onExit);
7533
+ instance.on("open", onOpen);
7534
+ instance.on("failure", onFailure);
7535
+ instance.on("autoRestarted", onAutoRestarted);
7536
+ this.instanceListeners.set(instance, () => {
7537
+ instance.off("data", onData);
7538
+ instance.off("exit", onExit);
7539
+ instance.off("open", onOpen);
7540
+ instance.off("failure", onFailure);
7541
+ instance.off("autoRestarted", onAutoRestarted);
7357
7542
  });
7358
7543
  }
7544
+ async suspendForReload() {
7545
+ this.startupCancelled = true;
7546
+ await Promise.all([...this.instances.values()].map((instance) => instance.suspendForReload()));
7547
+ await this.takeover;
7548
+ }
7549
+ /** Transfer the existing owners, including ordinary child/PTY/Docker handles. */
7550
+ adoptInstances(instances) {
7551
+ this.startupCancelled = true;
7552
+ for (const instance of instances)
7553
+ this.addInstance(instance);
7554
+ }
7555
+ async resumeAfterReload() {
7556
+ for (const instance of this.instances.values()) {
7557
+ await instance.resumeAfterReload(new dispatcher_1.default());
7558
+ }
7559
+ }
7560
+ detachListeners() {
7561
+ for (const dispose of this.instanceListeners.values())
7562
+ dispose();
7563
+ this.instanceListeners.clear();
7564
+ }
7565
+ releaseAfterReload() {
7566
+ this.detachListeners();
7567
+ this.instances.clear();
7568
+ }
7359
7569
  async removeInstance(instanceUuid, deleteFile) {
7360
7570
  const instance = this.getInstance(instanceUuid);
7361
7571
  if (instance) {
@@ -7374,6 +7584,8 @@ class InstanceSubsystem extends events_1.default {
7374
7584
  system_instance_control_1.default.deleteInstanceAllTask(instanceUuid);
7375
7585
  system_storage_1.default.delete("InstanceConfig", instanceUuid);
7376
7586
  await instance.destroy();
7587
+ this.instanceListeners.get(instance)?.();
7588
+ this.instanceListeners.delete(instance);
7377
7589
  this.instances.delete(instanceUuid);
7378
7590
  return true;
7379
7591
  }
@@ -7415,8 +7627,14 @@ class InstanceSubsystem extends events_1.default {
7415
7627
  return this.instances.has(instanceUuid);
7416
7628
  }
7417
7629
  async exitInstance(instance, force = true) {
7418
- if (!this.isGlobalInstance(instance))
7419
- system_storage_1.default.store("InstanceConfig", instance.instanceUuid, instance.config);
7630
+ if (!this.isGlobalInstance(instance)) {
7631
+ try {
7632
+ system_storage_1.default.store("InstanceConfig", instance.instanceUuid, instance.config);
7633
+ }
7634
+ catch (error) {
7635
+ log_1.default.error(`Failed to save instance ${instance.instanceUuid} before shutdown:`, error);
7636
+ }
7637
+ }
7420
7638
  if (instance.status() === instance_1.default.STATUS_BUSY) {
7421
7639
  log_1.default.info(`Killing ${instance.config.nickname} (${instance.instanceUuid})...`);
7422
7640
  await instance.execPreset("kill");
@@ -7606,6 +7824,11 @@ class InstanceControlSubsystem {
7606
7824
  taskMap = new Map();
7607
7825
  taskJobMap = new Map();
7608
7826
  constructor() {
7827
+ this.loadSchedules();
7828
+ }
7829
+ loadSchedules() {
7830
+ if (this.taskMap.size)
7831
+ return;
7609
7832
  // Initialize all persistent data and load into memory one by one
7610
7833
  system_storage_1.default.list("TaskConfig").forEach((uuid) => {
7611
7834
  try {
@@ -7876,7 +8099,7 @@ async function killContainer(containerInfo) {
7876
8099
  container.kill().catch(() => { });
7877
8100
  container.remove().catch(() => { });
7878
8101
  }
7879
- async function takeoverContainer() {
8102
+ async function takeoverContainer(isActive = () => true) {
7880
8103
  const docker = new docker_service_1.DefaultDocker();
7881
8104
  const containers = new Map();
7882
8105
  (await docker.listContainers()).forEach((containerInfo) => {
@@ -7890,10 +8113,12 @@ async function takeoverContainer() {
7890
8113
  containers.set(label, containerInfo);
7891
8114
  });
7892
8115
  for (const item of containers) {
8116
+ if (!isActive())
8117
+ return;
7893
8118
  const [uuid, container] = item;
7894
8119
  const instance = system_instance_1.default.getInstance(uuid);
7895
8120
  try {
7896
- if (instance && instance.config.processType == "docker") {
8121
+ if (instance && !instance.process && instance.config.processType == "docker") {
7897
8122
  await instance.forceExec(new docker_takeover_1.default(container));
7898
8123
  }
7899
8124
  }
@@ -9449,9 +9674,9 @@ function recordFromPackage(pluginId, name, source, packageRoot, metadata, side)
9449
9674
  /**
9450
9675
  * Every npm plugin the runtime can load for a side, built-in and project-installed.
9451
9676
  *
9452
- * Built-in mappings win over a project mapping for the same id: a third-party
9453
- * package must not shadow a plugin that ships with the runtime. A project package
9454
- * that does not describe the requested side is skipped instead of failing, which
9677
+ * Built-in package names win over a project mapping for the same id. An explicit
9678
+ * project dependency on that same package selects the user's installed version.
9679
+ * A project package that does not describe the requested side is skipped instead of failing, which
9455
9680
  * is what lets a two-sided project share one dependency list between sides.
9456
9681
  */
9457
9682
  function listNpmPlugins(side, options = {}) {
@@ -9488,7 +9713,10 @@ function listNpmPlugins(side, options = {}) {
9488
9713
  continue;
9489
9714
  if (dependenciesOf(host)[name] === undefined)
9490
9715
  continue;
9491
- mappings.set(pluginId, { name, source: "bundled" });
9716
+ mappings.set(pluginId, {
9717
+ name,
9718
+ source: dependenciesOf(project)[name] === undefined ? "bundled" : "project"
9719
+ });
9492
9720
  }
9493
9721
  const hostDependencies = dependenciesOf(host);
9494
9722
  const projectDependencies = dependenciesOf(project);
@@ -9663,9 +9891,9 @@ async function installNpmPlugin(options) {
9663
9891
  if (!host)
9664
9892
  throw new Error(`Missing runtime package metadata: ${packageRoot}`);
9665
9893
  const hostId = Object.entries(mappingsOf(host)).find(([, value]) => value === name && dependenciesOf(host)[name] !== undefined)?.[0];
9666
- const targetRoot = hostId ? packageRoot : projectRoot;
9667
- if (hostId && isEssentialNpmPlugin(hostId))
9668
- throw new Error(`The essential plugin "${hostId}" cannot be replaced while running.`);
9894
+ const targetRoot = hostId && dependenciesOf(readProjectManifest(projectRoot))[name] === undefined
9895
+ ? packageRoot
9896
+ : projectRoot;
9669
9897
  if (options.pluginId &&
9670
9898
  mappingsOf(host)[options.pluginId] &&
9671
9899
  dependenciesOf(host)[String(mappingsOf(host)[options.pluginId])] !== undefined &&
@@ -9712,7 +9940,7 @@ async function installNpmPlugin(options) {
9712
9940
  const mapped = mappingsOf(readProjectManifest(targetRoot))[pluginId];
9713
9941
  if (mapped !== undefined && mapped !== name)
9714
9942
  throw new Error(`Plugin id ${pluginId} is already installed as ${String(mapped)}.`);
9715
- const record = recordFromPackage(pluginId, name, hostId ? "bundled" : "project", entry.root, entry.metadata, side);
9943
+ const record = recordFromPackage(pluginId, name, targetRoot === packageRoot ? "bundled" : "project", entry.root, entry.metadata, side);
9716
9944
  if (!record)
9717
9945
  throw new Error(`The package ${name} does not provide a ${side} plugin named ${pluginId}.`);
9718
9946
  if (/^[0-9]/.test(version) && record.version !== version)
@@ -9795,7 +10023,9 @@ async function uninstallNpmPlugin(pluginId, options = {}) {
9795
10023
  return withProjectLock(projectRoot, async () => {
9796
10024
  const host = readJson(path_1.default.join(packageRoot, "package.json"));
9797
10025
  const bundledName = mappingsOf(host)[pluginId];
9798
- const targetRoot = typeof bundledName === "string" && dependenciesOf(host)[bundledName] !== undefined
10026
+ const targetRoot = typeof bundledName === "string" &&
10027
+ dependenciesOf(host)[bundledName] !== undefined &&
10028
+ dependenciesOf(readProjectManifest(projectRoot))[bundledName] === undefined
9799
10029
  ? packageRoot
9800
10030
  : projectRoot;
9801
10031
  const manifest = readProjectManifest(targetRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementspanel/epanel-plugin-instance",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Application instance and Java runtime management.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Csituka_D <694428809@qq.com> (https://github.com/JessDaodao)",
@@ -50,8 +50,8 @@
50
50
  }
51
51
  },
52
52
  "peerDependencies": {
53
- "@elementspanel/panel": "1.0.1",
54
- "@elementspanel/daemon": "1.0.1"
53
+ "@elementspanel/panel": "^1.0.2",
54
+ "@elementspanel/daemon": "^1.0.2"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "@elementspanel/panel": {
@@ -10536,9 +10536,9 @@ function recordFromPackage(pluginId, name, source, packageRoot, metadata, side)
10536
10536
  /**
10537
10537
  * Every npm plugin the runtime can load for a side, built-in and project-installed.
10538
10538
  *
10539
- * Built-in mappings win over a project mapping for the same id: a third-party
10540
- * package must not shadow a plugin that ships with the runtime. A project package
10541
- * that does not describe the requested side is skipped instead of failing, which
10539
+ * Built-in package names win over a project mapping for the same id. An explicit
10540
+ * project dependency on that same package selects the user's installed version.
10541
+ * A project package that does not describe the requested side is skipped instead of failing, which
10542
10542
  * is what lets a two-sided project share one dependency list between sides.
10543
10543
  */
10544
10544
  function listNpmPlugins(side, options = {}) {
@@ -10576,7 +10576,10 @@ function listNpmPlugins(side, options = {}) {
10576
10576
  continue;
10577
10577
  if (dependenciesOf(host)[name] === undefined)
10578
10578
  continue;
10579
- mappings.set(pluginId, { name, source: "bundled" });
10579
+ mappings.set(pluginId, {
10580
+ name,
10581
+ source: dependenciesOf(project)[name] === undefined ? "bundled" : "project"
10582
+ });
10580
10583
  }
10581
10584
  const hostDependencies = dependenciesOf(host);
10582
10585
  const projectDependencies = dependenciesOf(project);
@@ -10755,9 +10758,9 @@ async function installNpmPlugin(options) {
10755
10758
  if (!host)
10756
10759
  throw new Error(`Missing runtime package metadata: ${packageRoot}`);
10757
10760
  const hostId = (_a = Object.entries(mappingsOf(host)).find(([, value]) => value === name && dependenciesOf(host)[name] !== undefined)) === null || _a === void 0 ? void 0 : _a[0];
10758
- const targetRoot = hostId ? packageRoot : projectRoot;
10759
- if (hostId && isEssentialNpmPlugin(hostId))
10760
- throw new Error(`The essential plugin "${hostId}" cannot be replaced while running.`);
10761
+ const targetRoot = hostId && dependenciesOf(readProjectManifest(projectRoot))[name] === undefined
10762
+ ? packageRoot
10763
+ : projectRoot;
10761
10764
  if (options.pluginId &&
10762
10765
  mappingsOf(host)[options.pluginId] &&
10763
10766
  dependenciesOf(host)[String(mappingsOf(host)[options.pluginId])] !== undefined &&
@@ -10804,7 +10807,7 @@ async function installNpmPlugin(options) {
10804
10807
  const mapped = mappingsOf(readProjectManifest(targetRoot))[pluginId];
10805
10808
  if (mapped !== undefined && mapped !== name)
10806
10809
  throw new Error(`Plugin id ${pluginId} is already installed as ${String(mapped)}.`);
10807
- const record = recordFromPackage(pluginId, name, hostId ? "bundled" : "project", entry.root, entry.metadata, side);
10810
+ const record = recordFromPackage(pluginId, name, targetRoot === packageRoot ? "bundled" : "project", entry.root, entry.metadata, side);
10808
10811
  if (!record)
10809
10812
  throw new Error(`The package ${name} does not provide a ${side} plugin named ${pluginId}.`);
10810
10813
  if (/^[0-9]/.test(version) && record.version !== version)
@@ -10888,7 +10891,9 @@ async function uninstallNpmPlugin(pluginId, options = {}) {
10888
10891
  var _a, _b;
10889
10892
  const host = readJson(path_1.default.join(packageRoot, "package.json"));
10890
10893
  const bundledName = mappingsOf(host)[pluginId];
10891
- const targetRoot = typeof bundledName === "string" && dependenciesOf(host)[bundledName] !== undefined
10894
+ const targetRoot = typeof bundledName === "string" &&
10895
+ dependenciesOf(host)[bundledName] !== undefined &&
10896
+ dependenciesOf(readProjectManifest(projectRoot))[bundledName] === undefined
10892
10897
  ? packageRoot
10893
10898
  : projectRoot;
10894
10899
  const manifest = readProjectManifest(targetRoot);