COMFY_DIR alone manages ComfyUI: derive the launch command from the standard venv layout
This commit is contained in:
@@ -51,11 +51,14 @@ type Config struct {
|
||||
LLMBusyStatus int
|
||||
BusyRetryAfter int
|
||||
|
||||
// ComfyCmd spawns and supervises a ComfyUI server on demand (empty =
|
||||
// unmanaged, the current behavior). ComfyDir is its working directory.
|
||||
// The managed server is stopped after ComfyIdleTimeout without
|
||||
// requests, freeing its VRAM; ComfyStartTimeout bounds how long a
|
||||
// request waits for it to come up.
|
||||
// ComfyCmd spawns and supervises a ComfyUI server on demand. When
|
||||
// ComfyCmd is empty but ComfyDir is set, management is enabled with the
|
||||
// standard venv layout under ComfyDir (.venv + main.py or
|
||||
// ComfyUI/main.py; --port from the COMFY_URL port) — ComfyCmd is the
|
||||
// override for other layouts and doubles as the working directory when
|
||||
// set explicitly. The managed server is stopped after ComfyIdleTimeout
|
||||
// without requests, freeing its VRAM; ComfyStartTimeout bounds how long
|
||||
// a request waits for it to come up.
|
||||
ComfyCmd string
|
||||
ComfyDir string
|
||||
ComfyIdleTimeout time.Duration
|
||||
@@ -305,6 +308,9 @@ func Load(getenv func(string) string) (Config, error) {
|
||||
if cfg.ComfyCmd != "" && cfg.ComfyURL == "" {
|
||||
return cfg, fmt.Errorf("COMFY_CMD requires COMFY_URL to be set (the proxy needs somewhere to forward)")
|
||||
}
|
||||
if cfg.ComfyCmd == "" && cfg.ComfyDir != "" && cfg.ComfyURL == "" {
|
||||
return cfg, fmt.Errorf("COMFY_DIR without COMFY_CMD requires COMFY_URL to be set (it enables the managed ComfyUI)")
|
||||
}
|
||||
if cfg.OllamaURL == "" && cfg.ComfyURL == "" {
|
||||
return cfg, ErrNoConsumer
|
||||
}
|
||||
|
||||
@@ -222,3 +222,28 @@ func TestGameDetectionSettings(t *testing.T) {
|
||||
t.Fatal("GPUIgnoreProcs default must not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComfyDirOnlyEnablesManaged(t *testing.T) {
|
||||
// COMFY_DIR without COMFY_CMD and without COMFY_URL is a mistake.
|
||||
_, err := Load(func(k string) string {
|
||||
if k == "COMFY_DIR" {
|
||||
return `C:\ComfyUI`
|
||||
}
|
||||
return ""
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "COMFY_DIR") {
|
||||
t.Fatalf("err = %v, want COMFY_DIR/COMFY_URL validation error", err)
|
||||
}
|
||||
// With COMFY_URL it loads — the launch command is derived from the dir.
|
||||
if _, err := Load(func(k string) string {
|
||||
switch k {
|
||||
case "COMFY_DIR":
|
||||
return `C:\ComfyUI`
|
||||
case "COMFY_URL":
|
||||
return "http://127.0.0.1:8189"
|
||||
}
|
||||
return ""
|
||||
}); err != nil {
|
||||
t.Fatalf("COMFY_DIR with COMFY_URL must load: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ func sampleEntries(logFile string) []sampleEntry {
|
||||
{"OLLAMA_URL", "http://127.0.0.1:11434", "Ollama upstream URL; setting it enables the Ollama consumer (default: empty = disabled)", false},
|
||||
{"COMFY_URL", "http://127.0.0.1:8188", "ComfyUI upstream URL; setting it enables the ComfyUI consumer (default: empty = disabled)", false},
|
||||
{"WARM_MODEL", "", "Optional model to reload after an image job (default: empty = none)", false},
|
||||
{"COMFY_CMD", `"C:\ComfyUI\.venv\Scripts\python.exe" ComfyUI\main.py --port 8188`, "Spawn and supervise ComfyUI on demand: the first request starts it, it stops after COMFY_IDLE_TIMEOUT to free VRAM (default: empty = unmanaged)", false},
|
||||
{"COMFY_DIR", `C:\ComfyUI`, "Working directory for COMFY_CMD (default: empty = inherit)", false},
|
||||
{"COMFY_CMD", `"C:\ComfyUI\.venv\Scripts\python.exe" ComfyUI\main.py --port 8188`, "Spawn and supervise ComfyUI on demand: the first request starts it, it stops after COMFY_IDLE_TIMEOUT to free VRAM (default: derived from COMFY_DIR; both empty = unmanaged)", false},
|
||||
{"COMFY_DIR", `C:\ComfyUI`, "Root of a standard ComfyUI venv install (.venv + main.py): setting it alone supervises ComfyUI with the derived launch command; also the working directory for COMFY_CMD", false},
|
||||
{"COMFY_IDLE_TIMEOUT", "5m", "Stop the managed ComfyUI after this long without requests or jobs (frees VRAM)", false},
|
||||
{"COMFY_START_TIMEOUT", "2m", "How long a request waits for the managed ComfyUI to come up", false},
|
||||
{"GAME_PROCS", "cyberpunk2077.exe,hl2.exe", "While a listed process runs, the GPU counts as held by it: requests wait, Ollama unloads, managed ComfyUI stops (default: empty = disabled)", false},
|
||||
|
||||
@@ -9,13 +9,54 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ComfyLayout returns the interpreter and script path of a standard ComfyUI
|
||||
// venv install rooted at dir for the given GOOS: .venv\Scripts\python.exe
|
||||
// on Windows, .venv/bin/python elsewhere. The script is main.py — either in
|
||||
// a ComfyUI subdirectory or directly under dir, whichever exists (the
|
||||
// subdirectory form wins ties and is the default when neither exists yet,
|
||||
// so the caller's missing-file warning points at the documented layout).
|
||||
func ComfyLayout(goos, dir string) (python, script string) {
|
||||
if goos == "windows" {
|
||||
python = filepath.Join(dir, ".venv", "Scripts", "python.exe")
|
||||
} else {
|
||||
python = filepath.Join(dir, ".venv", "bin", "python")
|
||||
}
|
||||
script = filepath.Join(dir, "ComfyUI", "main.py")
|
||||
if _, err := os.Stat(script); err != nil {
|
||||
if _, err := os.Stat(filepath.Join(dir, "main.py")); err == nil {
|
||||
script = filepath.Join(dir, "main.py")
|
||||
}
|
||||
}
|
||||
return python, script
|
||||
}
|
||||
|
||||
// DefaultComfyCommand builds the launch command for the standard venv
|
||||
// layout (see ComfyLayout): the script is passed relative to dir so dir
|
||||
// stays the working directory, and --port is taken from comfyURL when the
|
||||
// URL carries one.
|
||||
func DefaultComfyCommand(goos, dir, comfyURL string) string {
|
||||
python, script := ComfyLayout(goos, dir)
|
||||
rel, err := filepath.Rel(dir, script)
|
||||
if err != nil {
|
||||
rel = script
|
||||
}
|
||||
cmd := `"` + python + `" ` + rel
|
||||
if u, err := url.Parse(comfyURL); err == nil && u.Port() != "" {
|
||||
cmd += " --port " + u.Port()
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Process is one managed child process.
|
||||
type Process struct {
|
||||
name string
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -190,3 +192,50 @@ func TestWatchIdleRespectsBusyGPU(t *testing.T) {
|
||||
t.Fatal("process was stopped while the GPU was busy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComfyLayoutAndDefaultCommand(t *testing.T) {
|
||||
// Nested layout (ComfyUI/main.py under dir) wins.
|
||||
dir := t.TempDir()
|
||||
nested := filepath.Join(dir, "ComfyUI", "main.py")
|
||||
if err := os.MkdirAll(filepath.Dir(nested), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(nested, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
python, script := ComfyLayout("windows", dir)
|
||||
if want := filepath.Join(dir, ".venv", "Scripts", "python.exe"); python != want {
|
||||
t.Errorf("python = %s, want %s", python, want)
|
||||
}
|
||||
if script != nested {
|
||||
t.Errorf("script = %s, want %s", script, nested)
|
||||
}
|
||||
cmd := DefaultComfyCommand("windows", dir, "http://127.0.0.1:8189")
|
||||
want := `"` + filepath.Join(dir, ".venv", "Scripts", "python.exe") + `" ` + filepath.Join("ComfyUI", "main.py") + " --port 8189"
|
||||
if cmd != want {
|
||||
t.Errorf("cmd = %q, want %q", cmd, want)
|
||||
}
|
||||
|
||||
// Flat layout (main.py directly under dir) is found too.
|
||||
flat := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(flat, "main.py"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, script := ComfyLayout("linux", flat); script != filepath.Join(flat, "main.py") {
|
||||
t.Errorf("flat script = %s", script)
|
||||
}
|
||||
cmd = DefaultComfyCommand("linux", flat, "http://comfy.internal")
|
||||
if strings.Contains(cmd, "--port") {
|
||||
t.Errorf("cmd = %q, want no --port for a port-less URL", cmd)
|
||||
}
|
||||
if !strings.HasSuffix(cmd, `" main.py`) {
|
||||
t.Errorf("cmd = %q, want quoted python + relative main.py", cmd)
|
||||
}
|
||||
|
||||
// Neither exists yet: default to the documented nested form so the
|
||||
// startup warning points there.
|
||||
empty := t.TempDir()
|
||||
if _, script := ComfyLayout("windows", empty); script != filepath.Join(empty, "ComfyUI", "main.py") {
|
||||
t.Errorf("missing-layout script = %s", script)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user