Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 46 additions & 11 deletions comfy_cli/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,22 +1112,57 @@ def validate_comfyui(_env_checker):
@app.command(help="Stop background ComfyUI")
@tracking.track_command()
def stop():
if constants.CONFIG_KEY_BACKGROUND not in ConfigManager().config["DEFAULT"]:
rprint("[bold red]No ComfyUI is running in the background.[/bold red]\n")
raise typer.Exit(code=1)
renderer = get_renderer()
config = ConfigManager()

bg_info = ConfigManager().background
bg_info = config.background if constants.CONFIG_KEY_BACKGROUND in config.config["DEFAULT"] else None
if not bg_info:
rprint("[bold red]No ComfyUI is running in the background.[/bold red]\n")
if renderer.is_json():
renderer.error(
code="no_background",
message="No ComfyUI is running in the background.",
command="stop",
)
else:
rprint("[bold red]No ComfyUI is running in the background.[/bold red]\n")
raise typer.Exit(code=1)
is_killed = utils.kill_all(bg_info[2])

if not is_killed:
rprint("[bold red]Failed to stop ComfyUI in the background.[/bold red]\n")
else:
rprint(f"[bold yellow]Background ComfyUI is stopped.[/bold yellow] ({bg_info[0]}:{bg_info[1]})")
host, port, pid = bg_info
is_killed = utils.kill_all(pid)

# kill_all returns False both when the process is already gone and when the
# kill genuinely fails. Only a server that is *still running* afterward is a
# real failure: keep its PID record so a later `comfy stop`/`comfy logs` can
# still target it, and report non-zero. An already-dead process is a
# successful "nothing left to stop" — clear the stale record and succeed.
if not is_killed and utils.is_running(pid):
if renderer.is_json():
renderer.error(
code="stop_failed",
message="Failed to stop ComfyUI in the background; it is still running.",
command="stop",
details={"host": host, "port": port, "pid": pid},
)
else:
rprint("[bold red]Failed to stop ComfyUI in the background.[/bold red]\n")
raise typer.Exit(code=1)

ConfigManager().remove_background()
config.remove_background()
Comment thread
mattmillerai marked this conversation as resolved.

if not renderer.is_json():
rprint(f"[bold yellow]Background ComfyUI is stopped.[/bold yellow] ({host}:{port})")

# In JSON mode, emit the envelope so programmatic callers can tell a
# successful stop from a malformed/absent response (pretty mode already
# printed the human line above; emit() is a no-op there). ``stopped`` is
# True only when we actively killed the process; an already-gone server is
# a success with ``changed=False``.
renderer.emit(
Comment thread
mattmillerai marked this conversation as resolved.
{"host": host, "port": port, "stopped": is_killed},
command="stop",
where="local",
changed=is_killed,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@app.command(help="Launch ComfyUI: ?[--background] ?[-- <extra args ...>]")
Expand Down
91 changes: 82 additions & 9 deletions comfy_cli/command/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from comfy_cli.command.custom_nodes.cm_cli_util import find_cm_cli, resolve_manager_gui_mode
from comfy_cli.config_manager import ConfigManager
from comfy_cli.env_checker import check_comfy_server_running
from comfy_cli.output import get_renderer
from comfy_cli.output import rprint as print # context-aware print: stderr in JSON mode
from comfy_cli.resolve_python import resolve_workspace_python
from comfy_cli.workspace_manager import WorkspaceManager, WorkspaceType
Expand Down Expand Up @@ -155,10 +156,18 @@ def launch(
resolved_workspace = workspace_manager.workspace_path

if not resolved_workspace:
print(
"\nComfyUI is not available.\nTo install ComfyUI, you can run:\n\n\tcomfy install\n\n",
file=sys.stderr,
)
renderer = get_renderer()
if renderer.is_json():
renderer.error(
code="workspace_not_found",
message="ComfyUI is not available.",
command="launch",
)
else:
print(
"\nComfyUI is not available.\nTo install ComfyUI, you can run:\n\n\tcomfy install\n\n",
file=sys.stderr,
)
raise typer.Exit(code=1)

if (extra is None or len(extra) == 0) and workspace_manager.workspace_type == WorkspaceType.DEFAULT:
Expand Down Expand Up @@ -189,11 +198,19 @@ def launch(


def background_launch(extra, frontend_pr=None):
renderer = get_renderer()
config_background = ConfigManager().background
if config_background is not None and utils.is_running(config_background[2]):
print(
"[bold red]ComfyUI is already running in background.\nYou cannot start more than one background service.[/bold red]\n"
)
if renderer.is_json():
renderer.error(
code="background_already_running",
message="ComfyUI is already running in background. You cannot start more than one background service.",
command="launch",
)
else:
print(
"[bold red]ComfyUI is already running in background.\nYou cannot start more than one background service.[/bold red]\n"
)
raise typer.Exit(code=1)

port = 8188
Expand All @@ -217,11 +234,43 @@ def background_launch(extra, frontend_pr=None):
try:
port = int(port)
Comment thread
mattmillerai marked this conversation as resolved.
except (TypeError, ValueError):
print(f"[bold red]Invalid --port value {port!r}; expected an integer.[/bold red]\n")
if renderer.is_json():
renderer.error(
code="invalid_port",
message=f"Invalid --port value {port!r}; expected an integer.",
command="launch",
details={"port": port},
)
else:
print(f"[bold red]Invalid --port value {port!r}; expected an integer.[/bold red]\n")
raise typer.Exit(code=1)

if not (1 <= port <= 65535):
# Out-of-range ports parse fine but never bind; surface the clear
# invalid_port signal here rather than a generic launch failure later.
if renderer.is_json():
renderer.error(
code="invalid_port",
message=f"Invalid --port value {port}; expected an integer in the range 1-65535.",
command="launch",
details={"port": port},
)
else:
print(f"[bold red]Invalid --port value {port}; expected an integer in the range 1-65535.[/bold red]\n")
raise typer.Exit(code=1)

if check_comfy_server_running(port):
print(f"[bold red]The {port} port is already in use. A new ComfyUI server cannot be launched.\n[bold red]\n")
if renderer.is_json():
renderer.error(
code="port_in_use",
message=f"The {port} port is already in use. A new ComfyUI server cannot be launched.",
command="launch",
details={"port": port},
)
else:
print(
f"[bold red]The {port} port is already in use. A new ComfyUI server cannot be launched.\n[bold red]\n"
)
raise typer.Exit(code=1)

cmd = [
Expand All @@ -248,6 +297,12 @@ def background_launch(extra, frontend_pr=None):
)

print("\n[bold red]Execution error: failed to launch ComfyUI[/bold red]\n")
if renderer.is_json():
renderer.error(
code="launch_failed",
message="Failed to launch ComfyUI in the background.",
command="launch",
)
# NOTE: os.exit(0) doesn't work
os._exit(1)

Expand Down Expand Up @@ -365,6 +420,24 @@ def _handle(line):
cfg.config["DEFAULT"][constants.CONFIG_KEY_BACKGROUND_LOG] = log_path
cfg.write_config()

# In JSON mode, emit the success envelope so programmatic callers
# (the local MCP, CI) can parse a real result instead of an empty
# stdout. No-op in pretty mode — the human line above already ran.
# Emit before os._exit(0): _write_json_line flushes, and os._exit
# skips interpreter cleanup so a buffered write would be lost.
# Guard the write so a stdout failure (e.g. BrokenPipeError if the
# caller closed the read end) still reaches the clean os._exit(0)
# below instead of unwinding the monitor with a traceback.
try:
get_renderer().emit(
{"host": listen, "port": port, "pid": process.pid, "background": True},
command="launch",
where="local",
changed=True,
)
except Exception:
pass

# NOTE: os.exit(0) doesn't work.
os._exit(0)
if logging_flag:
Expand Down
4 changes: 3 additions & 1 deletion comfy_cli/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@
# config
"comfy set-default": "set_default",
"comfy version": "version",
# background server logs
# background server lifecycle
"comfy launch": "launch",
Comment thread
mattmillerai marked this conversation as resolved.
"comfy stop": "stop",
"comfy logs": "logs",
}

Expand Down
42 changes: 42 additions & 0 deletions comfy_cli/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,48 @@ class ErrorCode:
"`comfy feedback` was run in JSON/non-interactive mode without an inline message.",
'comfy feedback "your feedback here"',
),
# --- launch / stop lifecycle ---------------------------------------------
ErrorCode(
"no_background",
"`comfy stop` found no ComfyUI running in the background (nothing to stop).",
"start one with `comfy launch --background` first",
),
ErrorCode(
"stop_failed",
"`comfy stop` tried to kill the background ComfyUI but it is still running (e.g. a "
"permission or transient error). The PID record is kept so it can be targeted again. "
"`details` carries the host/port/pid.",
"check permissions and retry `comfy stop`, or kill the pid in `details` manually",
),
ErrorCode(
"workspace_not_found",
"`comfy launch` could not resolve a ComfyUI workspace to launch.",
"run `comfy install`, or pass `--workspace <path>`",
),
ErrorCode(
"background_already_running",
"`comfy launch --background` refused because a ComfyUI is already running in the background "
"(only one background service is allowed).",
"stop it first with `comfy stop`",
),
ErrorCode(
"invalid_port",
"`comfy launch --background` was given a `--port` that isn't an integer. `details.port` carries "
"the offending value.",
"pass an integer port, e.g. `--port 8188`",
),
ErrorCode(
"port_in_use",
"`comfy launch --background` found the requested port already in use, so a new ComfyUI server "
"could not be started. `details.port` carries the port.",
"stop whatever holds the port, or launch on a different `--port`",
),
ErrorCode(
"launch_failed",
"`comfy launch --background` started ComfyUI but never saw the success marker before the process "
"exited — startup failed. The captured log is available via `comfy logs`.",
"inspect `comfy logs` for the startup error, fix it, and retry",
),
)


Expand Down
27 changes: 27 additions & 0 deletions comfy_cli/schemas/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://comfy.org/schemas/launch.json",
"title": "comfy launch --json data payload",
"description": "Emitted by `comfy launch --background` once ComfyUI has started successfully in the background. Foreground `comfy launch` blocks on the server and does not emit this envelope.",
"type": "object",
"required": ["host", "port", "pid", "background"],
"additionalProperties": true,
"properties": {
"host": {
"type": "string",
"description": "Host the background server bound to (e.g. `127.0.0.1`)."
},
"port": {
"type": "integer",
"description": "Port the background server is listening on."
},
"pid": {
"type": "integer",
"description": "Process id of the launched background ComfyUI. Pass to `comfy stop` semantics via the recorded background entry."
},
"background": {
"type": "boolean",
"description": "Always true — this payload is only emitted for `--background` launches."
}
}
}
23 changes: 23 additions & 0 deletions comfy_cli/schemas/stop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://comfy.org/schemas/stop.json",
"title": "comfy stop --json data payload",
"description": "Result of stopping the background ComfyUI started by `comfy launch --background`.",
"type": "object",
"required": ["host", "port", "stopped"],
"additionalProperties": true,
"properties": {
"host": {
"type": "string",
"description": "Host the background server was bound to (e.g. `127.0.0.1`)."
},
"port": {
"type": "integer",
"description": "Port the background server was listening on."
},
"stopped": {
"type": "boolean",
"description": "True when this call actively killed the background process. False when the server was already gone (still a success, with `changed=false`). The recorded background entry is cleared in both cases. If the kill fails while the server is still running, `comfy stop` instead emits an `ok=false` envelope with the `stop_failed` error and keeps the record."
}
}
}
Loading
Loading