fester/backend/executor/action_runner.py

75 lines
2.3 KiB
Python
Executable File

"""Action runner — legacy entry point for single-action execution.
F-05 remediation:
The original implementation treated ``action["cmd"]`` as a callable
and invoked it directly, which is undefined behavior for normal
shell-command actions. This module now delegates to
:func:`backend.executor.runtime_router.execute_action`, which is the
same path the pipeline engine uses — so a target routed here gets
the same real subprocess + runtime-selection logic as a target
routed through the engine.
"""
from __future__ import annotations
import logging
from typing import Any, Mapping, Optional
from backend.events.bus import EventBus
from backend.executor.runtime_router import execute_action
log = logging.getLogger(__name__)
def run_action(
action: Mapping[str, Any],
node: Mapping[str, Any],
bus: EventBus,
) -> int:
"""Execute one action on the chosen node and emit bus events.
Emits ``task_update(running)`` before dispatch and
``task_update(done|failed)`` after. Returns the subprocess exit
code (0 = success).
"""
action_name = action.get("name", "<unnamed>")
node_name = node.get("name", "localhost") if isinstance(node, Mapping) else "localhost"
bus.emit(
"task_update",
node=node_name,
action=action_name,
state="running",
)
# Normalise the action dict so runtime_router can consume it.
# Accept either ``cmd`` (legacy) or ``command`` (runtime_router
# convention). runtime_router looks at action["command"].
normalised = dict(action)
if "command" not in normalised and "cmd" in normalised:
normalised["command"] = normalised["cmd"]
workspace: Optional[str] = normalised.get("dir") or normalised.get("cwd") or "/tmp"
try:
rc = execute_action(normalised, workspace, dict(node) if isinstance(node, Mapping) else {"name": node_name})
state = "done" if rc == 0 else "failed"
except Exception as e:
log.exception("action %s crashed", action_name)
bus.emit(
"failure",
node=node_name,
action=action_name,
state="failed",
reason=f"{type(e).__name__}: {e}",
)
return 1
bus.emit(
"task_update",
node=node_name,
action=action_name,
state=state,
)
return 0 if state == "done" else 1