Start MiniMax Comfy headlessly so wake does not wait on the instance picker.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-30 07:40:43 -05:00
co-authored by Cursor
parent 18f350c32a
commit 5d828afbd7
2 changed files with 161 additions and 11 deletions
+159 -9
View File
@@ -2,7 +2,7 @@ import http from 'node:http'
import net from 'node:net' import net from 'node:net'
import { execFile, spawn } from 'node:child_process' import { execFile, spawn } from 'node:child_process'
import { promisify } from 'node:util' 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' import { basename, join, resolve, relative, isAbsolute } from 'node:path'
const execFileAsync = promisify(execFile) const execFileAsync = promisify(execFile)
@@ -157,20 +157,166 @@ async function processUp() {
} }
} }
function launchCommand() { function readJsonFile(path) {
if (process.env.COMFY_LAUNCH_CMD) return process.env.COMFY_LAUNCH_CMD 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 local = process.env.LOCALAPPDATA || ''
const desktop = `${local}\\Programs\\Comfy Desktop\\Comfy Desktop.exe` const desktop = `${local}\\Programs\\Comfy Desktop\\Comfy Desktop.exe`
return `start "" "${desktop}"` 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() markWork()
const command = launchCommand() const launch = resolveHeadlessLaunch()
const cwd = process.env.COMFY_LAUNCH_CWD || undefined if (launch?.file) {
const child = spawn(command, { shell: true, detached: true, stdio: 'ignore', windowsHide: true, cwd }) 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() child.unref()
return { started: true, command, cwd: cwd || null } 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()
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) { async function fetchLocalQueue(portNum) {
@@ -399,7 +545,11 @@ const server = http.createServer(async (req, res) => {
markWork() markWork()
return json(res, 200, { ok: true, started: false, already: true, asleep: false, port: healthyPort, proxyPort }) 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 }) return json(res, 200, { ok: true, asleep: false, ...launched })
} }
if (req.method === 'POST' && url.pathname === '/stop') { if (req.method === 'POST' && url.pathname === '/stop') {
+2 -2
View File
@@ -299,7 +299,7 @@ async function ensureUnlocked(onStatus: StatusFn, skipBusyWait = false) {
const localPort = remote.port ? ` Port ${remote.port} is healthy on the desktop.` : '' const localPort = remote.port ? ` Port ${remote.port} is healthy on the desktop.` : ''
throw createError({ throw createError({
statusCode: 503, 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() const running = await processRunning()
@@ -361,7 +361,7 @@ async function ensureUnlocked(onStatus: StatusFn, skipBusyWait = false) {
throw createError({ throw createError({
statusCode: 503, 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.'
}) })
} }