mirror of
https://github.com/th30d4y/ExecuTrace.git
synced 2026-05-26 11:35:51 +00:00
first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Core models and replay logic for ExecuTrace."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
|
||||
from exectrace.core.models import Workflow
|
||||
from exectrace.storage.factory import get_storage
|
||||
from exectrace.storage.json_storage import JsonStorage
|
||||
from exectrace.storage.xml_storage import XmlStorage
|
||||
|
||||
|
||||
class WorkflowEditor:
|
||||
"""Load, modify, and save workflows programmatically."""
|
||||
|
||||
def __init__(self, storage: Union[JsonStorage, XmlStorage] | None = None) -> None:
|
||||
self.storage = storage or get_storage("json")
|
||||
|
||||
def load(self, name: str) -> Workflow:
|
||||
return self.storage.load_workflow(name)
|
||||
|
||||
def save(self, workflow: Workflow) -> None:
|
||||
self.storage.save_workflow(workflow)
|
||||
|
||||
def remove_action(self, workflow: Workflow, index: int) -> None:
|
||||
if index < 0 or index >= len(workflow.actions):
|
||||
raise IndexError("Action index out of range")
|
||||
workflow.actions.pop(index)
|
||||
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from exectrace.utils.time_utils import utc_now_iso
|
||||
|
||||
|
||||
@dataclass
|
||||
class Action:
|
||||
action_type: str
|
||||
timestamp: str
|
||||
payload: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"action_type": self.action_type,
|
||||
"timestamp": self.timestamp,
|
||||
"payload": self.payload,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "Action":
|
||||
return cls(
|
||||
action_type=data["action_type"],
|
||||
timestamp=data["timestamp"],
|
||||
payload=data.get("payload", {}),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Workflow:
|
||||
name: str
|
||||
version: str = "1.0"
|
||||
created_at: str = field(default_factory=utc_now_iso)
|
||||
updated_at: str = field(default_factory=utc_now_iso)
|
||||
actions: List[Action] = field(default_factory=list)
|
||||
|
||||
def add_action(self, action_type: str, payload: Dict[str, Any]) -> None:
|
||||
self.actions.append(Action(action_type=action_type, timestamp=utc_now_iso(), payload=payload))
|
||||
self.updated_at = utc_now_iso()
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"version": self.version,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"actions": [action.to_dict() for action in self.actions],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "Workflow":
|
||||
return cls(
|
||||
name=data["name"],
|
||||
version=data.get("version", "1.0"),
|
||||
created_at=data.get("created_at", utc_now_iso()),
|
||||
updated_at=data.get("updated_at", utc_now_iso()),
|
||||
actions=[Action.from_dict(item) for item in data.get("actions", [])],
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from exectrace.core.models import Action, Workflow
|
||||
from exectrace.storage.json_storage import JsonStorage
|
||||
from exectrace.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Replayer:
|
||||
def __init__(self, storage: JsonStorage | None = None) -> None:
|
||||
self.storage = storage or JsonStorage()
|
||||
|
||||
def _signature(self, action: Action) -> str:
|
||||
serialized = f"{action.action_type}|{action.payload}"
|
||||
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
def replay(
|
||||
self,
|
||||
workflow: Workflow,
|
||||
dry_run: bool = False,
|
||||
explain: bool = False,
|
||||
smart: bool = False,
|
||||
) -> int:
|
||||
completed = self.storage.load_replay_state(workflow.name) if smart else set()
|
||||
newly_completed = set(completed)
|
||||
|
||||
for idx, action in enumerate(workflow.actions, start=1):
|
||||
signature = self._signature(action)
|
||||
if smart and signature in completed:
|
||||
print(f"[{idx}] SKIP (smart): {action.action_type}")
|
||||
continue
|
||||
|
||||
if explain:
|
||||
print(self._explain_action(idx, action))
|
||||
|
||||
if dry_run:
|
||||
print(f"[{idx}] DRY-RUN: {action.action_type}")
|
||||
newly_completed.add(signature)
|
||||
continue
|
||||
|
||||
self._execute_action(idx, action)
|
||||
newly_completed.add(signature)
|
||||
|
||||
if smart:
|
||||
self.storage.save_replay_state(workflow.name, newly_completed)
|
||||
|
||||
return len(workflow.actions)
|
||||
|
||||
def _explain_action(self, idx: int, action: Action) -> str:
|
||||
if action.action_type == "command":
|
||||
cmd = action.payload.get("command", "")
|
||||
cwd = action.payload.get("cwd", ".")
|
||||
return f"[{idx}] Execute command in {cwd}: {cmd}"
|
||||
|
||||
path = action.payload.get("path", "")
|
||||
if action.action_type == "file_create":
|
||||
return f"[{idx}] Create file: {path}"
|
||||
if action.action_type == "file_modify":
|
||||
return f"[{idx}] Modify file: {path}"
|
||||
if action.action_type == "file_delete":
|
||||
return f"[{idx}] Delete file: {path}"
|
||||
return f"[{idx}] Unknown action: {action.action_type}"
|
||||
|
||||
def _execute_action(self, idx: int, action: Action) -> None:
|
||||
if action.action_type == "command":
|
||||
cmd = str(action.payload.get("command", "")).strip()
|
||||
cwd = str(action.payload.get("cwd", "."))
|
||||
if not cmd:
|
||||
logger.warning("[%d] Empty command skipped", idx)
|
||||
return
|
||||
print(f"[{idx}] RUN: {cmd}")
|
||||
result = subprocess.run(cmd, cwd=cwd, shell=True, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Command failed with code {result.returncode}: {cmd}")
|
||||
return
|
||||
|
||||
if action.action_type in {"file_create", "file_modify"}:
|
||||
path = Path(str(action.payload["path"]))
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
content_b64 = str(action.payload.get("content_b64", ""))
|
||||
raw = base64.b64decode(content_b64.encode("ascii"))
|
||||
path.write_bytes(raw)
|
||||
print(f"[{idx}] WRITE: {path}")
|
||||
return
|
||||
|
||||
if action.action_type == "file_delete":
|
||||
path = Path(str(action.payload["path"]))
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
elif path.exists():
|
||||
os.remove(path)
|
||||
print(f"[{idx}] DELETE: {path}")
|
||||
return
|
||||
|
||||
logger.warning("[%d] Unknown action type: %s", idx, action.action_type)
|
||||
Reference in New Issue
Block a user