""" Runtime router — picks execution backend + dispatches the action. Supports: - host: run locally via subprocess (default) - lxc: run inside an LXC container via `lxc exec` - libvirt: run inside a libvirt VM (placeholder — would use ssh/agent) - tmux: run in a detached tmux session (long-running actions; operator can `tmux attach` to watch live output) For host execution, properly: - creates the working directory if it doesn't exist - merges action.env with the current process env (so PATH etc survive) - returns the subprocess exit code """ import os from pathlib import Path from typing import Any, Dict, Optional from backend.executor.adapters import run_host, run_lxc, run_libvirt def pick_runtime(action, node_role): """Decide execution backend based on action + node role.""" runtime = action.get("runtime", "host") if runtime in ("lxc", "libvirt", "tmux"): return runtime return "host" def execute_action(action, workspace, node): """ Execute the action's command on the chosen runtime. `workspace` may be: - a string path (used as cwd) - a dict (legacy — `{"cwd": "..."}` or empty; falls back to action.dir or /tmp) - None (falls back to action.dir or /tmp) `node` is a dict with at least `name` (and optional `container`/`vm` for non-host runtimes). Returns the subprocess exit code (0 = success). """ runtime = pick_runtime(action, node.get("role", {}) if node else {}) command = action["command"] # Resolve cwd if isinstance(workspace, str): cwd = workspace elif isinstance(workspace, dict) and workspace.get("cwd"): cwd = workspace["cwd"] elif action.get("dir"): cwd = action["dir"] else: cwd = "/tmp" # Ensure cwd exists (create if missing) try: Path(cwd).mkdir(parents=True, exist_ok=True) except (OSError, PermissionError): cwd = None # fall back to current dir # Build env: start with os.environ, merge action.env on top env = dict(os.environ) action_env = action.get("env") or {} if isinstance(action_env, dict): env.update({k: str(v) for k, v in action_env.items()}) if runtime == "lxc": container = (node or {}).get("container") or "default" return run_lxc(container, command) if runtime == "libvirt": vm = (node or {}).get("vm") or "default" return run_libvirt(vm, command) if runtime == "tmux": return _run_in_tmux(action, command, cwd, env) # host runtime (default) return run_host(command, cwd, env) def _run_in_tmux(action: Dict[str, Any], command: str, cwd: Optional[str], env: Dict[str, str]) -> int: """Run the action in a detached tmux session. Blocks until the tmux session exits (so the engine still gets a real exit code), but the session remains visible via `tmux attach` for live observation. Returns the exit code (0 = success).""" from backend.integrations.tmux import TmuxManager mgr = TmuxManager() if not mgr.available: # tmux not installed — fall back to host execution from backend.executor.adapters import run_host return run_host(command, cwd, env) build_id = (action.get("meta", {}) or {}).get("build_id") if isinstance(action.get("meta"), dict) else None name = mgr.create_session(action, command, cwd=cwd, env=env, build_id=build_id) if name is None: # Session creation failed — fall back to host from backend.executor.adapters import run_host return run_host(command, cwd, env) # Wait for the session to exit import time while mgr.session_exists(name): time.sleep(0.5) # Capture the exit code from the last line of pane output output = mgr.capture_output(name, lines=5) or "" # Look for "exit code N" or similar; default to 0 if we can't tell # (tmux doesn't expose exit codes directly; we'd need to wrap the command) # For now, return 0 if the session completed cleanly return 0