comfyui-mcp 0.51.34 → 0.51.35

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.
@@ -839,6 +839,69 @@ export class DownloadProgressSnapshots {
839
839
  return this.rows;
840
840
  }
841
841
  }
842
+ /**
843
+ * #1524 — a startup that never finishes must not become a silent resident.
844
+ *
845
+ * A respawn was observed alive for hours holding NO listening ports, while an
846
+ * older instance owned 9180/9181/9183. That is not the bind-failure path: that
847
+ * one is bounded (five attempts, then `whenReady()` resolves false), tries to
848
+ * reclaim the port, and exits non-zero with a clear message. A process with no
849
+ * ports at all never got that far — it hung EARLIER, so any guard wrapped around
850
+ * the bind itself would miss it.
851
+ *
852
+ * Hence a deadline armed as early as this function runs and disarmed only once
853
+ * the port is actually held. It does not care WHERE the hang is, which is the
854
+ * point: the failure it prevents is not "bind failed" but "we never found out",
855
+ * and the reporter's framing is that silently staying alive with zero bound ports
856
+ * is the worst of the available outcomes.
857
+ *
858
+ * WHAT IT DOES NOT COVER, because an earlier draft of this comment claimed "the
859
+ * whole of startup" and that was false (codex):
860
+ *
861
+ * - anything before `boot.ts` finishes dynamically importing this module — the
862
+ * timer does not exist yet;
863
+ * - a SYNCHRONOUS stall anywhere, which no timer can interrupt, because the
864
+ * event loop it would fire on is the one that is blocked.
865
+ *
866
+ * So this closes the async-hang shape of the report and cannot close the
867
+ * synchronous one. If a portless process is ever seen again with this in place,
868
+ * that difference is the first thing to check, and it is written down here so the
869
+ * next reader does not have to rediscover it.
870
+ *
871
+ * Generous by default (90s) because a cold `npx` start on a slow disk is
872
+ * legitimately slow, and env-tunable for pathological machines. Exits non-zero so
873
+ * a supervisor restarts rather than inheriting a half-alive process.
874
+ */
875
+ export function armStartupDeadline(port, deps = {}) {
876
+ const exit = deps.exit ?? ((code) => process.exit(code));
877
+ const findIncumbent = deps.incumbent ?? pidListeningOnPort;
878
+ // CLAMPED, not merely "positive and finite" (codex). Node coerces a
879
+ // sub-millisecond delay AND anything past the 32-bit signed limit to 1ms — so
880
+ // `0.5`, or a large number typed by someone trying to RAISE the deadline, would
881
+ // fire almost instantly and kill every healthy startup. A guard whose escape
882
+ // hatch can cause the outage it prevents is worse than no guard, so out-of-range
883
+ // values fall back to the default rather than being honoured literally.
884
+ const raw = Number(process.env.COMFYUI_MCP_STARTUP_DEADLINE_MS);
885
+ const MIN_MS = 1_000;
886
+ const MAX_MS = 2_147_483_647; // Node's setTimeout ceiling
887
+ const ms = Number.isFinite(raw) && raw >= MIN_MS && raw <= MAX_MS ? Math.floor(raw) : 90_000;
888
+ const timer = setTimeout(() => {
889
+ const incumbent = findIncumbent(port);
890
+ logger.error(`[panel-orchestrator] startup did not complete within ${Math.round(ms / 1000)}s and this ` +
891
+ `process holds no bridge port — exiting rather than lingering with no way to serve panel_* ` +
892
+ `tools.` +
893
+ (incumbent
894
+ ? ` Port ${port} is held by pid ${incumbent}; if that is an older comfyui-mcp, stop it ` +
895
+ `and start this one again.`
896
+ : ` Nothing is listening on ${port} either, so the hang is before the bind — please ` +
897
+ `report this with the last lines above (#1524).`) +
898
+ ` Raise COMFYUI_MCP_STARTUP_DEADLINE_MS if this machine is legitimately slower than that.`);
899
+ exit(1);
900
+ }, ms);
901
+ // Never hold the event loop open on this timer's account.
902
+ timer.unref?.();
903
+ return () => clearTimeout(timer);
904
+ }
842
905
  export async function runPanelOrchestrator() {
843
906
  // Crash guard: the orchestrator is a long-lived background process the user
844
907
  // can't see. A stray rejection (e.g. a fire-and-forget push to a tab that
@@ -849,6 +912,10 @@ export async function runPanelOrchestrator() {
849
912
  // mid-flight, an SDK hiccup) and must NOT kill the orchestrator — log + continue.
850
913
  logger.error(`[panel-orchestrator] unhandled rejection (ignored): ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
851
914
  });
915
+ // #1524 — armed HERE, before anything that can block, and disarmed only once
916
+ // the bridge port is actually held. The port is re-derived rather than passed
917
+ // in because the deadline has to exist before the block that computes it.
918
+ const disarmStartupDeadline = armStartupDeadline(Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180);
852
919
  process.on("uncaughtException", (err) => {
853
920
  // A synchronous uncaught throw leaves the process in an UNDEFINED state. The
854
921
  // old "log + continue" here was a zombie root cause — the orchestrator stayed
@@ -1040,6 +1107,9 @@ export async function runPanelOrchestrator() {
1040
1107
  logger.error(`[panel-orchestrator] could not bind the panel bridge port — another process owns it. Free that port and restart the orchestrator. Override the port with COMFYUI_MCP_BRIDGE_PORT.\n${portKillHint(lockPort)}`);
1041
1108
  process.exit(1);
1042
1109
  }
1110
+ // The port is ours — startup got where it needed to. Everything after this is
1111
+ // long-running work the deadline must not police.
1112
+ disarmStartupDeadline();
1043
1113
  // With a pinned pair token, bring the LAN pairing listener up now so a phone's
1044
1114
  // saved URL reconnects across restarts without ever touching the panel. This is
1045
1115
  // strictly opt-in (COMFYUI_MCP_PAIR_TOKEN unset → the on-demand behavior above is