| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- # -*- coding: utf-8 -*-
- """
- Command Action - Führt Shell-Befehle aus.
- """
- import asyncio
- import os
- import shlex
- from typing import Any
- from trixy_core.scheduler.action.base import Action
- from trixy_core.scheduler.form_fields import FieldType, FormField, FormValidationError
- class CommandAction(Action):
- """
- Action die einen Shell-Befehl ausführt.
- Beispiel:
- - CommandAction("echo 'Hello World'")
- - CommandAction(["python", "-c", "print('Hello')"])
- - CommandAction("cleanup.sh", cwd="/scripts", env={"DEBUG": "1"})
- Sicherheitshinweis:
- Diese Action führt System-Befehle aus.
- Nur für vertrauenswürdige Eingaben verwenden!
- """
- def __init__(
- self,
- command: str | list[str],
- cwd: str | None = None,
- env: dict[str, str] | None = None,
- shell: bool = False,
- capture_output: bool = True,
- name: str | None = None,
- timeout_seconds: float = 60.0,
- ):
- """
- Initialisiert die Action.
- Args:
- command: Befehl als String oder Liste
- cwd: Arbeitsverzeichnis
- env: Umgebungsvariablen (werden zu os.environ hinzugefügt)
- shell: Shell verwenden (Vorsicht bei untrusted input!)
- capture_output: stdout/stderr erfassen
- name: Action-Name
- timeout_seconds: Timeout
- """
- cmd_str = command if isinstance(command, str) else " ".join(command)
- super().__init__(name or f"command_{cmd_str[:30]}", timeout_seconds)
- self._command = command
- self._cwd = cwd
- self._env = env
- self._shell = shell
- self._capture_output = capture_output
- @property
- def command(self) -> str | list[str]:
- """Befehl."""
- return self._command
- @property
- def cwd(self) -> str | None:
- """Arbeitsverzeichnis."""
- return self._cwd
- async def _execute(self, context: dict) -> dict:
- """Führt den Befehl aus."""
- # Environment vorbereiten
- env = os.environ.copy()
- if self._env:
- env.update(self._env)
- # Kontext-Variablen als Env-Vars
- for key, value in context.items():
- if isinstance(value, (str, int, float, bool)):
- env[f"TRIXY_{key.upper()}"] = str(value)
- # Befehl ausführen
- if self._shell:
- cmd = self._command if isinstance(self._command, str) else " ".join(self._command)
- process = await asyncio.create_subprocess_shell(
- cmd,
- stdout=asyncio.subprocess.PIPE if self._capture_output else None,
- stderr=asyncio.subprocess.PIPE if self._capture_output else None,
- cwd=self._cwd,
- env=env,
- )
- else:
- cmd = self._command if isinstance(self._command, list) else shlex.split(self._command)
- process = await asyncio.create_subprocess_exec(
- *cmd,
- stdout=asyncio.subprocess.PIPE if self._capture_output else None,
- stderr=asyncio.subprocess.PIPE if self._capture_output else None,
- cwd=self._cwd,
- env=env,
- )
- try:
- stdout, stderr = await asyncio.wait_for(
- process.communicate(),
- timeout=self._timeout_seconds,
- )
- except asyncio.TimeoutError:
- process.kill()
- await process.wait()
- raise TimeoutError(f"Befehl-Timeout nach {self._timeout_seconds}s")
- # Ergebnis zusammenstellen
- result = {
- "return_code": process.returncode,
- "success": process.returncode == 0,
- }
- if self._capture_output:
- result["stdout"] = stdout.decode("utf-8", errors="replace") if stdout else ""
- result["stderr"] = stderr.decode("utf-8", errors="replace") if stderr else ""
- if process.returncode != 0:
- raise RuntimeError(
- f"Befehl fehlgeschlagen (code={process.returncode}): "
- f"{result.get('stderr', 'Kein Output')}"
- )
- return result
- def to_dict(self) -> dict:
- """Konvertiert zu Dictionary."""
- return {
- **self._base_dict(),
- "command": self._command,
- "cwd": self._cwd,
- "env": self._env,
- "shell": self._shell,
- "capture_output": self._capture_output,
- }
- @classmethod
- def from_dict(cls, data: dict) -> "CommandAction":
- """Erstellt Action aus Dictionary."""
- return cls(
- command=data["command"],
- cwd=data.get("cwd"),
- env=data.get("env"),
- shell=data.get("shell", False),
- capture_output=data.get("capture_output", True),
- name=data.get("name"),
- timeout_seconds=data.get("timeout_seconds", 60.0),
- )
- @classmethod
- def display_name(cls) -> str:
- """Anzeigename fuer die TUI."""
- return "Shell-Befehl"
- @classmethod
- def form_fields(cls) -> list[FormField]:
- """Gibt die Formular-Felder fuer die TUI zurueck."""
- return [
- FormField(
- name="command", label="Befehl", required=True,
- placeholder="echo 'Hello World'",
- help_text="Shell-Befehl oder Pfad",
- ),
- FormField(
- name="cwd", label="Arbeitsverzeichnis",
- placeholder="/home/user",
- ),
- FormField(
- name="shell", label="Shell verwenden",
- field_type=FieldType.CHECKBOX, default=False,
- help_text="Vorsicht: Nur fuer vertrauenswuerdige Eingaben",
- ),
- FormField(
- name="capture_output", label="Ausgabe erfassen",
- field_type=FieldType.CHECKBOX, default=True,
- ),
- FormField(
- name="timeout_seconds", label="Timeout (Sek.)",
- field_type=FieldType.NUMBER,
- default=60.0, min_value=1.0, is_float=True,
- ),
- ]
- @classmethod
- def validate_form(cls, data: dict) -> list[FormValidationError]:
- """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
- errors: list[FormValidationError] = []
- if not data.get("command", "").strip():
- errors.append(FormValidationError("command", "Befehl ist erforderlich"))
- return errors
- class ScriptAction(Action):
- """
- Action die ein Script ausführt.
- Beispiel:
- - ScriptAction("/path/to/script.py")
- - ScriptAction("backup.sh", interpreter="bash")
- """
- INTERPRETERS = {
- ".py": "python3",
- ".sh": "bash",
- ".bash": "bash",
- ".zsh": "zsh",
- ".rb": "ruby",
- ".pl": "perl",
- ".js": "node",
- }
- def __init__(
- self,
- script_path: str,
- args: list[str] | None = None,
- interpreter: str | None = None,
- cwd: str | None = None,
- env: dict[str, str] | None = None,
- name: str | None = None,
- timeout_seconds: float = 300.0,
- ):
- """
- Initialisiert die Action.
- Args:
- script_path: Pfad zum Script
- args: Argumente für das Script
- interpreter: Interpreter (wird aus Extension abgeleitet wenn None)
- cwd: Arbeitsverzeichnis
- env: Umgebungsvariablen
- name: Action-Name
- timeout_seconds: Timeout
- """
- script_name = os.path.basename(script_path)
- super().__init__(name or f"script_{script_name}", timeout_seconds)
- self._script_path = script_path
- self._args = args or []
- self._interpreter = interpreter
- self._cwd = cwd
- self._env = env
- @property
- def script_path(self) -> str:
- """Script-Pfad."""
- return self._script_path
- def _get_interpreter(self) -> str | None:
- """Ermittelt Interpreter aus Extension."""
- if self._interpreter:
- return self._interpreter
- _, ext = os.path.splitext(self._script_path)
- return self.INTERPRETERS.get(ext.lower())
- async def _execute(self, context: dict) -> dict:
- """Führt das Script aus."""
- # Prüfe ob Script existiert
- if not os.path.isfile(self._script_path):
- raise FileNotFoundError(f"Script nicht gefunden: {self._script_path}")
- # Befehl zusammenstellen
- interpreter = self._get_interpreter()
- if interpreter:
- cmd = [interpreter, self._script_path] + self._args
- else:
- # Script direkt ausführen (muss executable sein)
- cmd = [self._script_path] + self._args
- # CommandAction für Ausführung nutzen
- command_action = CommandAction(
- command=cmd,
- cwd=self._cwd or os.path.dirname(self._script_path),
- env=self._env,
- timeout_seconds=self._timeout_seconds,
- )
- return await command_action._execute(context)
- def to_dict(self) -> dict:
- """Konvertiert zu Dictionary."""
- return {
- **self._base_dict(),
- "script_path": self._script_path,
- "args": self._args,
- "interpreter": self._interpreter,
- "cwd": self._cwd,
- "env": self._env,
- }
- @classmethod
- def from_dict(cls, data: dict) -> "ScriptAction":
- """Erstellt Action aus Dictionary."""
- return cls(
- script_path=data["script_path"],
- args=data.get("args"),
- interpreter=data.get("interpreter"),
- cwd=data.get("cwd"),
- env=data.get("env"),
- name=data.get("name"),
- timeout_seconds=data.get("timeout_seconds", 300.0),
- )
- @classmethod
- def display_name(cls) -> str:
- """Anzeigename fuer die TUI."""
- return "Script ausfuehren"
- @classmethod
- def form_fields(cls) -> list[FormField]:
- """Gibt die Formular-Felder fuer die TUI zurueck."""
- return [
- FormField(
- name="script_path", label="Script-Pfad", required=True,
- placeholder="/path/to/script.py",
- ),
- FormField(
- name="args", label="Argumente",
- field_type=FieldType.TAGS,
- placeholder="--verbose, --output=/tmp",
- help_text="Komma-separierte Argumente",
- ),
- FormField(
- name="interpreter", label="Interpreter",
- placeholder="python3",
- help_text="Wird aus Dateiendung abgeleitet wenn leer",
- ),
- FormField(
- name="cwd", label="Arbeitsverzeichnis",
- ),
- FormField(
- name="timeout_seconds", label="Timeout (Sek.)",
- field_type=FieldType.NUMBER,
- default=300.0, min_value=1.0, is_float=True,
- ),
- ]
- @classmethod
- def validate_form(cls, data: dict) -> list[FormValidationError]:
- """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
- errors: list[FormValidationError] = []
- if not data.get("script_path", "").strip():
- errors.append(FormValidationError("script_path", "Script-Pfad ist erforderlich"))
- return errors
|