From fa32fb0a8e2ab7d25029683604fa2e61e059f538 Mon Sep 17 00:00:00 2001 From: Towsty Date: Tue, 1 Sep 2026 07:07:41 -0500 Subject: [PATCH] Start Comfy without a visible command prompt on the desktop. Co-authored-by: Cursor --- scripts/comfy-host-agent.mjs | 91 +++++++++++++++++++++++++++------- server/utils/comfyLifecycle.ts | 18 ++++--- 2 files changed, 86 insertions(+), 23 deletions(-) diff --git a/scripts/comfy-host-agent.mjs b/scripts/comfy-host-agent.mjs index 377dcd3..b60df9f 100644 --- a/scripts/comfy-host-agent.mjs +++ b/scripts/comfy-host-agent.mjs @@ -2,8 +2,9 @@ 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, readFileSync, openSync } from 'node:fs' -import { basename, join, resolve, relative, isAbsolute } from 'node:path' +import { readdirSync, existsSync, rmSync, readFileSync, openSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs' +import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path' +import { tmpdir } from 'node:os' const execFileAsync = promisify(execFile) const port = Number(process.env.COMFY_CONTROL_PORT || 8199) @@ -235,10 +236,11 @@ function desktopShellCommand() { async function pythonMainUp() { try { - const { stdout } = await execFileAsync('wmic', [ - 'process', 'where', "name='python.exe'", 'get', 'commandline' + const { stdout } = await execFileAsync('powershell.exe', [ + '-NoProfile', '-Command', + "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'ComfyUI\\\\main.py|ComfyUI/main.py' } | Select-Object -First 1 -ExpandProperty ProcessId" ], { windowsHide: true, timeout: 6000 }) - return /main\.py/i.test(String(stdout || '')) + return Boolean(String(stdout).trim()) } catch { return false } @@ -268,6 +270,70 @@ async function pickListenPort() { return preferred } +function windowlessPython(pythonPath) { + if (process.platform !== 'win32') return pythonPath + if (!/python\.exe$/i.test(pythonPath)) return pythonPath + const pythonw = pythonPath.replace(/python\.exe$/i, 'pythonw.exe') + return existsSync(pythonw) ? pythonw : pythonPath +} + +function vbsString(value) { + return `"${String(value).replace(/"/g, '""')}"` +} + +function spawnViaWscript(file, args, opts = {}) { + const command = [file, ...args].map((part) => { + const text = String(part) + return /\s|"/.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text + }).join(' ') + const lines = ['Set sh = CreateObject("WScript.Shell")'] + if (opts.cwd) lines.push(`sh.CurrentDirectory = ${vbsString(opts.cwd)}`) + lines.push(`sh.Run ${vbsString(command)}, 0, False`) + const tmp = join(tmpdir(), `aigen-launch-${process.pid}-${Date.now()}.vbs`) + writeFileSync(tmp, `${lines.join('\r\n')}\r\n`) + const child = spawn('wscript.exe', ['//B', '//Nologo', tmp], { + detached: true, + stdio: 'ignore', + windowsHide: true + }) + child.unref() + const cleanup = () => { + try { unlinkSync(tmp) } catch { /* ignore */ } + } + child.once('exit', cleanup) + setTimeout(cleanup, 15000) + return child +} + +function spawnHidden(file, args, opts = {}) { + const exe = windowlessPython(file) + // python.exe + detached:true allocates an empty console on the active display. + // pythonw.exe is windowless. If it is missing, launch via hidden WScript. + if (process.platform === 'win32' && /python\.exe$/i.test(exe)) { + return spawnViaWscript(exe, args, opts) + } + const child = spawn(exe, args, { + detached: true, + stdio: opts.stdio || 'ignore', + windowsHide: true, + cwd: opts.cwd + }) + child.unref() + return child +} + +function spawnShellHidden(command, cwd) { + if (process.platform === 'win32') { + const match = String(command).match(/start\s+""\s+"([^"]+)"/i) + || String(command).match(/^"([^"]+\.exe)"\s*$/i) + if (match) return spawnHidden(match[1], [], { cwd }) + return spawnViaWscript(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', command], { cwd }) + } + const child = spawn(command, { shell: true, detached: true, stdio: 'ignore', cwd }) + child.unref() + return child +} + async function startComfy() { markWork() const launch = resolveHeadlessLaunch() @@ -277,15 +343,13 @@ async function startComfy() { if (portIdx >= 0) launch.args[portIdx + 1] = String(port) let stdio = 'ignore' try { + mkdirSync(dirname(launch.log), { recursive: true }) stdio = ['ignore', openSync(launch.log, 'a'), openSync(launch.log, 'a')] } catch { /* ignore */ } - const child = spawn(launch.file, launch.args, { - detached: true, + const child = spawnHidden(launch.file, launch.args, { stdio, - windowsHide: true, cwd: launch.cwd }) - child.unref() console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'start', @@ -302,14 +366,7 @@ async function startComfy() { 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() + spawnShellHidden(fallback.command, fallback.cwd) console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'start', diff --git a/server/utils/comfyLifecycle.ts b/server/utils/comfyLifecycle.ts index d7d4f03..73e014c 100644 --- a/server/utils/comfyLifecycle.ts +++ b/server/utils/comfyLifecycle.ts @@ -226,12 +226,18 @@ async function processRunning() { function spawnLaunchCommand(command: string) { log('spawn', { command: command.slice(0, 180) }) - const child = spawn(command, { - shell: true, - detached: true, - stdio: 'ignore', - windowsHide: true - }) + const win = process.platform === 'win32' + const exe = win + ? command.match(/start\s+""\s+"([^"]+)"/i)?.[1] || command.match(/^"([^"]+\.exe)"\s*$/i)?.[1] + : '' + const child = exe + ? spawn(exe, [], { detached: true, stdio: 'ignore', windowsHide: true }) + : spawn(command, { + shell: true, + detached: !win, + stdio: 'ignore', + windowsHide: true + }) child.unref() }