Subprocess and Process Spawning#
Overview#
comfy-cli launches ComfyUI and its toolchain by constructing subprocess commands with a resolved Python interpreter. The three central files are:
comfy_cli/resolve_python.pyβ interpreter resolution logiccomfy_cli/command/launch.pyβ foreground and background process spawningcomfy_cli/uv.pyβ dependency management subprocesses (pip/uv invocations)
Python Executable Resolution#
All subprocess commands that touch the ComfyUI workspace must use the workspace-local Python, not sys.executable. When comfy-cli is installed via pipx or uv tool, sys.executable points to the tool's own isolated venv β using it directly would install packages into the wrong environment. PR #375 introduced resolve_python.py to fix this.
resolve_workspace_python(workspace_path) picks the interpreter in this priority order:
$VIRTUAL_ENVenvironment variable (active virtualenv)$CONDA_PREFIX(active conda environment)- Workspace-local
.venvorvenvdirectory - Fallback to
sys.executable
On Windows, the binary path is Scripts/python.exe; on POSIX it is bin/python .
ensure_workspace_python(workspace_path) goes further: if the process is running from an isolated tool environment (pipx, uv tool) or from a PEP 668 externally-managed system Python (e.g. Ubuntu 24.04), it auto-creates a .venv inside the workspace via create_workspace_venv() before returning the interpreter path.
Key callsites: launch.py calls resolve_workspace_python at the top of launch() before every subprocess invocation. uv.py's DependencyCompiler and all static methods accept an executable parameter (defaulting to sys.executable at definition time but always overridden at callsites) .
Foreground Launch#
launch_comfyui(extra, frontend_pr, python) runs ComfyUI in the foreground using subprocess.run:
subprocess.run([python, "main.py"] + extra, env=new_env, ...)
- The working directory is already set to the workspace via
os.chdir()before calling this function . - A
__COMFY_CLI_SESSION__env var is injected for session tracking;PYTHONENCODING=utf-8is set unconditionally . - Under
--jsonmode, the child's stdout is redirected to stderr to avoid polluting the machine-readable envelope on stdout . - The loop re-runs
subprocess.runto support ComfyUI's reboot-file mechanism .
Background Launch#
comfy launch --background follows a two-process relay:
- Monitor process (
background_launchβlaunch_and_monitor): spawns a childcomfy launchprocess withCOMFY_CLI_BACKGROUND=trueset, then tails the logfile waiting for the"To see the GUI go to:"success marker. - Worker process (
launch_comfyuiwithCOMFY_CLI_BACKGROUNDset): usessubprocess.Popenwithstdout=PIPE/stderr=PIPEand redirector threads to avoid broken-pipe errors on background stdout/stderr .
Platform differences in Popen:
- Win32:
shell=True,creationflags=subprocess.CREATE_NEW_PROCESS_GROUP - POSIX: plain
Popenwithoutshell
The child's stdout/stderr are redirected into a workspace logfile (<workspace>/user/comfyui_<port>.log, truncated on each launch) rather than being held in memory. The monitor tails that file to detect the success line; after it exits, the child continues writing to the same fd .
The log file is opened with O_NOFOLLOW and 0o600 permissions to defend against symlink attacks on shared hosts .
Exits use os._exit() (via _hard_exit()) rather than sys.exit() because normal exit handlers cannot run once the redirector threads are live.
Python 3.14 Compatibility: asyncio.run()#
Issue: comfy launch --background crashed on Python 3.14 with RuntimeError: There is no current event loop in thread 'MainThread' .
Root cause: The old code in background_launch() used the two-step pattern:
loop = asyncio.get_event_loop()
log = loop.run_until_complete(launch_and_monitor(cmd, listen, port))
asyncio.get_event_loop() implicitly created an event loop on the main thread in Python β€ 3.13, deprecated in 3.10/3.12, and removed in 3.14 .
Fix (PR #481): Replaced with the single modern idiom:
log = asyncio.run(launch_and_monitor(cmd, listen, port))
asyncio.run() creates, runs, and closes its own loop β universal from Python 3.7 through 3.14+. launch_and_monitor uses only subprocess.Popen + threads internally (no asyncio tasks or async generators), so a fresh loop lifecycle is equivalent . This was the only asyncio.get_event_loop() call site in the codebase.
Do not reintroduce
asyncio.get_event_loop()or addsys.version_infoguards βasyncio.run()is already universal.
Dependency Management Subprocesses (uv.py)#
comfy_cli/uv.py wraps uv pip compile, uv pip install, uv pip sync, and pip wheel as subprocess calls using the workspace-resolved executable:
_run(cmd, cwd)βsubprocess.runwithcapture_output=True_check_call(cmd, cwd)βsubprocess.check_call; on uv pip install/sync failure, prints a hint aboutUV_LINK_MODE=copyfor network filesystems (RunPod, NFS)ensure_pip(python)β idempotent pip bootstrap; if the workspace venv has no pip (common in uv-managed venvs), runsuv pip install piporpython -m ensurepip --upgrade
All DependencyCompiler static methods prefix their command with [str(executable), "-m", "uv", "pip", ...], so the target interpreter is always explicit .