mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-09-30 19:50:09 +02:00
refactor(modules-sdk): write all events to stdout as JSONL
This commit is contained in:
1 parent
c6e9557541
commit
04c8383739
5 files changed
+195
-94
No files matched your search
@@ -20,7 +20,7 @@ from typing import TYPE_CHECKING
|
|||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from fuzzforge_modules_sdk.api.constants import PATH_TO_INPUTS, PATH_TO_OUTPUTS
|
from fuzzforge_modules_sdk.api.constants import PATH_TO_INPUTS, PATH_TO_OUTPUTS
|
||||||
from fuzzforge_modules_sdk.api.models import FuzzForgeModuleResults
|
from fuzzforge_modules_sdk.api.models import FuzzForgeModuleResults, FuzzForgeModuleStatus
|
||||||
from fuzzforge_modules_sdk.api.modules.base import FuzzForgeModule
|
from fuzzforge_modules_sdk.api.modules.base import FuzzForgeModule
|
||||||
|
|
||||||
from module.models import Input, Output, CrashInfo, FuzzingStats, TargetResult
|
from module.models import Input, Output, CrashInfo, FuzzingStats, TargetResult
|
||||||
@@ -79,19 +79,19 @@ class Module(FuzzForgeModule):
|
|||||||
logger.info("cargo-fuzzer starting", resource_count=len(resources))
|
logger.info("cargo-fuzzer starting", resource_count=len(resources))
|
||||||
|
|
||||||
# Emit initial progress
|
# Emit initial progress
|
||||||
self.emit_progress(0, status="initializing", message="Setting up fuzzing environment")
|
self.emit_progress(0, status=FuzzForgeModuleStatus.INITIALIZING, message="Setting up fuzzing environment")
|
||||||
self.emit_event("module_started", resource_count=len(resources))
|
self.emit_event("module_started", resource_count=len(resources))
|
||||||
|
|
||||||
# Setup the fuzzing environment
|
# Setup the fuzzing environment
|
||||||
if not self._setup_environment(resources):
|
if not self._setup_environment(resources):
|
||||||
self.emit_progress(100, status="failed", message="Failed to setup environment")
|
self.emit_progress(100, status=FuzzForgeModuleStatus.FAILED, message="Failed to setup environment")
|
||||||
return FuzzForgeModuleResults.FAILURE
|
return FuzzForgeModuleResults.FAILURE
|
||||||
|
|
||||||
# Get list of fuzz targets
|
# Get list of fuzz targets
|
||||||
targets = self._get_fuzz_targets()
|
targets = self._get_fuzz_targets()
|
||||||
if not targets:
|
if not targets:
|
||||||
logger.error("no fuzz targets found")
|
logger.error("no fuzz targets found")
|
||||||
self.emit_progress(100, status="failed", message="No fuzz targets found")
|
self.emit_progress(100, status=FuzzForgeModuleStatus.FAILED, message="No fuzz targets found")
|
||||||
return FuzzForgeModuleResults.FAILURE
|
return FuzzForgeModuleResults.FAILURE
|
||||||
|
|
||||||
# Filter targets if specific ones were requested
|
# Filter targets if specific ones were requested
|
||||||
@@ -100,7 +100,7 @@ class Module(FuzzForgeModule):
|
|||||||
targets = [t for t in targets if t in requested]
|
targets = [t for t in targets if t in requested]
|
||||||
if not targets:
|
if not targets:
|
||||||
logger.error("none of the requested targets found", requested=list(requested))
|
logger.error("none of the requested targets found", requested=list(requested))
|
||||||
self.emit_progress(100, status="failed", message="Requested targets not found")
|
self.emit_progress(100, status=FuzzForgeModuleStatus.FAILED, message="Requested targets not found")
|
||||||
return FuzzForgeModuleResults.FAILURE
|
return FuzzForgeModuleResults.FAILURE
|
||||||
|
|
||||||
logger.info("found fuzz targets", targets=targets)
|
logger.info("found fuzz targets", targets=targets)
|
||||||
@@ -137,7 +137,7 @@ class Module(FuzzForgeModule):
|
|||||||
progress = int((i / len(targets)) * 100) if not is_continuous else 50
|
progress = int((i / len(targets)) * 100) if not is_continuous else 50
|
||||||
self.emit_progress(
|
self.emit_progress(
|
||||||
progress,
|
progress,
|
||||||
status="running",
|
status=FuzzForgeModuleStatus.RUNNING,
|
||||||
message=progress_msg,
|
message=progress_msg,
|
||||||
current_task=target,
|
current_task=target,
|
||||||
metrics={
|
metrics={
|
||||||
@@ -177,7 +177,7 @@ class Module(FuzzForgeModule):
|
|||||||
# Emit final progress
|
# Emit final progress
|
||||||
self.emit_progress(
|
self.emit_progress(
|
||||||
100,
|
100,
|
||||||
status="completed",
|
status=FuzzForgeModuleStatus.COMPLETED,
|
||||||
message=f"Fuzzing completed. Found {total_crashes} crashes.",
|
message=f"Fuzzing completed. Found {total_crashes} crashes.",
|
||||||
metrics={
|
metrics={
|
||||||
"targets_fuzzed": len(self._target_results),
|
"targets_fuzzed": len(self._target_results),
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
PATH_TO_DATA: Path = Path("/data")
|
PATH_TO_DATA: Path = Path("/fuzzforge")
|
||||||
PATH_TO_INPUTS: Path = PATH_TO_DATA.joinpath("input")
|
PATH_TO_INPUTS: Path = PATH_TO_DATA.joinpath("input")
|
||||||
PATH_TO_INPUT: Path = PATH_TO_INPUTS.joinpath("input.json")
|
PATH_TO_INPUT: Path = PATH_TO_INPUTS.joinpath("input.json")
|
||||||
PATH_TO_OUTPUTS: Path = PATH_TO_DATA.joinpath("output")
|
PATH_TO_OUTPUTS: Path = PATH_TO_DATA.joinpath("output")
|
||||||
PATH_TO_ARTIFACTS: Path = PATH_TO_OUTPUTS.joinpath("artifacts")
|
PATH_TO_ARTIFACTS: Path = PATH_TO_OUTPUTS.joinpath("artifacts")
|
||||||
PATH_TO_RESULTS: Path = PATH_TO_OUTPUTS.joinpath("results.json")
|
PATH_TO_RESULTS: Path = PATH_TO_OUTPUTS.joinpath("results.json")
|
||||||
PATH_TO_LOGS: Path = PATH_TO_OUTPUTS.joinpath("logs.jsonl")
|
PATH_TO_LOGS: Path = PATH_TO_OUTPUTS.joinpath("logs.jsonl")
|
||||||
|
|
||||||
# Streaming output paths for real-time progress
|
|
||||||
PATH_TO_PROGRESS: Path = PATH_TO_OUTPUTS.joinpath("progress.json")
|
|
||||||
PATH_TO_STREAM: Path = PATH_TO_OUTPUTS.joinpath("stream.jsonl")
|
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
"""FuzzForge modules SDK models.
|
||||||
|
|
||||||
|
This module provides backward-compatible exports for all model types.
|
||||||
|
For Core SDK compatibility, use imports from `fuzzforge_modules_sdk.api.models.mod`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
from pathlib import Path # noqa: TC003 (required by pydantic at runtime)
|
||||||
|
|
||||||
|
from pydantic import ConfigDict
|
||||||
|
|
||||||
|
# Re-export from mod.py for Core SDK compatibility
|
||||||
|
from fuzzforge_modules_sdk.api.models.mod import (
|
||||||
|
Base,
|
||||||
|
FuzzForgeModuleInputBase,
|
||||||
|
FuzzForgeModuleResource,
|
||||||
|
FuzzForgeModuleResources,
|
||||||
|
FuzzForgeModulesSettingsBase,
|
||||||
|
FuzzForgeModulesSettingsType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzForgeModuleArtifacts(StrEnum):
|
||||||
|
"""Enumeration of artifact types."""
|
||||||
|
|
||||||
|
#: The artifact is an asset.
|
||||||
|
ASSET = "asset"
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzForgeModuleArtifact(Base):
|
||||||
|
"""An artifact generated by the module during its run."""
|
||||||
|
|
||||||
|
#: The description of the artifact.
|
||||||
|
description: str
|
||||||
|
#: The type of the artifact.
|
||||||
|
kind: FuzzForgeModuleArtifacts
|
||||||
|
#: The name of the artifact.
|
||||||
|
name: str
|
||||||
|
#: The path to the artifact on disk.
|
||||||
|
path: Path
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzForgeModuleResults(StrEnum):
|
||||||
|
"""Module execution result enumeration."""
|
||||||
|
|
||||||
|
SUCCESS = "success"
|
||||||
|
FAILURE = "failure"
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzForgeModuleStatus(StrEnum):
|
||||||
|
"""Possible statuses emitted by a running module."""
|
||||||
|
|
||||||
|
#: Module is setting up its environment.
|
||||||
|
INITIALIZING = "initializing"
|
||||||
|
#: Module is actively running.
|
||||||
|
RUNNING = "running"
|
||||||
|
#: Module finished successfully.
|
||||||
|
COMPLETED = "completed"
|
||||||
|
#: Module encountered an error.
|
||||||
|
FAILED = "failed"
|
||||||
|
#: Module was stopped by the orchestrator (SIGTERM).
|
||||||
|
STOPPED = "stopped"
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzForgeModuleOutputBase(Base):
|
||||||
|
"""The (standardized) output of a FuzzForge module."""
|
||||||
|
|
||||||
|
#: The collection of artifacts generated by the module during its run.
|
||||||
|
artifacts: list[FuzzForgeModuleArtifact]
|
||||||
|
#: The path to the logs.
|
||||||
|
logs: Path
|
||||||
|
#: The result of the module's run.
|
||||||
|
result: FuzzForgeModuleResults
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Core SDK compatible exports
|
||||||
|
"Base",
|
||||||
|
"FuzzForgeModuleInputBase",
|
||||||
|
"FuzzForgeModuleResource",
|
||||||
|
"FuzzForgeModuleResources",
|
||||||
|
"FuzzForgeModulesSettingsBase",
|
||||||
|
"FuzzForgeModulesSettingsType",
|
||||||
|
# OSS-specific exports (also used in OSS modules)
|
||||||
|
"FuzzForgeModuleArtifact",
|
||||||
|
"FuzzForgeModuleArtifacts",
|
||||||
|
"FuzzForgeModuleOutputBase",
|
||||||
|
"FuzzForgeModuleResults",
|
||||||
|
"FuzzForgeModuleStatus",
|
||||||
|
]
|
||||||
+10
-42
@@ -1,3 +1,9 @@
|
|||||||
|
"""Core module models for FuzzForge modules SDK.
|
||||||
|
|
||||||
|
This module contains the base classes for module settings, inputs, and resources.
|
||||||
|
These are compatible with the fuzzforge-core SDK structure.
|
||||||
|
"""
|
||||||
|
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from pathlib import Path # noqa: TC003 (required by pydantic at runtime)
|
from pathlib import Path # noqa: TC003 (required by pydantic at runtime)
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
@@ -6,27 +12,27 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
|
|
||||||
|
|
||||||
class Base(BaseModel):
|
class Base(BaseModel):
|
||||||
"""TODO."""
|
"""Base model for all FuzzForge module types."""
|
||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
|
||||||
class FuzzForgeModulesSettingsBase(Base):
|
class FuzzForgeModulesSettingsBase(Base):
|
||||||
"""TODO."""
|
"""Base class for module settings."""
|
||||||
|
|
||||||
|
|
||||||
FuzzForgeModulesSettingsType = TypeVar("FuzzForgeModulesSettingsType", bound=FuzzForgeModulesSettingsBase)
|
FuzzForgeModulesSettingsType = TypeVar("FuzzForgeModulesSettingsType", bound=FuzzForgeModulesSettingsBase)
|
||||||
|
|
||||||
|
|
||||||
class FuzzForgeModuleResources(StrEnum):
|
class FuzzForgeModuleResources(StrEnum):
|
||||||
"""Enumeration of artifact types."""
|
"""Enumeration of resource types."""
|
||||||
|
|
||||||
#: The type of the resource is unknown or irrelevant.
|
#: The type of the resource is unknown or irrelevant.
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
class FuzzForgeModuleResource(Base):
|
class FuzzForgeModuleResource(Base):
|
||||||
"""TODO."""
|
"""A resource provided to a module as input."""
|
||||||
|
|
||||||
#: The description of the resource.
|
#: The description of the resource.
|
||||||
description: str
|
description: str
|
||||||
@@ -45,41 +51,3 @@ class FuzzForgeModuleInputBase[FuzzForgeModulesSettingsType: FuzzForgeModulesSet
|
|||||||
resources: list[FuzzForgeModuleResource]
|
resources: list[FuzzForgeModuleResource]
|
||||||
#: The settings of the module.
|
#: The settings of the module.
|
||||||
settings: FuzzForgeModulesSettingsType
|
settings: FuzzForgeModulesSettingsType
|
||||||
|
|
||||||
|
|
||||||
class FuzzForgeModuleArtifacts(StrEnum):
|
|
||||||
"""Enumeration of artifact types."""
|
|
||||||
|
|
||||||
#: The artifact is an asset.
|
|
||||||
ASSET = "asset"
|
|
||||||
|
|
||||||
|
|
||||||
class FuzzForgeModuleArtifact(Base):
|
|
||||||
"""An artifact generated by the module during its run."""
|
|
||||||
|
|
||||||
#: The description of the artifact.
|
|
||||||
description: str
|
|
||||||
#: The type of the artifact.
|
|
||||||
kind: FuzzForgeModuleArtifacts
|
|
||||||
#: The name of the artifact.
|
|
||||||
name: str
|
|
||||||
#: The path to the artifact on disk.
|
|
||||||
path: Path
|
|
||||||
|
|
||||||
|
|
||||||
class FuzzForgeModuleResults(StrEnum):
|
|
||||||
"""TODO."""
|
|
||||||
|
|
||||||
SUCCESS = "success"
|
|
||||||
FAILURE = "failure"
|
|
||||||
|
|
||||||
|
|
||||||
class FuzzForgeModuleOutputBase(Base):
|
|
||||||
"""The (standardized) output of a FuzzForge module."""
|
|
||||||
|
|
||||||
#: The collection of artifacts generated by the module during its run.
|
|
||||||
artifacts: list[FuzzForgeModuleArtifacts]
|
|
||||||
#: The path to the logs.
|
|
||||||
logs: Path
|
|
||||||
#: The result of the module's run.
|
|
||||||
result: FuzzForgeModuleResults
|
|
||||||
+87
-40
@@ -1,5 +1,7 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import json
|
import json
|
||||||
|
import signal
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
@@ -11,9 +13,7 @@ from fuzzforge_modules_sdk.api.constants import (
|
|||||||
PATH_TO_ARTIFACTS,
|
PATH_TO_ARTIFACTS,
|
||||||
PATH_TO_INPUT,
|
PATH_TO_INPUT,
|
||||||
PATH_TO_LOGS,
|
PATH_TO_LOGS,
|
||||||
PATH_TO_PROGRESS,
|
|
||||||
PATH_TO_RESULTS,
|
PATH_TO_RESULTS,
|
||||||
PATH_TO_STREAM,
|
|
||||||
)
|
)
|
||||||
from fuzzforge_modules_sdk.api.exceptions import FuzzForgeModuleError
|
from fuzzforge_modules_sdk.api.exceptions import FuzzForgeModuleError
|
||||||
from fuzzforge_modules_sdk.api.models import (
|
from fuzzforge_modules_sdk.api.models import (
|
||||||
@@ -23,6 +23,7 @@ from fuzzforge_modules_sdk.api.models import (
|
|||||||
FuzzForgeModuleOutputBase,
|
FuzzForgeModuleOutputBase,
|
||||||
FuzzForgeModuleResource,
|
FuzzForgeModuleResource,
|
||||||
FuzzForgeModuleResults,
|
FuzzForgeModuleResults,
|
||||||
|
FuzzForgeModuleStatus,
|
||||||
FuzzForgeModulesSettingsType,
|
FuzzForgeModulesSettingsType,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,6 +53,11 @@ class FuzzForgeModule(ABC):
|
|||||||
#: Custom output data set by the module.
|
#: Custom output data set by the module.
|
||||||
__output_data: dict[str, Any]
|
__output_data: dict[str, Any]
|
||||||
|
|
||||||
|
#: Event set when stop is requested (SIGTERM received).
|
||||||
|
#: Using :class:`threading.Event` so multi-threaded modules can
|
||||||
|
#: efficiently wait on it via :pymethod:`threading.Event.wait`.
|
||||||
|
__stop_requested: threading.Event
|
||||||
|
|
||||||
def __init__(self, name: str, version: str) -> None:
|
def __init__(self, name: str, version: str) -> None:
|
||||||
"""Initialize an instance of the class.
|
"""Initialize an instance of the class.
|
||||||
|
|
||||||
@@ -65,10 +71,10 @@ class FuzzForgeModule(ABC):
|
|||||||
self.__version = version
|
self.__version = version
|
||||||
self.__start_time = time.time()
|
self.__start_time = time.time()
|
||||||
self.__output_data = {}
|
self.__output_data = {}
|
||||||
|
self.__stop_requested = threading.Event()
|
||||||
# Initialize streaming output files
|
|
||||||
PATH_TO_PROGRESS.parent.mkdir(exist_ok=True, parents=True)
|
# Register SIGTERM handler for graceful shutdown
|
||||||
PATH_TO_STREAM.parent.mkdir(exist_ok=True, parents=True)
|
signal.signal(signal.SIGTERM, self._handle_sigterm)
|
||||||
|
|
||||||
@final
|
@final
|
||||||
def get_logger(self) -> BoundLogger:
|
def get_logger(self) -> BoundLogger:
|
||||||
@@ -86,6 +92,58 @@ class FuzzForgeModule(ABC):
|
|||||||
return self.__version
|
return self.__version
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
def is_stop_requested(self) -> bool:
|
||||||
|
"""Check if stop was requested (SIGTERM received).
|
||||||
|
|
||||||
|
Long-running modules should check this periodically and exit gracefully
|
||||||
|
when True. Results will be written automatically on SIGTERM.
|
||||||
|
|
||||||
|
The underlying :class:`threading.Event` can be obtained via
|
||||||
|
:meth:`stop_event` for modules that need to *wait* on it.
|
||||||
|
|
||||||
|
:returns: True if SIGTERM was received.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.__stop_requested.is_set()
|
||||||
|
|
||||||
|
@final
|
||||||
|
def stop_event(self) -> threading.Event:
|
||||||
|
"""Return the stop :class:`threading.Event`.
|
||||||
|
|
||||||
|
Multi-threaded modules can use ``self.stop_event().wait(timeout)``
|
||||||
|
instead of polling :meth:`is_stop_requested` in a busy-loop.
|
||||||
|
|
||||||
|
:returns: The threading event that is set on SIGTERM.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.__stop_requested
|
||||||
|
|
||||||
|
@final
|
||||||
|
def _handle_sigterm(self, signum: int, frame: Any) -> None:
|
||||||
|
"""Handle SIGTERM signal for graceful shutdown.
|
||||||
|
|
||||||
|
Sets the stop event and emits a final progress update, then returns.
|
||||||
|
The normal :meth:`main` lifecycle (run → cleanup → write results) will
|
||||||
|
complete as usual once :meth:`_run` observes :meth:`is_stop_requested`
|
||||||
|
and returns, giving the module a chance to do any last-minute work
|
||||||
|
before the process exits.
|
||||||
|
|
||||||
|
:param signum: Signal number.
|
||||||
|
:param frame: Current stack frame.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.__stop_requested.set()
|
||||||
|
self.get_logger().info("received SIGTERM, stopping after current operation")
|
||||||
|
|
||||||
|
# Emit final progress update
|
||||||
|
self.emit_progress(
|
||||||
|
progress=100,
|
||||||
|
status=FuzzForgeModuleStatus.STOPPED,
|
||||||
|
message="Module stopped by orchestrator (SIGTERM)",
|
||||||
|
)
|
||||||
|
|
||||||
|
@final
|
||||||
|
|
||||||
def set_output(self, **kwargs: Any) -> None:
|
def set_output(self, **kwargs: Any) -> None:
|
||||||
"""Set custom output data to be included in results.json.
|
"""Set custom output data to be included in results.json.
|
||||||
|
|
||||||
@@ -107,63 +165,53 @@ class FuzzForgeModule(ABC):
|
|||||||
def emit_progress(
|
def emit_progress(
|
||||||
self,
|
self,
|
||||||
progress: int,
|
progress: int,
|
||||||
status: str = "running",
|
status: FuzzForgeModuleStatus = FuzzForgeModuleStatus.RUNNING,
|
||||||
message: str = "",
|
message: str = "",
|
||||||
metrics: dict[str, Any] | None = None,
|
metrics: dict[str, Any] | None = None,
|
||||||
current_task: str = "",
|
current_task: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Emit a progress update to the progress file.
|
"""Emit a structured progress event to stdout (JSONL).
|
||||||
|
|
||||||
This method writes to /data/output/progress.json which can be polled
|
Progress is written as a single JSON line to stdout so that the
|
||||||
by the orchestrator or UI to show real-time progress.
|
orchestrator can capture it via ``kubectl logs`` without requiring
|
||||||
|
any file-system access inside the container.
|
||||||
|
|
||||||
:param progress: Progress percentage (0-100).
|
:param progress: Progress percentage (0-100).
|
||||||
:param status: Current status ("initializing", "running", "completed", "failed").
|
:param status: Current module status.
|
||||||
:param message: Human-readable status message.
|
:param message: Human-readable status message.
|
||||||
:param metrics: Dictionary of metrics (e.g., {"executions": 1000, "coverage": 50}).
|
:param metrics: Dictionary of metrics (e.g., {"executions": 1000, "coverage": 50}).
|
||||||
:param current_task: Name of the current task being performed.
|
:param current_task: Name of the current task being performed.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
elapsed = time.time() - self.__start_time
|
self.emit_event(
|
||||||
|
"progress",
|
||||||
progress_data = {
|
status=status.value,
|
||||||
"module": self.__name,
|
progress=max(0, min(100, progress)),
|
||||||
"version": self.__version,
|
message=message,
|
||||||
"status": status,
|
current_task=current_task,
|
||||||
"progress": max(0, min(100, progress)),
|
metrics=metrics or {},
|
||||||
"message": message,
|
)
|
||||||
"current_task": current_task,
|
|
||||||
"elapsed_seconds": round(elapsed, 2),
|
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
||||||
"metrics": metrics or {},
|
|
||||||
}
|
|
||||||
|
|
||||||
PATH_TO_PROGRESS.write_text(json.dumps(progress_data, indent=2))
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
def emit_event(self, event: str, **data: Any) -> None:
|
def emit_event(self, event: str, **data: Any) -> None:
|
||||||
"""Emit a streaming event to the stream file.
|
"""Emit a structured event to stdout as a single JSONL line.
|
||||||
|
|
||||||
This method appends to /data/output/stream.jsonl which can be tailed
|
All module events (including progress updates) are written to stdout
|
||||||
by the orchestrator or UI for real-time event streaming.
|
so the orchestrator can stream them in real time via ``kubectl logs``.
|
||||||
|
|
||||||
:param event: Event type (e.g., "crash_found", "target_started", "metrics").
|
:param event: Event type (e.g., ``"crash_found"``, ``"target_started"``,
|
||||||
|
``"progress"``, ``"metrics"``).
|
||||||
:param data: Additional event data as keyword arguments.
|
:param data: Additional event data as keyword arguments.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
elapsed = time.time() - self.__start_time
|
|
||||||
|
|
||||||
event_data = {
|
event_data = {
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
"elapsed_seconds": round(elapsed, 2),
|
"elapsed_seconds": round(self.get_elapsed_seconds(), 2),
|
||||||
"module": self.__name,
|
"module": self.__name,
|
||||||
"event": event,
|
"event": event,
|
||||||
**data,
|
**data,
|
||||||
}
|
}
|
||||||
|
print(json.dumps(event_data), flush=True)
|
||||||
# Append to stream file (create if doesn't exist)
|
|
||||||
with PATH_TO_STREAM.open("a") as f:
|
|
||||||
f.write(json.dumps(event_data) + "\n")
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
def get_elapsed_seconds(self) -> float:
|
def get_elapsed_seconds(self) -> float:
|
||||||
@@ -208,7 +256,7 @@ class FuzzForgeModule(ABC):
|
|||||||
|
|
||||||
@final
|
@final
|
||||||
def main(self) -> None:
|
def main(self) -> None:
|
||||||
"""TODO."""
|
"""Execute the module lifecycle: prepare → run → cleanup → write results."""
|
||||||
result = FuzzForgeModuleResults.SUCCESS
|
result = FuzzForgeModuleResults.SUCCESS
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -238,9 +286,8 @@ class FuzzForgeModule(ABC):
|
|||||||
result=result,
|
result=result,
|
||||||
**self.__output_data,
|
**self.__output_data,
|
||||||
)
|
)
|
||||||
buffer = output.model_dump_json().encode("utf-8")
|
|
||||||
PATH_TO_RESULTS.parent.mkdir(exist_ok=True, parents=True)
|
PATH_TO_RESULTS.parent.mkdir(exist_ok=True, parents=True)
|
||||||
PATH_TO_RESULTS.write_bytes(buffer)
|
PATH_TO_RESULTS.write_bytes(output.model_dump_json().encode("utf-8"))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
Reference in new issue
Block a user