Stop idle Comfy after 30 minutes and add a Wake GPU poke.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,6 +9,8 @@ COMFY_LAUNCH_CMD=
|
|||||||
# Windows host agent (scripts/comfy-host-agent.mjs) so Coolify can start Comfy remotely
|
# Windows host agent (scripts/comfy-host-agent.mjs) so Coolify can start Comfy remotely
|
||||||
COMFY_CONTROL_URL=http://192.168.77.7:8199
|
COMFY_CONTROL_URL=http://192.168.77.7:8199
|
||||||
COMFY_CONTROL_TOKEN=
|
COMFY_CONTROL_TOKEN=
|
||||||
|
# Idle-stop lives on the Beast host agent only (default 30 min). Set on the desktop, not here.
|
||||||
|
# COMFY_IDLE_MS=1800000
|
||||||
|
|
||||||
NUXT_SESSION_SECRET=change-me
|
NUXT_SESSION_SECRET=change-me
|
||||||
NUXT_PUBLIC_AUTH_ENABLED=true
|
NUXT_PUBLIC_AUTH_ENABLED=true
|
||||||
|
|||||||
+71
-16
@@ -10,14 +10,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 text-sm">
|
<div class="flex items-center gap-3 text-sm">
|
||||||
<span
|
<div class="flex items-center gap-2">
|
||||||
class="hidden items-center gap-2 rounded-full border px-3 py-1 sm:inline-flex"
|
<span
|
||||||
:class="rigBadge.className"
|
class="hidden items-center gap-2 rounded-full border px-3 py-1 sm:inline-flex"
|
||||||
:title="rigBadge.title"
|
:class="rigBadge.className"
|
||||||
>
|
:title="rigBadge.title"
|
||||||
<span class="h-2 w-2 rounded-full" :class="rigBadge.dot" />
|
>
|
||||||
{{ rigBadge.label }}
|
<span class="h-2 w-2 rounded-full" :class="rigBadge.dot" />
|
||||||
</span>
|
{{ rigBadge.label }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="showWakeGpu"
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white disabled:cursor-wait disabled:opacity-80"
|
||||||
|
:class="gpuWaking ? 'border-amber-300/40 text-amber-100' : 'border-white/10'"
|
||||||
|
:disabled="gpuWaking"
|
||||||
|
:title="gpuWakeError || (gpuWaking ? 'Starting Comfy Desktop on the GPU' : 'Poke / Wake GPU — start Comfy before generating')"
|
||||||
|
@click="pokeGpu"
|
||||||
|
>
|
||||||
|
{{ gpuWaking ? 'Starting GPU…' : 'Wake GPU' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<span class="hidden md:block text-zinc-400">{{ userName }}</span>
|
<span class="hidden md:block text-zinc-400">{{ userName }}</span>
|
||||||
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white" @click="settingsOpen = true">
|
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white" @click="settingsOpen = true">
|
||||||
Settings
|
Settings
|
||||||
@@ -2358,6 +2371,9 @@ const playerSize = ref({ width: 1344, height: 768 })
|
|||||||
const toasts = ref<{ id: string; message: string }[]>([])
|
const toasts = ref<{ id: string; message: string }[]>([])
|
||||||
const comfyOk = ref(false)
|
const comfyOk = ref(false)
|
||||||
const comfyBusy = ref(false)
|
const comfyBusy = ref(false)
|
||||||
|
const comfyAsleep = ref(false)
|
||||||
|
const gpuWaking = ref(false)
|
||||||
|
const gpuWakeError = ref('')
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
const authEnabled = ref(false)
|
const authEnabled = ref(false)
|
||||||
const authMode = ref('')
|
const authMode = ref('')
|
||||||
@@ -2574,13 +2590,24 @@ const liveMaxStep = computed(() => (
|
|||||||
const rigBusy = computed(() => (
|
const rigBusy = computed(() => (
|
||||||
comfyBusy.value || imageComfyBusy.value || videoBusy.value || editBusy.value
|
comfyBusy.value || imageComfyBusy.value || videoBusy.value || editBusy.value
|
||||||
))
|
))
|
||||||
|
const showWakeGpu = computed(() => gpuWaking.value || (!comfyOk.value && !imageComfyOk.value))
|
||||||
const rigBadge = computed(() => {
|
const rigBadge = computed(() => {
|
||||||
|
if (gpuWaking.value) {
|
||||||
|
return {
|
||||||
|
label: 'Starting…',
|
||||||
|
className: 'border-amber-400/40 text-amber-200',
|
||||||
|
dot: 'bg-amber-400 animate-pulse',
|
||||||
|
title: 'Starting Comfy Desktop on the GPU'
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!comfyOk.value && !imageComfyOk.value) {
|
if (!comfyOk.value && !imageComfyOk.value) {
|
||||||
return {
|
return {
|
||||||
label: 'Offline',
|
label: 'Offline',
|
||||||
className: 'border-red-400/30 text-red-300',
|
className: 'border-red-400/30 text-red-300',
|
||||||
dot: 'bg-red-400',
|
dot: 'bg-red-400',
|
||||||
title: 'Desktop GPU is unreachable'
|
title: comfyAsleep.value
|
||||||
|
? 'Desktop GPU is asleep. Click Wake GPU or generate to start Comfy.'
|
||||||
|
: 'Desktop GPU is unreachable'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rigBusy.value) {
|
if (rigBusy.value) {
|
||||||
@@ -2696,7 +2723,7 @@ const recommendHint = computed(() => {
|
|||||||
const editBlocked = computed(() => {
|
const editBlocked = computed(() => {
|
||||||
if (!file.value || !prompt.value.trim() || !folderId.value) return true
|
if (!file.value || !prompt.value.trim() || !folderId.value) return true
|
||||||
if (editPassQueue.value.some(item => !item.prompt.trim())) return true
|
if (editPassQueue.value.some(item => !item.prompt.trim())) return true
|
||||||
if (!comfyOk.value && !imageComfyOk.value) return true
|
if (!comfyOk.value && !imageComfyOk.value && !gpuWaking.value) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const editBlockReason = computed(() => {
|
const editBlockReason = computed(() => {
|
||||||
@@ -2705,7 +2732,7 @@ const editBlockReason = computed(() => {
|
|||||||
if (!folderId.value) return 'Choose a library folder before editing.'
|
if (!folderId.value) return 'Choose a library folder before editing.'
|
||||||
if (editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
|
if (editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
|
||||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
|
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
|
||||||
if (!comfyOk.value && !imageComfyOk.value) return 'Desktop GPU is offline.'
|
if (!comfyOk.value && !imageComfyOk.value && !gpuWaking.value) return 'Desktop GPU is offline. Click Wake GPU first.'
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
const stillBHint = computed(() => {
|
const stillBHint = computed(() => {
|
||||||
@@ -2719,7 +2746,7 @@ const editV2Blocked = computed(() => {
|
|||||||
if (v2Mode.value !== 'generate' && !file.value) return true
|
if (v2Mode.value !== 'generate' && !file.value) return true
|
||||||
if (v2Mode.value === 'compose' && !editRefFile.value) return true
|
if (v2Mode.value === 'compose' && !editRefFile.value) return true
|
||||||
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
|
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
|
||||||
if (!comfyOk.value && !imageComfyOk.value) return true
|
if (!comfyOk.value && !imageComfyOk.value && !gpuWaking.value) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const editV2BlockReason = computed(() => {
|
const editV2BlockReason = computed(() => {
|
||||||
@@ -2729,7 +2756,7 @@ const editV2BlockReason = computed(() => {
|
|||||||
if (!prompt.value.trim()) return 'Write a prompt first.'
|
if (!prompt.value.trim()) return 'Write a prompt first.'
|
||||||
if (!folderId.value) return 'Choose a library folder before generating.'
|
if (!folderId.value) return 'Choose a library folder before generating.'
|
||||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
|
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
|
||||||
if (!comfyOk.value && !imageComfyOk.value) return 'Desktop GPU is offline.'
|
if (!comfyOk.value && !imageComfyOk.value && !gpuWaking.value) return 'Desktop GPU is offline. Click Wake GPU first.'
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
const editV2SubmitLabel = computed(() => {
|
const editV2SubmitLabel = computed(() => {
|
||||||
@@ -3206,7 +3233,7 @@ function scheduleLibraryRefresh() {
|
|||||||
|
|
||||||
let healthFailStreak = 0
|
let healthFailStreak = 0
|
||||||
function applyComfyHealth(health: {
|
function applyComfyHealth(health: {
|
||||||
comfy?: { ok?: boolean; busy?: boolean }
|
comfy?: { ok?: boolean; busy?: boolean; asleep?: boolean }
|
||||||
imageComfy?: {
|
imageComfy?: {
|
||||||
ok?: boolean
|
ok?: boolean
|
||||||
busy?: boolean
|
busy?: boolean
|
||||||
@@ -3219,7 +3246,11 @@ function applyComfyHealth(health: {
|
|||||||
const nextComfyOk = Boolean(health?.comfy?.ok)
|
const nextComfyOk = Boolean(health?.comfy?.ok)
|
||||||
const nextImageOk = Boolean(health?.imageComfy?.ok)
|
const nextImageOk = Boolean(health?.imageComfy?.ok)
|
||||||
const reachable = nextComfyOk || nextImageOk
|
const reachable = nextComfyOk || nextImageOk
|
||||||
if (reachable) healthFailStreak = 0
|
if (reachable) {
|
||||||
|
healthFailStreak = 0
|
||||||
|
gpuWaking.value = false
|
||||||
|
gpuWakeError.value = ''
|
||||||
|
}
|
||||||
else healthFailStreak += 1
|
else healthFailStreak += 1
|
||||||
const keepLast = !reachable && (healthFailStreak < 3 || videoBusy.value || editBusy.value)
|
const keepLast = !reachable && (healthFailStreak < 3 || videoBusy.value || editBusy.value)
|
||||||
if (!keepLast) {
|
if (!keepLast) {
|
||||||
@@ -3227,6 +3258,7 @@ function applyComfyHealth(health: {
|
|||||||
imageComfyOk.value = nextImageOk
|
imageComfyOk.value = nextImageOk
|
||||||
imageBeastOk.value = Boolean(health?.imageComfy?.beast?.ok)
|
imageBeastOk.value = Boolean(health?.imageComfy?.beast?.ok)
|
||||||
imageSidecarOk.value = Boolean(health?.imageComfy?.sidecar?.ok)
|
imageSidecarOk.value = Boolean(health?.imageComfy?.sidecar?.ok)
|
||||||
|
comfyAsleep.value = Boolean(health?.comfy?.asleep) && !reachable
|
||||||
}
|
}
|
||||||
comfyBusy.value = Boolean((keepLast ? comfyOk.value : nextComfyOk) && health?.comfy?.busy)
|
comfyBusy.value = Boolean((keepLast ? comfyOk.value : nextComfyOk) && health?.comfy?.busy)
|
||||||
imageComfyConfigured.value = Boolean(health?.imageComfy?.configured)
|
imageComfyConfigured.value = Boolean(health?.imageComfy?.configured)
|
||||||
@@ -3246,10 +3278,33 @@ async function pollLoras() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pollComfyHealth() {
|
async function pollComfyHealth() {
|
||||||
const health = await $fetch<{ comfy?: { ok?: boolean; busy?: boolean } }>('/api/health').catch(() => ({ comfy: { ok: false, busy: false } }))
|
const health = await $fetch<{ comfy?: { ok?: boolean; busy?: boolean; asleep?: boolean } }>('/api/health').catch(() => ({ comfy: { ok: false, busy: false } }))
|
||||||
applyComfyHealth(health)
|
applyComfyHealth(health)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pokeGpu() {
|
||||||
|
if (gpuWaking.value) return
|
||||||
|
gpuWaking.value = true
|
||||||
|
gpuWakeError.value = ''
|
||||||
|
try {
|
||||||
|
await $fetch('/api/comfy/wake', { method: 'POST' })
|
||||||
|
const deadline = Date.now() + 180_000
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
await pollComfyHealth()
|
||||||
|
if (comfyOk.value || imageComfyOk.value) {
|
||||||
|
gpuWaking.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
}
|
||||||
|
gpuWakeError.value = 'Started Comfy but it is not answering yet. If the instance picker is showing, open ComfyUI (1).'
|
||||||
|
} catch (error: any) {
|
||||||
|
gpuWakeError.value = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Could not wake the GPU.'
|
||||||
|
} finally {
|
||||||
|
gpuWaking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
withSound.value = localStorage.getItem('aigen-generate-sound') !== 'false'
|
withSound.value = localStorage.getItem('aigen-generate-sound') !== 'false'
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ const port = Number(process.env.COMFY_CONTROL_PORT || 8199)
|
|||||||
const proxyPort = Number(process.env.COMFY_PROXY_PORT || 8198)
|
const proxyPort = Number(process.env.COMFY_PROXY_PORT || 8198)
|
||||||
const token = process.env.COMFY_CONTROL_TOKEN || ''
|
const token = process.env.COMFY_CONTROL_TOKEN || ''
|
||||||
const defaultHttp = (process.env.COMFY_HOST || 'http://127.0.0.1:8188').replace(/\/$/, '')
|
const defaultHttp = (process.env.COMFY_HOST || 'http://127.0.0.1:8188').replace(/\/$/, '')
|
||||||
|
const idleMs = Math.max(60_000, Number(process.env.COMFY_IDLE_MS || 30 * 60 * 1000) || 30 * 60 * 1000)
|
||||||
|
|
||||||
|
let lastWorkAt = Date.now()
|
||||||
|
let lastQueueRunning = 0
|
||||||
|
let stoppedByAgent = false
|
||||||
|
|
||||||
|
function markWork() {
|
||||||
|
lastWorkAt = Date.now()
|
||||||
|
stoppedByAgent = false
|
||||||
|
}
|
||||||
|
|
||||||
function json(res, status, body) {
|
function json(res, status, body) {
|
||||||
const payload = JSON.stringify(body)
|
const payload = JSON.stringify(body)
|
||||||
@@ -110,7 +120,11 @@ function ensureProxy(targetPort) {
|
|||||||
}
|
}
|
||||||
client.on('error', fail)
|
client.on('error', fail)
|
||||||
upstream.on('error', fail)
|
upstream.on('error', fail)
|
||||||
client.pipe(upstream)
|
client.once('data', (chunk) => {
|
||||||
|
if (isWorkHttp(chunk)) markWork()
|
||||||
|
upstream.write(chunk)
|
||||||
|
client.pipe(upstream)
|
||||||
|
})
|
||||||
upstream.pipe(client)
|
upstream.pipe(client)
|
||||||
})
|
})
|
||||||
proxyServer.on('error', (error) => {
|
proxyServer.on('error', (error) => {
|
||||||
@@ -151,6 +165,7 @@ function launchCommand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startComfy() {
|
function startComfy() {
|
||||||
|
markWork()
|
||||||
const command = launchCommand()
|
const command = launchCommand()
|
||||||
const cwd = process.env.COMFY_LAUNCH_CWD || undefined
|
const cwd = process.env.COMFY_LAUNCH_CWD || undefined
|
||||||
const child = spawn(command, { shell: true, detached: true, stdio: 'ignore', windowsHide: true, cwd })
|
const child = spawn(command, { shell: true, detached: true, stdio: 'ignore', windowsHide: true, cwd })
|
||||||
@@ -158,6 +173,96 @@ function startComfy() {
|
|||||||
return { started: true, command, cwd: cwd || null }
|
return { started: true, command, cwd: cwd || null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchLocalQueue(portNum) {
|
||||||
|
if (!portNum) return { running: 0, pending: 0, ok: false }
|
||||||
|
try {
|
||||||
|
const res = await fetch(`http://127.0.0.1:${portNum}/queue`, { signal: AbortSignal.timeout(2500) })
|
||||||
|
if (!res.ok) return { running: 0, pending: 0, ok: false }
|
||||||
|
const payload = await res.json()
|
||||||
|
return {
|
||||||
|
running: Array.isArray(payload?.queue_running) ? payload.queue_running.length : 0,
|
||||||
|
pending: Array.isArray(payload?.queue_pending) ? payload.queue_pending.length : 0,
|
||||||
|
ok: true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { running: 0, pending: 0, ok: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWorkHttp(chunk) {
|
||||||
|
const head = chunk.toString('latin1', 0, Math.min(chunk.length, 256))
|
||||||
|
const line = head.split('\r\n')[0] || ''
|
||||||
|
const match = line.match(/^(GET|HEAD|POST|PUT|PATCH|DELETE)\s+(\S+)/i)
|
||||||
|
if (!match) return false
|
||||||
|
const method = match[1].toUpperCase()
|
||||||
|
let path = String(match[2] || '').split('?')[0] || ''
|
||||||
|
try { path = decodeURIComponent(path) } catch { /* keep raw */ }
|
||||||
|
if (method === 'GET' || method === 'HEAD') return false
|
||||||
|
if (/^\/(system_stats|queue|object_info|internal)\b/i.test(path)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function taskkillImage(image) {
|
||||||
|
try {
|
||||||
|
await execFileAsync('taskkill', ['/IM', image, '/F', '/T'], { windowsHide: true, timeout: 8000 })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopComfyProcesses() {
|
||||||
|
const killed = []
|
||||||
|
if (await taskkillImage('Comfy Desktop.exe')) killed.push('Comfy Desktop.exe')
|
||||||
|
if (await taskkillImage('ComfyUI.exe')) killed.push('ComfyUI.exe')
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFileAsync('powershell.exe', [
|
||||||
|
'-NoProfile', '-Command',
|
||||||
|
"Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'ComfyUI\\\\main.py|ComfyUI/main.py' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $_.ProcessId }"
|
||||||
|
], { windowsHide: true, timeout: 8000 })
|
||||||
|
if (String(stdout).trim()) killed.push('main.py')
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return killed
|
||||||
|
}
|
||||||
|
|
||||||
|
async function noteQueue(portNum) {
|
||||||
|
const queue = await fetchLocalQueue(portNum)
|
||||||
|
if (!queue.ok) return queue
|
||||||
|
if (queue.running > 0 || queue.pending > 0) {
|
||||||
|
markWork()
|
||||||
|
} else if (lastQueueRunning > 0 && queue.running === 0) {
|
||||||
|
lastWorkAt = Date.now()
|
||||||
|
}
|
||||||
|
lastQueueRunning = queue.running
|
||||||
|
return queue
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeIdleStop(healthyPort) {
|
||||||
|
if (healthyPort) {
|
||||||
|
stoppedByAgent = false
|
||||||
|
const queue = await noteQueue(healthyPort)
|
||||||
|
if (!queue.ok) return
|
||||||
|
if (queue.running > 0 || queue.pending > 0) return
|
||||||
|
} else if (await processUp()) {
|
||||||
|
stoppedByAgent = false
|
||||||
|
} else {
|
||||||
|
stoppedByAgent = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Date.now() - lastWorkAt < idleMs) return
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
src: 'comfy-host-agent',
|
||||||
|
event: 'idle-stop',
|
||||||
|
idleMs,
|
||||||
|
lastActivityAt: new Date(lastWorkAt).toISOString(),
|
||||||
|
http: Boolean(healthyPort)
|
||||||
|
}))
|
||||||
|
const killed = await stopComfyProcesses()
|
||||||
|
stoppedByAgent = true
|
||||||
|
lastQueueRunning = 0
|
||||||
|
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'stopped', reason: 'idle', killed }))
|
||||||
|
}
|
||||||
|
|
||||||
function sharedRoots() {
|
function sharedRoots() {
|
||||||
const local = process.env.LOCALAPPDATA || ''
|
const local = process.env.LOCALAPPDATA || ''
|
||||||
return {
|
return {
|
||||||
@@ -274,10 +379,16 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const url = new URL(req.url || '/', 'http://localhost')
|
const url = new URL(req.url || '/', 'http://localhost')
|
||||||
if (req.method === 'GET' && url.pathname === '/status') {
|
if (req.method === 'GET' && url.pathname === '/status') {
|
||||||
const [healthyPort, process] = await Promise.all([syncProxy(), processUp()])
|
const [healthyPort, process] = await Promise.all([syncProxy(), processUp()])
|
||||||
|
if (healthyPort) await noteQueue(healthyPort)
|
||||||
|
const processUpNow = Boolean(process || healthyPort)
|
||||||
return json(res, 200, {
|
return json(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
http: Boolean(healthyPort),
|
http: Boolean(healthyPort),
|
||||||
process,
|
process: processUpNow,
|
||||||
|
processUp: processUpNow,
|
||||||
|
asleep: Boolean(stoppedByAgent || (!healthyPort && !process)),
|
||||||
|
lastActivityAt: new Date(lastWorkAt).toISOString(),
|
||||||
|
idleMs,
|
||||||
port: healthyPort || null,
|
port: healthyPort || null,
|
||||||
proxyPort: healthyPort ? proxyPort : null
|
proxyPort: healthyPort ? proxyPort : null
|
||||||
})
|
})
|
||||||
@@ -285,10 +396,28 @@ const server = http.createServer(async (req, res) => {
|
|||||||
if (req.method === 'POST' && url.pathname === '/start') {
|
if (req.method === 'POST' && url.pathname === '/start') {
|
||||||
const healthyPort = await syncProxy()
|
const healthyPort = await syncProxy()
|
||||||
if (healthyPort) {
|
if (healthyPort) {
|
||||||
return json(res, 200, { ok: true, started: false, already: true, port: healthyPort, proxyPort })
|
markWork()
|
||||||
|
return json(res, 200, { ok: true, started: false, already: true, asleep: false, port: healthyPort, proxyPort })
|
||||||
}
|
}
|
||||||
const launched = startComfy()
|
const launched = startComfy()
|
||||||
return json(res, 200, { ok: true, ...launched })
|
return json(res, 200, { ok: true, asleep: false, ...launched })
|
||||||
|
}
|
||||||
|
if (req.method === 'POST' && url.pathname === '/stop') {
|
||||||
|
const healthyPort = await syncProxy()
|
||||||
|
const queue = healthyPort ? await fetchLocalQueue(healthyPort) : { running: 0, pending: 0, ok: false }
|
||||||
|
if (queue.ok && (queue.running > 0 || queue.pending > 0)) {
|
||||||
|
return json(res, 409, {
|
||||||
|
ok: false,
|
||||||
|
error: 'queue-busy',
|
||||||
|
running: queue.running,
|
||||||
|
pending: queue.pending
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const killed = await stopComfyProcesses()
|
||||||
|
stoppedByAgent = true
|
||||||
|
lastQueueRunning = 0
|
||||||
|
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'stopped', reason: 'stop', killed }))
|
||||||
|
return json(res, 200, { ok: true, stopped: true, asleep: true, killed })
|
||||||
}
|
}
|
||||||
if (req.method === 'POST' && url.pathname === '/purge') {
|
if (req.method === 'POST' && url.pathname === '/purge') {
|
||||||
const body = await readJson(req)
|
const body = await readJson(req)
|
||||||
@@ -299,15 +428,23 @@ const server = http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
server.listen(port, '0.0.0.0', async () => {
|
server.listen(port, '0.0.0.0', async () => {
|
||||||
const healthyPort = await syncProxy()
|
const healthyPort = await syncProxy()
|
||||||
|
if (healthyPort) await noteQueue(healthyPort)
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
src: 'comfy-host-agent',
|
src: 'comfy-host-agent',
|
||||||
event: 'listen',
|
event: 'listen',
|
||||||
port,
|
port,
|
||||||
proxyPort,
|
proxyPort,
|
||||||
comfyPort: healthyPort || null,
|
comfyPort: healthyPort || null,
|
||||||
|
idleMs,
|
||||||
candidates: candidatePorts()
|
candidates: candidatePorts()
|
||||||
}))
|
}))
|
||||||
|
let lastIdleCheck = 0
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
syncProxy().catch(() => {})
|
syncProxy().then(async (nextPort) => {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastIdleCheck < 10_000) return
|
||||||
|
lastIdleCheck = now
|
||||||
|
await maybeIdleStop(nextPort)
|
||||||
|
}).catch(() => {})
|
||||||
}, 3000)
|
}, 3000)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
' Launch the Comfy host agent with no console window.
|
||||||
|
' Window style 0 = hidden. Wait = True so Task Scheduler keeps the task marked Running.
|
||||||
|
Option Explicit
|
||||||
|
Dim sh, root, node, agent
|
||||||
|
Set sh = CreateObject("WScript.Shell")
|
||||||
|
root = CreateObject("Scripting.FileSystemObject").GetParentFolderName(WScript.ScriptFullName)
|
||||||
|
root = CreateObject("Scripting.FileSystemObject").GetParentFolderName(root)
|
||||||
|
node = """C:\Program Files\nodejs\node.exe"""
|
||||||
|
agent = """" & root & "\scripts\comfy-host-agent.mjs"""
|
||||||
|
sh.CurrentDirectory = root
|
||||||
|
sh.Run node & " " & agent, 0, True
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
@echo off
|
@echo off
|
||||||
cd /d "%~dp0.."
|
REM Starts the host agent hidden. Do not leave a Command Prompt open for this.
|
||||||
node "%~dp0comfy-host-agent.mjs"
|
REM Agent stays running after idle-stop (default 30 min, override with COMFY_IDLE_MS).
|
||||||
|
start "" wscript.exe //B "%~dp0start-comfy-host-agent-hidden.vbs"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { requestComfyWake } from '~/server/utils/comfyLifecycle'
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
return await requestComfyWake()
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { checkComfyHttp, fetchQueue } from '~/server/utils/comfyLifecycle'
|
import { checkComfyHttp, fetchQueue, fetchComfyControlStatus } from '~/server/utils/comfyLifecycle'
|
||||||
import { comfyConfigured } from '~/server/utils/comfy'
|
import { comfyConfigured } from '~/server/utils/comfy'
|
||||||
import { imageComfyConfigured, getBeastImageHost, getSidecarImageHost, sameImageHost } from '~/server/utils/imageComfy'
|
import { imageComfyConfigured, getBeastImageHost, getSidecarImageHost, sameImageHost } from '~/server/utils/imageComfy'
|
||||||
import { checkImageComfyHttp, fetchQueueAt } from '~/server/utils/imageComfyLifecycle'
|
import { checkImageComfyHttp, fetchQueueAt } from '~/server/utils/imageComfyLifecycle'
|
||||||
@@ -9,7 +9,7 @@ export default defineEventHandler(async () => {
|
|||||||
const beastHost = getBeastImageHost()
|
const beastHost = getBeastImageHost()
|
||||||
const promptHost = getSidecarImageHost()
|
const promptHost = getSidecarImageHost()
|
||||||
const promptIsBeast = Boolean(promptHost && beastHost && sameImageHost(promptHost, beastHost))
|
const promptIsBeast = Boolean(promptHost && beastHost && sameImageHost(promptHost, beastHost))
|
||||||
const [comfy, queue, beast, beastQueue, prompt, promptQueue] = await Promise.all([
|
const [comfy, queue, beast, beastQueue, prompt, promptQueue, control] = await Promise.all([
|
||||||
videoConfigured
|
videoConfigured
|
||||||
? checkComfyHttp().catch(() => ({ ok: false, host: '', stats: null }))
|
? checkComfyHttp().catch(() => ({ ok: false, host: '', stats: null }))
|
||||||
: Promise.resolve({ ok: false, host: '', stats: null }),
|
: Promise.resolve({ ok: false, host: '', stats: null }),
|
||||||
@@ -21,7 +21,8 @@ export default defineEventHandler(async () => {
|
|||||||
promptHost && !promptIsBeast
|
promptHost && !promptIsBeast
|
||||||
? checkImageComfyHttp(5000, promptHost).catch(() => ({ ok: false, host: promptHost, stats: null }))
|
? checkImageComfyHttp(5000, promptHost).catch(() => ({ ok: false, host: promptHost, stats: null }))
|
||||||
: Promise.resolve({ ok: false, host: promptHost, stats: null }),
|
: Promise.resolve({ ok: false, host: promptHost, stats: null }),
|
||||||
promptHost && !promptIsBeast ? fetchQueueAt(promptHost) : Promise.resolve({ running: 0, pending: 0, ok: false })
|
promptHost && !promptIsBeast ? fetchQueueAt(promptHost) : Promise.resolve({ running: 0, pending: 0, ok: false }),
|
||||||
|
fetchComfyControlStatus().catch(() => null)
|
||||||
])
|
])
|
||||||
const comfyReachable = Boolean(comfy.ok || queue.ok)
|
const comfyReachable = Boolean(comfy.ok || queue.ok)
|
||||||
const beastReachable = Boolean(beast.ok || beastQueue.ok || comfyReachable)
|
const beastReachable = Boolean(beast.ok || beastQueue.ok || comfyReachable)
|
||||||
@@ -43,6 +44,9 @@ export default defineEventHandler(async () => {
|
|||||||
host: comfy.host || '',
|
host: comfy.host || '',
|
||||||
queueRunning: queue.running,
|
queueRunning: queue.running,
|
||||||
queuePending: queue.pending,
|
queuePending: queue.pending,
|
||||||
|
asleep: Boolean(control?.asleep) && !comfyReachable,
|
||||||
|
processUp: Boolean(control?.process ?? control?.processUp),
|
||||||
|
lastActivityAt: control?.lastActivityAt || null,
|
||||||
stats: comfy.ok ? comfy.stats : undefined
|
stats: comfy.ok ? comfy.stats : undefined
|
||||||
},
|
},
|
||||||
imageComfy: {
|
imageComfy: {
|
||||||
|
|||||||
@@ -98,17 +98,59 @@ async function controlRequest(path: string, method = 'GET') {
|
|||||||
ok?: boolean
|
ok?: boolean
|
||||||
http?: boolean
|
http?: boolean
|
||||||
process?: boolean
|
process?: boolean
|
||||||
|
processUp?: boolean
|
||||||
|
asleep?: boolean
|
||||||
|
lastActivityAt?: string | null
|
||||||
|
idleMs?: number
|
||||||
pid?: number
|
pid?: number
|
||||||
started?: boolean
|
started?: boolean
|
||||||
already?: boolean
|
already?: boolean
|
||||||
port?: number | null
|
port?: number | null
|
||||||
proxyPort?: number | null
|
proxyPort?: number | null
|
||||||
|
stopped?: boolean
|
||||||
|
error?: string
|
||||||
|
status?: number
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchComfyControlStatus() {
|
||||||
|
return await controlRequest('/status')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestComfyWake() {
|
||||||
|
const { controlUrl } = settings()
|
||||||
|
if (!controlUrl) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 503,
|
||||||
|
statusMessage: 'COMFY_CONTROL_URL is not set. The studio cannot poke the Beast host agent.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const remote = await controlRequest('/start', 'POST')
|
||||||
|
if (!remote) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 503,
|
||||||
|
statusMessage: 'Beast host agent is unreachable. Confirm it is running on the desktop.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (remote.ok === false) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: Number(remote.status) || 502,
|
||||||
|
statusMessage: 'Beast host agent refused to start Comfy.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
started: Boolean(remote.started),
|
||||||
|
already: Boolean(remote.already),
|
||||||
|
asleep: false,
|
||||||
|
port: remote.port ?? null,
|
||||||
|
proxyPort: remote.proxyPort ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function findLocalComfyProcess() {
|
async function findLocalComfyProcess() {
|
||||||
const platform = process.platform
|
const platform = process.platform
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user