lnlink-server 1.1.2 → 1.1.3

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.
package/dist/index.js CHANGED
@@ -122,6 +122,15 @@ var require_linkLogger = __commonJS({
122
122
  }
123
123
  // Cache for logger instances to avoid creating multiple instances with the same logType
124
124
  static instances = /* @__PURE__ */ new Map();
125
+ // Silent mode flag - when true, all logging is suppressed (used during graceful shutdown)
126
+ static silentMode = false;
127
+ /**
128
+ * Set silent mode to suppress all logging (used during graceful shutdown)
129
+ * @param {boolean} value - Whether to enable silent mode
130
+ */
131
+ static setSilentMode(value) {
132
+ _Logger.silentMode = value;
133
+ }
125
134
  constructor(logType) {
126
135
  if (_Logger.instances.has(logType)) {
127
136
  return _Logger.instances.get(logType);
@@ -131,21 +140,27 @@ var require_linkLogger = __commonJS({
131
140
  _Logger.instances.set(logType, this);
132
141
  }
133
142
  debug(message, ...args) {
143
+ if (_Logger.silentMode) return;
134
144
  this.logger.debug({ msg: message }, ...args);
135
145
  }
136
146
  info(message, ...args) {
147
+ if (_Logger.silentMode) return;
137
148
  this.logger.info({ msg: message }, ...args);
138
149
  }
139
150
  warn(message, ...args) {
151
+ if (_Logger.silentMode) return;
140
152
  this.logger.warn({ msg: message }, ...args);
141
153
  }
142
154
  error(message, ...args) {
155
+ if (_Logger.silentMode) return;
143
156
  this.logger.error({ msg: message }, ...args);
144
157
  }
145
158
  fatal(message, ...args) {
159
+ if (_Logger.silentMode) return;
146
160
  this.logger.fatal({ msg: message }, ...args);
147
161
  }
148
162
  logWithData(level, message, data) {
163
+ if (_Logger.silentMode) return;
149
164
  this.logger[level]({ ...data, msg: message });
150
165
  }
151
166
  };
@@ -2720,7 +2735,6 @@ var require_getConfig = __commonJS({
2720
2735
  "LINK_DEBUG",
2721
2736
  "LINK_ENABLE_TOR",
2722
2737
  "LINK_NODE_ENV",
2723
- "LINK_AUTO_ASSIGN_PORTS",
2724
2738
  "LINK_HTTPS_PROXY",
2725
2739
  "LINK_DATABASE_URL",
2726
2740
  "LINK_REPORT_BASE_URL",
@@ -2885,9 +2899,7 @@ var require_getConfig = __commonJS({
2885
2899
  combinedConfig.LINK_TOR_SOCKS_PORT = combinedConfig.LINK_TOR_SOCKS_PORT || 9050;
2886
2900
  combinedConfig.LINK_TOR_CONTROL_PORT = combinedConfig.LINK_TOR_CONTROL_PORT || 9051;
2887
2901
  try {
2888
- if (processConfig.LINK_AUTO_ASSIGN_PORTS !== "false") {
2889
- combinedConfig = await assignAvailablePorts(combinedConfig);
2890
- }
2902
+ combinedConfig = await assignAvailablePorts(combinedConfig);
2891
2903
  } catch (portError) {
2892
2904
  console.warn(
2893
2905
  "Port assignment failed, using default ports:",
@@ -5577,7 +5589,6 @@ var require_processManager = __commonJS({
5577
5589
  "business/service/nodeManage/processManager.js"(exports2, module2) {
5578
5590
  var { spawn } = require("node:child_process");
5579
5591
  var findProcess = require("find-process").default;
5580
- var { getLightningService } = require_common();
5581
5592
  var { getConfig: getConfig2 } = require_getConfig();
5582
5593
  var Logger = require_linkLogger();
5583
5594
  var { sleep } = require_timeUtils();
@@ -5587,6 +5598,11 @@ var require_processManager = __commonJS({
5587
5598
  return config.LINK_EXTERNAL_NODES === "true" || config.LINK_EXTERNAL_NODES === true;
5588
5599
  }
5589
5600
  __name(isExternalNodesMode, "isExternalNodesMode");
5601
+ function shouldDetach() {
5602
+ const config = getConfig2();
5603
+ return config.LINK_NODE_ENV !== "app";
5604
+ }
5605
+ __name(shouldDetach, "shouldDetach");
5590
5606
  function terminateProcess(pid, force = false) {
5591
5607
  const logger2 = new Logger("processManager");
5592
5608
  try {
@@ -5601,9 +5617,19 @@ var require_processManager = __commonJS({
5601
5617
  }
5602
5618
  __name(terminateProcess, "terminateProcess");
5603
5619
  var serviceState = {
5604
- services: null
5620
+ services: null,
5605
5621
  // Will be initialized when needed
5622
+ isShuttingDown: false
5623
+ // Flag to suppress async logs during shutdown
5606
5624
  };
5625
+ function setShuttingDown(value) {
5626
+ serviceState.isShuttingDown = value;
5627
+ }
5628
+ __name(setShuttingDown, "setShuttingDown");
5629
+ function isShuttingDown() {
5630
+ return serviceState.isShuttingDown;
5631
+ }
5632
+ __name(isShuttingDown, "isShuttingDown");
5607
5633
  function initializeServices() {
5608
5634
  if (!serviceState.services) {
5609
5635
  serviceState.services = getServiceConfig();
@@ -5654,7 +5680,8 @@ var require_processManager = __commonJS({
5654
5680
  }
5655
5681
  }, "settleReject");
5656
5682
  const proc = spawn(service.command, args, {
5657
- stdio: "pipe"
5683
+ stdio: "pipe",
5684
+ detached: shouldDetach()
5658
5685
  });
5659
5686
  setServiceProcess("litd", proc);
5660
5687
  timeoutId = setTimeout(() => {
@@ -5664,7 +5691,9 @@ var require_processManager = __commonJS({
5664
5691
  }
5665
5692
  }, LITD_STARTUP_TIMEOUT);
5666
5693
  proc.once("exit", (code, signal) => {
5667
- logger2.info(`litd process exited with code ${code ?? "null"}, signal ${signal ?? "null"}`);
5694
+ if (!isShuttingDown()) {
5695
+ logger2.info(`litd process exited with code ${code ?? "null"}, signal ${signal ?? "null"}`);
5696
+ }
5668
5697
  const currentService = getService("litd");
5669
5698
  if (currentService?.process === proc) {
5670
5699
  setServiceProcess("litd", null);
@@ -5727,7 +5756,8 @@ var require_processManager = __commonJS({
5727
5756
  }, "settleReject");
5728
5757
  logger2.info(`Tor config: ${JSON.stringify(args, null, 2)}`);
5729
5758
  const proc = spawn(service.command, args, {
5730
- stdio: "pipe"
5759
+ stdio: "pipe",
5760
+ detached: shouldDetach()
5731
5761
  });
5732
5762
  setServiceProcess("tor", proc);
5733
5763
  timeoutId = setTimeout(() => {
@@ -5737,7 +5767,9 @@ var require_processManager = __commonJS({
5737
5767
  }
5738
5768
  }, TOR_STARTUP_TIMEOUT);
5739
5769
  proc.once("exit", (code, signal) => {
5740
- logger2.info(`tor process exited with code ${code ?? "null"}, signal ${signal ?? "null"}`);
5770
+ if (!isShuttingDown()) {
5771
+ logger2.info(`tor process exited with code ${code ?? "null"}, signal ${signal ?? "null"}`);
5772
+ }
5741
5773
  const currentService = getService("tor");
5742
5774
  if (currentService?.process === proc) {
5743
5775
  setServiceProcess("tor", null);
@@ -5805,7 +5837,8 @@ var require_processManager = __commonJS({
5805
5837
  }
5806
5838
  }, "settleReject");
5807
5839
  const proc = spawn(service.command, args, {
5808
- stdio: "pipe"
5840
+ stdio: "pipe",
5841
+ detached: shouldDetach()
5809
5842
  });
5810
5843
  setServiceProcess("rgb", proc);
5811
5844
  timeoutId = setTimeout(() => {
@@ -5815,7 +5848,9 @@ var require_processManager = __commonJS({
5815
5848
  }
5816
5849
  }, RGB_STARTUP_TIMEOUT);
5817
5850
  proc.once("exit", (code, signal) => {
5818
- logger2.info(`rgb process exited with code ${code ?? "null"}, signal ${signal ?? "null"}`);
5851
+ if (!isShuttingDown()) {
5852
+ logger2.info(`rgb process exited with code ${code ?? "null"}, signal ${signal ?? "null"}`);
5853
+ }
5819
5854
  const currentService = getService("rgb");
5820
5855
  if (currentService?.process === proc) {
5821
5856
  setServiceProcess("rgb", null);
@@ -5923,35 +5958,12 @@ var require_processManager = __commonJS({
5923
5958
  const trackedProcess = initialService.process;
5924
5959
  const processAlive = trackedProcess && !trackedProcess.killed && trackedProcess.exitCode === null;
5925
5960
  if (processAlive) {
5926
- try {
5927
- logger2.info("Attempting graceful shutdown via RPC...");
5928
- const lightningService = getLightningService();
5929
- await lightningService.stopDaemon();
5930
- logger2.info("LND daemon stopped gracefully via RPC");
5931
- let waitCount = 0;
5932
- const maxWait = 10;
5933
- while (waitCount < maxWait) {
5934
- const currentProcess = getService("litd")?.process ?? trackedProcess;
5935
- if (!currentProcess || currentProcess.killed || currentProcess.exitCode !== null) {
5936
- setServiceProcess("litd", null);
5937
- return true;
5938
- }
5939
- await sleep(1e3);
5940
- waitCount++;
5941
- }
5942
- logger2.warn("Process did not exit after RPC shutdown, proceeding with fallback...");
5943
- } catch (error) {
5944
- logger2.warn(`RPC shutdown failed: ${error.message}, falling back to process termination`);
5945
- }
5946
- } else {
5947
- logger2.info("litd process already exited, skipping RPC shutdown");
5948
- setServiceProcess("litd", null);
5949
- }
5950
- const fallbackService = getService("litd") ?? initialService;
5951
- if (fallbackService.process) {
5952
- return await killProcess(fallbackService.process, "litd");
5961
+ logger2.info("Stopping litd via SIGTERM...");
5962
+ return await killProcess(trackedProcess, "litd", 5e3);
5953
5963
  }
5954
- return await findAndKillProcess(fallbackService.command, "litd");
5964
+ logger2.info("litd process already exited, cleaning up...");
5965
+ setServiceProcess("litd", null);
5966
+ return await findAndKillProcess(initialService.command, "litd");
5955
5967
  }
5956
5968
  __name(stopLitdService, "stopLitdService");
5957
5969
  async function stopTorService() {
@@ -6007,32 +6019,12 @@ var require_processManager = __commonJS({
6007
6019
  if (!service) {
6008
6020
  throw new Error("rgb service not found");
6009
6021
  }
6010
- try {
6011
- logger2.info("Attempting graceful shutdown of RGB via SIGTERM...");
6012
- const processList = await findProcess("name", "rgb-lightning-node");
6013
- if (processList.length > 0) {
6014
- processList.forEach((p) => terminateProcess(p.pid, false));
6015
- await sleep(2e3);
6016
- setServiceProcess("rgb", null);
6017
- const remaining = await findProcess("name", "rgb-lightning-node");
6018
- if (remaining.length === 0) {
6019
- logger2.info("RGB stopped gracefully");
6020
- return true;
6021
- }
6022
- logger2.warn(`${remaining.length} RGB process(es) still running after SIGTERM, force killing...`);
6023
- remaining.forEach((p) => terminateProcess(p.pid, true));
6024
- setServiceProcess("rgb", null);
6025
- return true;
6026
- }
6027
- logger2.info("No RGB processes found via find-process, trying tracked process...");
6028
- } catch (error) {
6029
- logger2.warn(`RGB graceful shutdown failed: ${error.message}, falling back to process termination`);
6030
- }
6031
6022
  if (service.process) {
6032
- return await killProcess(service.process, "rgb");
6033
- } else {
6034
- return await findAndKillProcess("rgb-lightning-node", "rgb");
6023
+ logger2.info("Stopping RGB via tracked process...");
6024
+ return await killProcess(service.process, "rgb", 5e3);
6035
6025
  }
6026
+ logger2.info("No tracked RGB process, falling back to system scan...");
6027
+ return await findAndKillProcess("rgb-lightning-node", "rgb");
6036
6028
  }
6037
6029
  __name(stopRgbService, "stopRgbService");
6038
6030
  async function stopService(name) {
@@ -6079,13 +6071,24 @@ var require_processManager = __commonJS({
6079
6071
  };
6080
6072
  }
6081
6073
  __name(getServicesState, "getServicesState");
6074
+ function getServicePids() {
6075
+ const services = initializeServices();
6076
+ return {
6077
+ litd: services.litd?.process?.pid || null,
6078
+ tor: services.tor?.process?.pid || null,
6079
+ rgb: services.rgb?.process?.pid || null
6080
+ };
6081
+ }
6082
+ __name(getServicePids, "getServicePids");
6082
6083
  module2.exports = {
6083
6084
  startService,
6084
6085
  stopService,
6085
6086
  stopLitdService,
6086
6087
  stopTorService,
6087
6088
  stopRgbService,
6088
- getServicesState
6089
+ getServicesState,
6090
+ getServicePids,
6091
+ setShuttingDown
6089
6092
  };
6090
6093
  }
6091
6094
  });
@@ -6202,21 +6205,11 @@ var require_node = __commonJS({
6202
6205
  tag: TASKS.EnableRGBNode
6203
6206
  });
6204
6207
  }
6205
- const unlockPromise = rgbClient.node.unlockNode(unlockNodeParams);
6206
- const timeoutMs = 3e4;
6207
- let timeoutId;
6208
- const timeoutPromise = new Promise((resolve) => {
6209
- timeoutId = setTimeout(async () => {
6210
- const status = await getNodeState();
6211
- resolve({
6212
- message: "Request submitted, current node status:",
6213
- status
6214
- });
6215
- }, timeoutMs);
6216
- });
6217
- const result = await Promise.race([unlockPromise, timeoutPromise]);
6218
- clearTimeout(timeoutId);
6219
- return result || true;
6208
+ rgbClient.node.unlockNode(unlockNodeParams).then(() => {
6209
+ }).catch((err) => console.error("[rgb] unlockNode background error:", err.message));
6210
+ const linkCache = require_linkCache();
6211
+ linkCache.set("rgbState", 3);
6212
+ return { state: 3, message: "Unlock submitted, syncing..." };
6220
6213
  }
6221
6214
  __name(unlockNode, "unlockNode");
6222
6215
  async function backupNode({ password }) {
@@ -6276,6 +6269,7 @@ var require_statusChecker = __commonJS({
6276
6269
  var { getNodeState } = require_node();
6277
6270
  var { getServiceConfig } = require_config2();
6278
6271
  var { getServicesState } = require_processManager();
6272
+ var normalizePath = /* @__PURE__ */ __name((s) => s ? s.replace(/\\/g, "/") : "", "normalizePath");
6279
6273
  function isExternalNodesMode() {
6280
6274
  const config = getConfig2();
6281
6275
  return config.LINK_EXTERNAL_NODES === "true" || config.LINK_EXTERNAL_NODES === true;
@@ -6313,10 +6307,26 @@ var require_statusChecker = __commonJS({
6313
6307
  }
6314
6308
  return ServiceStatus.STOPPED;
6315
6309
  }
6310
+ const { LINK_DATA_PATH } = getConfig2();
6316
6311
  const processList = await findProcess("name", "litd");
6312
+ if (!LINK_DATA_PATH) {
6313
+ if (processList.length > 0) {
6314
+ logger2.warn("LINK_DATA_PATH not set, cannot filter litd by ownership. Returning RUNNING based on process name match.");
6315
+ return ServiceStatus.RUNNING;
6316
+ }
6317
+ return ServiceStatus.STOPPED;
6318
+ }
6319
+ const lnlinkLitdDataDir = `${LINK_DATA_PATH}/.litd`;
6317
6320
  if (processList.length > 0) {
6318
- logger2.info(`Found litd process(es): ${processList.map((p) => p.pid).join(", ")}`);
6319
- return ServiceStatus.RUNNING;
6321
+ const targetDir = normalizePath(lnlinkLitdDataDir);
6322
+ const lnlinkLitdProcess = processList.find((p) => p.cmd && normalizePath(p.cmd).includes(targetDir));
6323
+ if (lnlinkLitdProcess) {
6324
+ logger2.info(`Found lnlink litd process: PID ${lnlinkLitdProcess.pid}`);
6325
+ return ServiceStatus.RUNNING;
6326
+ } else {
6327
+ logger2.debug(`Found ${processList.length} litd process(es), but none belong to lnlink (data dir: ${lnlinkLitdDataDir})`);
6328
+ return ServiceStatus.STOPPED;
6329
+ }
6320
6330
  }
6321
6331
  return ServiceStatus.STOPPED;
6322
6332
  } catch (error) {
@@ -6341,10 +6351,26 @@ var require_statusChecker = __commonJS({
6341
6351
  }
6342
6352
  return ServiceStatus.STOPPED;
6343
6353
  }
6354
+ const { LINK_DATA_PATH } = getConfig2();
6344
6355
  const processList = await findProcess("name", "rgb-lightning-node");
6356
+ if (!LINK_DATA_PATH) {
6357
+ if (processList.length > 0) {
6358
+ logger2.warn("LINK_DATA_PATH not set, cannot filter RGB by ownership. Returning RUNNING based on process name match.");
6359
+ return ServiceStatus.RUNNING;
6360
+ }
6361
+ return ServiceStatus.STOPPED;
6362
+ }
6363
+ const lnlinkRgbDataDir = `${LINK_DATA_PATH}/.rgb`;
6345
6364
  if (processList.length > 0) {
6346
- logger2.info(`Found rgb-lightning-node process(es): ${processList.map((p) => p.pid).join(", ")}`);
6347
- return ServiceStatus.RUNNING;
6365
+ const targetDir = normalizePath(lnlinkRgbDataDir);
6366
+ const lnlinkRgbProcess = processList.find((p) => p.cmd && normalizePath(p.cmd).includes(targetDir));
6367
+ if (lnlinkRgbProcess) {
6368
+ logger2.info(`Found lnlink RGB process: PID ${lnlinkRgbProcess.pid}`);
6369
+ return ServiceStatus.RUNNING;
6370
+ } else {
6371
+ logger2.debug(`Found ${processList.length} rgb-lightning-node process(es), but none belong to lnlink (data dir: ${lnlinkRgbDataDir})`);
6372
+ return ServiceStatus.STOPPED;
6373
+ }
6348
6374
  }
6349
6375
  return ServiceStatus.STOPPED;
6350
6376
  } catch (error) {
@@ -6383,12 +6409,25 @@ var require_statusChecker = __commonJS({
6383
6409
  }
6384
6410
  }
6385
6411
  __name(checkProcess, "checkProcess");
6386
- function getTerminalStatus(filter = "") {
6387
- const services = getServiceConfig();
6412
+ var terminalStatusCache = {
6413
+ data: null,
6414
+ timestamp: 0,
6415
+ TTL_MS: 5e3
6416
+ // 5 seconds cache
6417
+ };
6418
+ async function getTerminalStatus(filter = "") {
6388
6419
  const logger2 = new Logger("statusChecker");
6420
+ if (!filter && terminalStatusCache.data) {
6421
+ const age = Date.now() - terminalStatusCache.timestamp;
6422
+ if (age < terminalStatusCache.TTL_MS) {
6423
+ logger2.debug(`getTerminalStatus: returning cached result (age: ${age}ms)`);
6424
+ return [...terminalStatusCache.data];
6425
+ }
6426
+ }
6427
+ const services = getServiceConfig();
6389
6428
  const shouldInclude = /* @__PURE__ */ __name((name) => name !== "rgb" && (!filter || name.includes(filter)), "shouldInclude");
6390
6429
  const filteredServices = Object.keys(services).filter(shouldInclude);
6391
- return Promise.all(
6430
+ const result = await Promise.all(
6392
6431
  filteredServices.map(async (name) => {
6393
6432
  try {
6394
6433
  const state = await checkProcess(name);
@@ -6403,8 +6442,18 @@ var require_statusChecker = __commonJS({
6403
6442
  }
6404
6443
  })
6405
6444
  );
6445
+ if (!filter) {
6446
+ terminalStatusCache.data = result;
6447
+ terminalStatusCache.timestamp = Date.now();
6448
+ }
6449
+ return result;
6406
6450
  }
6407
6451
  __name(getTerminalStatus, "getTerminalStatus");
6452
+ function invalidateTerminalStatusCache() {
6453
+ terminalStatusCache.data = null;
6454
+ terminalStatusCache.timestamp = 0;
6455
+ }
6456
+ __name(invalidateTerminalStatusCache, "invalidateTerminalStatusCache");
6408
6457
  async function getRGBStatus() {
6409
6458
  const logger2 = new Logger("statusChecker");
6410
6459
  try {
@@ -6461,6 +6510,7 @@ var require_statusChecker = __commonJS({
6461
6510
  checkProcess,
6462
6511
  getRGBStatus,
6463
6512
  getTerminalStatus,
6513
+ invalidateTerminalStatusCache,
6464
6514
  isServiceRunning,
6465
6515
  waitForServiceStatus
6466
6516
  };
@@ -6486,6 +6536,7 @@ var require_nodeManage = __commonJS({
6486
6536
  var { startService, stopService } = require_processManager();
6487
6537
  var {
6488
6538
  getTerminalStatus,
6539
+ invalidateTerminalStatusCache,
6489
6540
  isServiceRunning,
6490
6541
  getRGBStatus,
6491
6542
  ServiceStatus
@@ -6546,22 +6597,30 @@ var require_nodeManage = __commonJS({
6546
6597
  return { matched: false, statuses: lastStatuses };
6547
6598
  }
6548
6599
  __name(waitForRgbStatus, "waitForRgbStatus");
6600
+ var isStartingTerminal = false;
6601
+ var isStartingRGB = false;
6549
6602
  async function startTerminal({ waitForRunning = true } = {}) {
6550
6603
  const logger2 = new Logger("litdService");
6551
- const { LINK_ENABLE_TOR } = getConfig2();
6552
- const isStartTor = LINK_ENABLE_TOR === "true" || LINK_ENABLE_TOR === true;
6553
- logger2.info("Starting terminal services...");
6554
- const startStatus = await getTerminalStatus();
6555
- const litdStatus = startStatus.find((item) => item.service_name === "litd");
6556
- const torStatus = startStatus.find((item) => item.service_name === "tor");
6557
- logger2.info(`Current status - Litd: ${litdStatus?.status}, Tor: ${torStatus?.status}`);
6558
- let torSucceeded = !isStartTor;
6604
+ if (isStartingTerminal) {
6605
+ logger2.warn("Terminal start already in progress, returning current status");
6606
+ return await getTerminalStatus();
6607
+ }
6608
+ isStartingTerminal = true;
6559
6609
  try {
6610
+ const { LINK_ENABLE_TOR } = getConfig2();
6611
+ const isStartTor = LINK_ENABLE_TOR === "true" || LINK_ENABLE_TOR === true;
6612
+ logger2.info("Starting terminal services...");
6613
+ const startStatus = await getTerminalStatus();
6614
+ const litdStatus = startStatus.find((item) => item.service_name === "litd");
6615
+ const torStatus = startStatus.find((item) => item.service_name === "tor");
6616
+ logger2.info(`Current status - Litd: ${litdStatus?.status}, Tor: ${torStatus?.status}`);
6617
+ let torSucceeded = !isStartTor;
6560
6618
  if (isStartTor && torStatus?.status === ServiceStatus.STOPPED) {
6561
6619
  logger2.info("Starting Tor service...");
6562
6620
  try {
6563
6621
  const torArgs = buildTorArgs();
6564
6622
  await startService("tor", torArgs);
6623
+ invalidateTerminalStatusCache();
6565
6624
  torSucceeded = true;
6566
6625
  logger2.info("Tor service started successfully");
6567
6626
  } catch (torError) {
@@ -6579,6 +6638,7 @@ var require_nodeManage = __commonJS({
6579
6638
  logger2.info(`Starting Litd service (Tor: ${useTor})...`);
6580
6639
  const litdArgs = buildLitdArgs(useTor);
6581
6640
  await startService("litd", litdArgs);
6641
+ invalidateTerminalStatusCache();
6582
6642
  logger2.info("Litd service started successfully");
6583
6643
  }
6584
6644
  const endStatus = await getTerminalStatus();
@@ -6600,6 +6660,8 @@ var require_nodeManage = __commonJS({
6600
6660
  } catch (error) {
6601
6661
  logger2.error(`Failed to start terminal services: ${error.message}`);
6602
6662
  throw error;
6663
+ } finally {
6664
+ isStartingTerminal = false;
6603
6665
  }
6604
6666
  }
6605
6667
  __name(startTerminal, "startTerminal");
@@ -6613,6 +6675,7 @@ var require_nodeManage = __commonJS({
6613
6675
  logger2.info("Stopping terminal services...");
6614
6676
  try {
6615
6677
  const stopRet = await stopService("litd");
6678
+ invalidateTerminalStatusCache();
6616
6679
  if (!stopRet) {
6617
6680
  logger2.warn("stopService returned a falsy result");
6618
6681
  return false;
@@ -6706,6 +6769,11 @@ var require_nodeManage = __commonJS({
6706
6769
  async function startRGB(args = {}) {
6707
6770
  const { waitForRunning = true, lnlinkUser } = args;
6708
6771
  const logger2 = new Logger("rgbService");
6772
+ if (isStartingRGB) {
6773
+ logger2.warn("RGB start already in progress, returning current status");
6774
+ return await getRGBStatus();
6775
+ }
6776
+ isStartingRGB = true;
6709
6777
  logger2.info("Starting RGB service...");
6710
6778
  try {
6711
6779
  const startStatus = await getRGBStatus();
@@ -6740,6 +6808,8 @@ var require_nodeManage = __commonJS({
6740
6808
  } catch (error) {
6741
6809
  logger2.error(`Failed to start RGB service: ${error.message}`);
6742
6810
  throw error;
6811
+ } finally {
6812
+ isStartingRGB = false;
6743
6813
  }
6744
6814
  }
6745
6815
  __name(startRGB, "startRGB");
@@ -6848,24 +6918,6 @@ var require_nodeManage = __commonJS({
6848
6918
  }
6849
6919
  }
6850
6920
  __name(updateNodeName, "updateNodeName");
6851
- if (globalThis.addCleanupFunction) {
6852
- const logger2 = new Logger("nodeManage");
6853
- globalThis.addCleanupFunction(async () => {
6854
- logger2.info("Cleaning up node processes...");
6855
- try {
6856
- for (const name of ["litd", "tor", "rgb"]) {
6857
- try {
6858
- await stopService(name);
6859
- logger2.info(`${name} process stopped`);
6860
- } catch (err) {
6861
- logger2.warn(`Failed to stop ${name}: ${err.message}`);
6862
- }
6863
- }
6864
- } catch (error) {
6865
- logger2.error(`Error during node cleanup: ${error.message}`);
6866
- }
6867
- });
6868
- }
6869
6921
  module2.exports = {
6870
6922
  // Terminal (Litd + Tor) management
6871
6923
  startTerminal,
@@ -7127,7 +7179,7 @@ var require_package = __commonJS({
7127
7179
  "package.json"(exports2, module2) {
7128
7180
  module2.exports = {
7129
7181
  name: "lnlink-server",
7130
- version: "1.1.2",
7182
+ version: "1.1.3",
7131
7183
  private: false,
7132
7184
  main: "dist/index.js",
7133
7185
  files: [
@@ -7137,13 +7189,13 @@ var require_package = __commonJS({
7137
7189
  ],
7138
7190
  scripts: {
7139
7191
  build: "node build.js && node build.js --mode development --external all --entry electron",
7140
- "start:bin": "dotenv -e .env.bin -- node scripts/start-bin.js",
7192
+ "start:bin": "node scripts/start-bin.js",
7141
7193
  "start:docker:dev": "dotenv -e .env.dev -- docker compose -f ./docker-compose.dev.yml up --build",
7142
7194
  "start:dev": 'dotenv -e .env.dev -- sh -c "prisma generate && (prisma migrate dev --name auto_update || prisma db push) && clinic heapprof -- node ./app.js"',
7143
7195
  "start:regtest": "docker compose --env-file ./.env.regtest -f ./docker-compose-lnlink.yml up --build",
7144
7196
  "start:testnet": "docker compose --env-file ./.env.testnet -f ./docker-compose-lnlink.yml up --build",
7145
7197
  "start:mainnet": "docker compose --env-file ./.env.mainnet -f ./docker-compose-lnlink.yml up --build",
7146
- "start:bin:dist": "dotenv -e .env.bin -- node scripts/start-bin.js --dist",
7198
+ "start:bin:dist": "node scripts/start-bin.js --dist",
7147
7199
  lint: "eslint .",
7148
7200
  "lint:fix": "eslint . --fix",
7149
7201
  "lint:staged": "lint-staged",
@@ -7462,13 +7514,9 @@ var require_lndService = __commonJS({
7462
7514
  LINK_NODE_ADDR,
7463
7515
  LINK_LND_PEER_LISTEN_PORT
7464
7516
  } = getConfig2();
7465
- const currentWalletState = await getWalletState(true);
7466
- const terminalStatus = await getTerminalStatus();
7467
- const linkStatusObj = {};
7468
- terminalStatus.forEach((item) => {
7469
- linkStatusObj[item.service_name] = item.status;
7470
- });
7471
7517
  const logger2 = new Logger("lnd");
7518
+ const version = packageJSON.version;
7519
+ const currentWalletState = await getWalletState(true);
7472
7520
  const timeoutMs = 15e3;
7473
7521
  const fallbackCombineInfo = linkCache.get("combineNodeInfo") || {};
7474
7522
  let timeoutId;
@@ -7481,16 +7529,34 @@ var require_lndService = __commonJS({
7481
7529
  });
7482
7530
  }, timeoutMs);
7483
7531
  });
7484
- const result = await Promise.race([
7485
- combineNodeInfoAsync(currentWalletState).then((data2) => ({ fromCache: false, data: data2 })),
7486
- timeoutPromise
7487
- ]);
7488
- clearTimeout(timeoutId);
7489
- const combineInfo = result.data || {};
7490
- const version = packageJSON.version;
7491
- const taprootAssetsEnabled = await checkTprEnabled().catch(() => false);
7492
- const readOnlyAccount = await getLnlinkUser({
7493
- account_type: ACCOUNT_TYPE.READ_ONLY
7532
+ let results;
7533
+ try {
7534
+ results = await Promise.allSettled([
7535
+ Promise.race([
7536
+ combineNodeInfoAsync(currentWalletState).then((data2) => ({ fromCache: false, data: data2 })),
7537
+ timeoutPromise
7538
+ ]),
7539
+ getTerminalStatus(),
7540
+ checkTprEnabled(),
7541
+ getLnlinkUser({ account_type: ACCOUNT_TYPE.READ_ONLY })
7542
+ ]);
7543
+ } finally {
7544
+ clearTimeout(timeoutId);
7545
+ }
7546
+ const [combineSettled, terminalSettled, tprSettled, userSettled] = results;
7547
+ const combineInfo = combineSettled.value?.data || {};
7548
+ const terminalStatus = terminalSettled.status === "fulfilled" ? terminalSettled.value : [];
7549
+ const taprootAssetsEnabled = tprSettled.status === "fulfilled" ? tprSettled.value : false;
7550
+ const readOnlyAccount = userSettled.status === "fulfilled" ? userSettled.value : null;
7551
+ if (terminalSettled.status === "rejected") {
7552
+ logger2.warn(`getNodeInfo: getTerminalStatus failed: ${terminalSettled.reason?.message}`);
7553
+ }
7554
+ if (userSettled.status === "rejected") {
7555
+ logger2.warn(`getNodeInfo: getLnlinkUser failed: ${userSettled.reason?.message}`);
7556
+ }
7557
+ const linkStatusObj = {};
7558
+ terminalStatus.forEach((item) => {
7559
+ linkStatusObj[item.service_name] = item.status;
7494
7560
  });
7495
7561
  const uri = combineInfo?.uris?.[0];
7496
7562
  const data = {
@@ -7722,6 +7788,9 @@ var require_lndService = __commonJS({
7722
7788
  async function combineNodeInfoAsync(walletState) {
7723
7789
  let retInfo = {};
7724
7790
  const logger2 = new Logger("lnd");
7791
+ const CACHE_KEY = "combineNodeInfo";
7792
+ const CACHE_TTL_KEY = "combineNodeInfo_ts";
7793
+ const CACHE_TTL_MS = 5e3;
7725
7794
  try {
7726
7795
  const { isMacaroonDecrypted } = getCacheMacaroon();
7727
7796
  let state = walletState;
@@ -7730,18 +7799,33 @@ var require_lndService = __commonJS({
7730
7799
  state = WALLET_STATE_CODE.LOCKED;
7731
7800
  }
7732
7801
  }
7802
+ const cachedInfo = linkCache.get(CACHE_KEY);
7803
+ const cacheTimestamp = linkCache.get(CACHE_TTL_KEY);
7804
+ if (cachedInfo && cacheTimestamp && cachedInfo.state === state) {
7805
+ const age = Date.now() - cacheTimestamp;
7806
+ if (age < CACHE_TTL_MS) {
7807
+ logger2.debug(`combineNodeInfoAsync: returning cached result (age: ${age}ms)`);
7808
+ return cachedInfo;
7809
+ }
7810
+ }
7733
7811
  if (state >= WALLET_STATE_CODE.RPC_ACTIVE && isMacaroonDecrypted && state !== WALLET_STATE_CODE.WAITING_TO_START) {
7734
7812
  const lightningService = getLightningService();
7735
- const { settings } = await getMainLnlinkConfig();
7736
7813
  const ret = await Promise.allSettled([
7814
+ getMainLnlinkConfig(),
7737
7815
  lightningService.getInfo(),
7738
7816
  lightningService.walletBalance(),
7739
7817
  lightningService.listPeers({})
7740
7818
  ]);
7741
- const [infoResult, balanceResult, peersResult] = ret;
7742
- const nodeInfo = infoResult.value;
7743
- const balance = balanceResult.value;
7744
- const retListpeers = peersResult.value;
7819
+ const [configResult, infoResult, balanceResult, peersResult] = ret;
7820
+ let settings = null;
7821
+ if (configResult.status === "fulfilled") {
7822
+ settings = configResult.value?.settings;
7823
+ } else {
7824
+ logger2.warn(`combineNodeInfoAsync: getMainLnlinkConfig failed: ${configResult.reason?.message}`);
7825
+ }
7826
+ const nodeInfo = infoResult.status === "fulfilled" ? infoResult.value : null;
7827
+ const balance = balanceResult.status === "fulfilled" ? balanceResult.value : null;
7828
+ const retListpeers = peersResult.status === "fulfilled" ? peersResult.value : null;
7745
7829
  const peers = retListpeers?.peers || [];
7746
7830
  const mapPeers = peers.map((peer) => {
7747
7831
  const isLnfiPeer = peer.pub_key === settings?.officialLndPeer;
@@ -7771,7 +7855,8 @@ var require_lndService = __commonJS({
7771
7855
  ...retInfo,
7772
7856
  state
7773
7857
  };
7774
- linkCache.set("combineNodeInfo", retInfo);
7858
+ linkCache.set(CACHE_KEY, retInfo);
7859
+ linkCache.set(CACHE_TTL_KEY, Date.now());
7775
7860
  return retInfo;
7776
7861
  } catch (e) {
7777
7862
  logger2.warn(`LND lndService combineNodeInfoAsync peer combineNodeInfo warn--->${e.message}`);
@@ -8291,6 +8376,15 @@ var require_lndService = __commonJS({
8291
8376
  }
8292
8377
  logger2.info("Stopping litd before config reload (to free ports)...");
8293
8378
  await stopTerminal({ lnlinkUser, waitForStopped: true });
8379
+ if (!enableTor && torRunning) {
8380
+ logger2.info("Stopping Tor service (was running standalone)...");
8381
+ try {
8382
+ await stopService("tor");
8383
+ logger2.info("Tor service stopped successfully");
8384
+ } catch (torError) {
8385
+ logger2.warn(`Failed to stop Tor service: ${torError.message}`);
8386
+ }
8387
+ }
8294
8388
  logger2.info("Reloading config after services stopped...");
8295
8389
  await reloadConfig();
8296
8390
  logger2.info("Starting litd with new Tor configuration...");
@@ -11908,12 +12002,17 @@ var require_info = __commonJS({
11908
12002
  LINK_RGB_LDK_PEER_LISTENING_PORT
11909
12003
  } = getConfig2();
11910
12004
  const logger2 = new Logger("rgb");
11911
- let statusEntries = [];
11912
- try {
11913
- statusEntries = await getRGBStatus();
11914
- } catch (error) {
11915
- logger2.warn(`getCombinedNodeInfo - getRGBStatus failed: ${error.message}`);
12005
+ const [statusResult, readOnlyAccountResult] = await Promise.allSettled([
12006
+ getRGBStatus(),
12007
+ getLnlinkUser({ account_type: ACCOUNT_TYPE.READ_ONLY })
12008
+ ]);
12009
+ let statusEntries = {};
12010
+ if (statusResult.status === "fulfilled") {
12011
+ statusEntries = statusResult.value;
12012
+ } else {
12013
+ logger2.warn(`getCombinedNodeInfo - getRGBStatus failed: ${statusResult.reason?.message}`);
11916
12014
  }
12015
+ const readOnlyAccount = readOnlyAccountResult.status === "fulfilled" ? readOnlyAccountResult.value : null;
11917
12016
  const serviceRunning = statusEntries.status === ServiceStatus.RUNNING;
11918
12017
  let nodeInfo = null;
11919
12018
  let balance = {
@@ -11945,7 +12044,8 @@ var require_info = __commonJS({
11945
12044
  ] = await Promise.allSettled([
11946
12045
  rgbClient.node.getNodeInfo(),
11947
12046
  rgbClient.onchain.getBtcBalance({
11948
- skip_sync: false
12047
+ skip_sync: true
12048
+ // Skip sync for faster response
11949
12049
  }),
11950
12050
  rgbClient.lightning.listPeers({})
11951
12051
  ]);
@@ -11964,14 +12064,11 @@ var require_info = __commonJS({
11964
12064
  } else {
11965
12065
  logger2.warn(`getCombinedNodeInfo - listPeers failed: ${peersResult.reason?.message || peersResult.reason}`);
11966
12066
  }
11967
- rgbClient.rgb.refreshTransfers({ skip_sync: false }).catch((error) => {
12067
+ rgbClient.rgb.refreshTransfers({ skip_sync: true }).catch((error) => {
11968
12068
  logger2.warn(`getCombinedNodeInfo - refreshTransfers failed: ${error.message}`);
11969
12069
  });
11970
12070
  }
11971
12071
  }
11972
- const readOnlyAccount = await getLnlinkUser({
11973
- account_type: ACCOUNT_TYPE.READ_ONLY
11974
- });
11975
12072
  const data = {
11976
12073
  pubkey: nodeInfo?.pubkey ?? null,
11977
12074
  host: `${LINK_RGB_HOST}:${LINK_RGB_LDK_PEER_LISTENING_PORT}`,
@@ -19001,17 +19098,6 @@ var require_job = __commonJS({
19001
19098
  logger2.info("Job system graceful shutdown completed");
19002
19099
  }
19003
19100
  __name(gracefulShutdown, "gracefulShutdown");
19004
- process.on("SIGINT", gracefulShutdown);
19005
- process.on("SIGTERM", gracefulShutdown);
19006
- process.on("uncaughtException", (error) => {
19007
- const logger2 = new Logger("job_uncaught");
19008
- logger2.error(`Uncaught exception: ${error.message}`);
19009
- gracefulShutdown();
19010
- });
19011
- process.on("unhandledRejection", (reason, promise) => {
19012
- const logger2 = new Logger("job_unhandled");
19013
- logger2.error(`Unhandled rejection at: ${promise}, reason: ${reason}`);
19014
- });
19015
19101
  module2.exports = {
19016
19102
  initJobSystem,
19017
19103
  startJobSystem,
@@ -19511,20 +19597,25 @@ var LnLink = class extends EventEmitter {
19511
19597
  */
19512
19598
  async stop() {
19513
19599
  try {
19514
- try {
19515
- const { stopService } = require_processManager();
19516
- for (const name of ["rgb", "litd", "tor"]) {
19600
+ const { stopService, setShuttingDown } = require_processManager();
19601
+ setShuttingDown(true);
19602
+ console.info("Stopping LN-Link (jobs + rgb + litd + tor)...");
19603
+ await Promise.allSettled([
19604
+ // Job system shutdown (cron tasks, state monitoring)
19605
+ (async () => {
19606
+ const { gracefulShutdown: shutdownJobs } = require_job();
19607
+ await shutdownJobs();
19608
+ })().catch((err) => console.warn(`Job system shutdown failed: ${err.message}`)),
19609
+ // Stop all child processes in parallel
19610
+ ...["rgb", "litd", "tor"].map(async (name) => {
19517
19611
  try {
19518
- console.info(`Stopping ${name} process...`);
19519
19612
  await stopService(name);
19520
- console.info(`${name} process stopped`);
19613
+ console.info(`${name} stopped`);
19521
19614
  } catch (err) {
19522
19615
  console.warn(`Failed to stop ${name}: ${err.message}`);
19523
19616
  }
19524
- }
19525
- } catch (err) {
19526
- console.warn(`Child process cleanup failed: ${err.message}`);
19527
- }
19617
+ })
19618
+ ]);
19528
19619
  if (this.server) {
19529
19620
  await new Promise((resolve) => {
19530
19621
  this.server.close(() => {
@@ -19564,6 +19655,14 @@ var LnLink = class extends EventEmitter {
19564
19655
  port: this.isRunning ? this.options.httpPort : null
19565
19656
  };
19566
19657
  }
19658
+ /**
19659
+ * Get PIDs of all managed child processes (litd, tor, rgb)
19660
+ * @returns {{ litd: number|null, tor: number|null, rgb: number|null }}
19661
+ */
19662
+ getServicePids() {
19663
+ const { getServicePids } = require_processManager();
19664
+ return getServicePids();
19665
+ }
19567
19666
  /**
19568
19667
  * Get configuration
19569
19668
  */