From 5d828afbd70590ba610fec29a6a449423cf6b1f4 Mon Sep 17 00:00:00 2001 From: Towsty Date: Sun, 30 Aug 2026 07:40:43 -0500 Subject: [PATCH] Start MiniMax Comfy headlessly so wake does not wait on the instance picker. Co-authored-by: Cursor --- scripts/comfy-host-agent.mjs | 168 +++++++++++++++++++++++++++++++-- server/utils/comfyLifecycle.ts | 4 +- 2 files changed, 161 insertions(+), 11 deletions(-) diff --git a/scripts/comfy-host-agent.mjs b/scripts/comfy-host-agent.mjs index e268928..377dcd3 100644 --- a/scripts/comfy-host-agent.mjs +++ b/scripts/comfy-host-agent.mjs @@ -2,7 +2,7 @@ import http from 'node:http' import net from 'node:net' import { execFile, spawn } from 'node:child_process' import { promisify } from 'node:util' -import { readdirSync, existsSync, rmSync } from 'node:fs' +import { readdirSync, existsSync, rmSync, readFileSync, openSync } from 'node:fs' import { basename, join, resolve, relative, isAbsolute } from 'node:path' const execFileAsync = promisify(execFile) @@ -157,20 +157,166 @@ async function processUp() { } } -function launchCommand() { - if (process.env.COMFY_LAUNCH_CMD) return process.env.COMFY_LAUNCH_CMD +function readJsonFile(path) { + try { + return JSON.parse(readFileSync(path, 'utf8')) + } catch { + return null + } +} + +function desktopListenPort() { + try { + const url = new URL(defaultHttp.includes('://') ? defaultHttp : `http://${defaultHttp}`) + return Number(url.port || 8188) || 8188 + } catch { + return 8188 + } +} + +function pickDesktopInstall() { + const wanted = String(process.env.COMFY_INSTANCE_NAME || 'ComfyUI (1)').trim() + const parsed = readJsonFile(join(process.env.APPDATA || '', 'Comfy Desktop', 'installations.json')) + const installs = (Array.isArray(parsed) ? parsed : []) + .filter(item => item && item.status === 'installed' && item.installPath && item.sourceId !== 'cloud') + if (!installs.length) return null + return installs.find(item => item.name === wanted) + || installs.slice().sort((a, b) => Number(b.lastLaunchedAt || 0) - Number(a.lastLaunchedAt || 0))[0] +} + +function resolveHeadlessLaunch() { + if (process.env.COMFY_LAUNCH_CMD) { + return { + command: process.env.COMFY_LAUNCH_CMD, + cwd: process.env.COMFY_LAUNCH_CWD || undefined, + mode: 'env' + } + } + const inst = pickDesktopInstall() + if (!inst) return null + const venvPython = join(inst.installPath, 'ComfyUI', '.venv', 'Scripts', 'python.exe') + const standalonePython = join(inst.installPath, 'standalone-env', 'python.exe') + const python = existsSync(venvPython) ? venvPython : standalonePython + const cwd = inst.installPath + const main = join(inst.installPath, 'ComfyUI', 'main.py') + if (!existsSync(python) || !existsSync(main)) return null + const extra = join(process.env.APPDATA || '', 'Comfy Desktop', 'instance-model-paths', `${inst.id}.yaml`) + const shared = join(process.env.APPDATA || '', 'Comfy Desktop', 'shared_model_paths.yaml') + const extraConfig = existsSync(extra) ? extra : (existsSync(shared) ? shared : '') + const roots = sharedRoots() + const args = [ + '-s', + '-u', + join('ComfyUI', 'main.py'), + '--listen', '0.0.0.0', + '--port', String(desktopListenPort()), + '--disable-auto-launch' + ] + const extraArgs = String(inst.launchArgs || '').trim() + if (extraArgs) args.push(...extraArgs.split(/\s+/).filter(Boolean)) + if (extraConfig) args.push('--extra-model-paths-config', extraConfig) + if (existsSync(roots.input)) args.push('--input-directory', roots.input) + if (existsSync(roots.output)) args.push('--output-directory', roots.output) + return { + file: python, + args, + cwd, + mode: 'headless', + instance: inst.name, + log: join(inst.installPath, 'logs', 'aigen-headless.log') + } +} + +function desktopShellCommand() { const local = process.env.LOCALAPPDATA || '' const desktop = `${local}\\Programs\\Comfy Desktop\\Comfy Desktop.exe` return `start "" "${desktop}"` } -function startComfy() { +async function pythonMainUp() { + try { + const { stdout } = await execFileAsync('wmic', [ + 'process', 'where', "name='python.exe'", 'get', 'commandline' + ], { windowsHide: true, timeout: 6000 }) + return /main\.py/i.test(String(stdout || '')) + } catch { + return false + } +} + +function portInUse(portNum) { + return new Promise((resolve) => { + const probe = net.createServer() + probe.once('error', () => resolve(true)) + probe.once('listening', () => { + probe.close(() => resolve(false)) + }) + probe.listen(portNum, '0.0.0.0') + }) +} + +async function pickListenPort() { + const preferred = desktopListenPort() + const ordered = [preferred, ...candidatePorts()] + const seen = new Set() + for (const next of ordered) { + if (!next || seen.has(next)) continue + seen.add(next) + if (await probeStats(next)) return next + if (!(await portInUse(next))) return next + } + return preferred +} + +async function startComfy() { markWork() - const command = launchCommand() - const cwd = process.env.COMFY_LAUNCH_CWD || undefined - const child = spawn(command, { shell: true, detached: true, stdio: 'ignore', windowsHide: true, cwd }) + const launch = resolveHeadlessLaunch() + if (launch?.file) { + const port = await pickListenPort() + const portIdx = launch.args.indexOf('--port') + if (portIdx >= 0) launch.args[portIdx + 1] = String(port) + let stdio = 'ignore' + try { + stdio = ['ignore', openSync(launch.log, 'a'), openSync(launch.log, 'a')] + } catch { /* ignore */ } + const child = spawn(launch.file, launch.args, { + detached: true, + stdio, + windowsHide: true, + cwd: launch.cwd + }) + child.unref() + console.log(JSON.stringify({ + src: 'comfy-host-agent', + event: 'start', + mode: launch.mode, + instance: launch.instance, + cwd: launch.cwd, + port, + pid: child.pid || null + })) + return { started: true, mode: launch.mode, instance: launch.instance, cwd: launch.cwd, port, pid: child.pid || null } + } + const fallback = { + command: launch?.command || desktopShellCommand(), + cwd: launch?.cwd || process.env.COMFY_LAUNCH_CWD || undefined, + mode: launch?.mode || 'desktop-shell' + } + const child = spawn(fallback.command, { + shell: true, + detached: true, + stdio: 'ignore', + windowsHide: true, + cwd: fallback.cwd + }) child.unref() - return { started: true, command, cwd: cwd || null } + console.log(JSON.stringify({ + src: 'comfy-host-agent', + event: 'start', + mode: fallback.mode, + cwd: fallback.cwd || null + })) + return { started: true, mode: fallback.mode, instance: null, cwd: fallback.cwd || null } } async function fetchLocalQueue(portNum) { @@ -399,7 +545,11 @@ const server = http.createServer(async (req, res) => { markWork() return json(res, 200, { ok: true, started: false, already: true, asleep: false, port: healthyPort, proxyPort }) } - const launched = startComfy() + if (await pythonMainUp()) { + return json(res, 200, { ok: true, started: false, already: true, booting: true, asleep: false }) + } + await taskkillImage('Comfy Desktop.exe') + const launched = await startComfy() return json(res, 200, { ok: true, asleep: false, ...launched }) } if (req.method === 'POST' && url.pathname === '/stop') { diff --git a/server/utils/comfyLifecycle.ts b/server/utils/comfyLifecycle.ts index c74fe55..f202301 100644 --- a/server/utils/comfyLifecycle.ts +++ b/server/utils/comfyLifecycle.ts @@ -299,7 +299,7 @@ async function ensureUnlocked(onStatus: StatusFn, skipBusyWait = false) { const localPort = remote.port ? ` Port ${remote.port} is healthy on the desktop.` : '' throw createError({ statusCode: 503, - statusMessage: `Comfy Desktop is open and the local API is up, but this server cannot reach ${host}.${localPort} Open the MiniMax instance “ComfyUI (1)” and wait until the graph editor loads — the instance picker is not enough.` + statusMessage: `Comfy is running on the desktop but this server cannot reach ${host}.${localPort} Check COMFY_HOST / the LAN proxy.` }) } const running = await processRunning() @@ -361,7 +361,7 @@ async function ensureUnlocked(onStatus: StatusFn, skipBusyWait = false) { throw createError({ statusCode: 503, - statusMessage: 'Started Comfy Desktop but the API never answered. If the instance picker is showing, click “ComfyUI (1)” (MiniMax) and wait until the graph editor is loaded. Cold start can take a couple of minutes.' + statusMessage: 'Started ComfyUI but the API never answered. A cold start can take a couple of minutes. Confirm the host agent launched the MiniMax install headlessly.' }) }