Files
robertvoy-ComfyUI-Distributed/utils/process.py
T
Robert Wojciechowski 3058b11ff2 Remove list splitter/collector feature and refactor distributed modules
- Remove DistributedListSplitter and DistributedListCollector nodes
- Remove /distributed/init_list_queue and /distributed/request_list_item endpoints
- Clean up all references from prompt_transform, queue_orchestration, constants, and tests
- Refactor and reorganize distributed modules for quality improvements
2026-03-07 05:22:09 +00:00

151 lines
4.2 KiB
Python

"""
Process management utilities for ComfyUI-Distributed.
"""
import asyncio
import os
import platform
import signal
import subprocess
import threading
from collections.abc import Sequence
from typing import Any
def _run_coroutine_blocking(coro):
"""Run a coroutine in blocking contexts, even if an event loop is active."""
try:
asyncio.get_running_loop()
except RuntimeError:
return asyncio.run(coro)
outcome = {}
def _runner():
try:
outcome["value"] = asyncio.run(coro)
except Exception as exc: # pragma: no cover - thin bridge
outcome["error"] = exc
thread = threading.Thread(target=_runner, daemon=True)
thread.start()
thread.join()
error = outcome.get("error")
if error is not None:
raise error
return outcome.get("value")
class AsyncProcessAdapter:
"""Sync-friendly adapter around asyncio subprocess.Process."""
def __init__(self, process):
self._process = process
self.pid = process.pid
@property
def returncode(self):
return self._process.returncode
@property
def stdout(self):
return self._process.stdout
def poll(self):
return self._process.returncode
def terminate(self):
self._process.terminate()
def kill(self):
self._process.kill()
def wait(self, timeout=None):
async def _wait():
if timeout is None:
return await self._process.wait()
return await asyncio.wait_for(self._process.wait(), timeout=float(timeout))
return _run_coroutine_blocking(_wait())
def _normalize_stdio_value(value):
if value is subprocess.PIPE:
return asyncio.subprocess.PIPE
if value is subprocess.STDOUT:
return asyncio.subprocess.STDOUT
if value is subprocess.DEVNULL:
return asyncio.subprocess.DEVNULL
return value
def launch_process_with_timeout(
command: Sequence[str | bytes | os.PathLike[str]],
timeout_seconds: float = 10.0,
**create_kwargs: Any,
) -> AsyncProcessAdapter:
"""Launch a child process with startup timeout and sync-friendly lifecycle methods."""
if isinstance(command, (str, bytes)):
raise TypeError("Command must be a sequence, not a string")
normalized_kwargs = dict(create_kwargs)
normalized_kwargs["stdout"] = _normalize_stdio_value(normalized_kwargs.get("stdout"))
normalized_kwargs["stderr"] = _normalize_stdio_value(normalized_kwargs.get("stderr"))
async def _launch():
return await asyncio.wait_for(
asyncio.create_subprocess_exec(*command, **normalized_kwargs),
timeout=float(timeout_seconds),
)
try:
process = _run_coroutine_blocking(_launch())
except asyncio.TimeoutError as exc:
raise TimeoutError(f"Timed out launching process after {timeout_seconds} seconds") from exc
return AsyncProcessAdapter(process)
def is_process_alive(pid: int) -> bool:
"""Check if a process with given PID is still alive."""
try:
if platform.system() == "Windows":
# Windows: use tasklist
result = subprocess.run(
['tasklist', '/FI', f'PID eq {pid}'],
capture_output=True,
text=True,
timeout=5.0,
)
return str(pid) in result.stdout
else:
# Unix: send signal 0
os.kill(pid, 0)
return True
except (OSError, subprocess.SubprocessError):
return False
def terminate_process(process, timeout=5):
"""Gracefully terminate a process with timeout."""
wait_timeout = max(float(timeout), 0.1)
if process.poll() is not None:
return True
process.terminate()
try:
process.wait(timeout=wait_timeout)
return True
except (subprocess.TimeoutExpired, TimeoutError):
process.kill()
try:
process.wait(timeout=wait_timeout)
return True
except (subprocess.TimeoutExpired, TimeoutError) as exc:
raise TimeoutError(
f"Process did not terminate within {wait_timeout} seconds after kill."
) from exc
def get_python_executable():
"""Get the Python executable path."""
import sys
return sys.executable