Files
fuzzforge_ai/cli/src/fuzzforge_cli/progress.py
Tanguy Duhamel 323a434c73 Initial commit
2025-09-29 21:26:41 +02:00

371 lines
12 KiB
Python

"""
Enhanced progress indicators and loading animations for FuzzForge CLI.
Provides rich progress bars, spinners, and status displays for all long-running operations.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import time
import asyncio
from contextlib import contextmanager
from typing import Optional, Callable, Any, Dict, List
from datetime import datetime, timedelta
from rich.console import Console
from rich.progress import (
Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn,
TimeElapsedColumn, TimeRemainingColumn, MofNCompleteColumn
)
from rich.panel import Panel
from rich.live import Live
from rich.table import Table
from rich.text import Text
from rich import box
console = Console()
class ProgressManager:
"""Enhanced progress manager with multiple progress types."""
def __init__(self):
self.progress = None
self.live = None
def create_progress(self, show_speed: bool = False, show_eta: bool = False) -> Progress:
"""Create a rich progress instance with customizable columns."""
columns = [
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(bar_width=40),
TaskProgressColumn(),
]
if show_speed:
columns.append(TextColumn("[cyan]{task.fields[speed]}/s"))
columns.extend([
TimeElapsedColumn(),
])
if show_eta:
columns.append(TimeRemainingColumn())
return Progress(*columns, console=console)
@contextmanager
def workflow_submission(self, workflow_name: str, target_path: str):
"""Progress context for workflow submission."""
with self.create_progress() as progress:
task = progress.add_task(
f"🚀 Submitting workflow: [yellow]{workflow_name}[/yellow]",
total=4
)
# Step 1: Validation
progress.update(task, description="🔍 Validating parameters...", advance=1)
yield progress, task
# Step 2: API Connection
progress.update(task, description="🌐 Connecting to API...", advance=1)
time.sleep(0.5) # Brief pause for visual feedback
# Step 3: Submission
progress.update(task, description="📤 Submitting workflow...", advance=1)
time.sleep(0.3)
# Step 4: Complete
progress.update(task, description="✅ Workflow submitted successfully!", advance=1)
@contextmanager
def data_export(self, format_type: str, record_count: int):
"""Progress context for data export operations."""
with self.create_progress(show_eta=True) as progress:
task = progress.add_task(
f"📊 Exporting {record_count} records as [yellow]{format_type.upper()}[/yellow]",
total=record_count
)
yield progress, task
@contextmanager
def file_operations(self, operation: str, file_count: int):
"""Progress context for file operations."""
with self.create_progress(show_eta=True) as progress:
task = progress.add_task(
f"📁 {operation} {file_count} files...",
total=file_count
)
yield progress, task
@contextmanager
def api_requests(self, operation: str, request_count: Optional[int] = None):
"""Progress context for API requests."""
if request_count:
with self.create_progress() as progress:
task = progress.add_task(
f"🌐 {operation}...",
total=request_count
)
yield progress, task
else:
# Indeterminate progress for unknown request count
with self.create_progress() as progress:
task = progress.add_task(
f"🌐 {operation}...",
total=None
)
yield progress, task
def create_live_stats_display(self) -> Dict[str, Any]:
"""Create a live statistics display layout."""
return {
"layout": None,
"stats_table": None,
"progress_bars": None
}
@contextmanager
def spinner(text: str, success_text: Optional[str] = None):
"""Simple spinner context manager for quick operations."""
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
console=console
) as progress:
task = progress.add_task(text, total=None)
try:
yield progress
if success_text:
progress.update(task, description=f"{success_text}")
time.sleep(0.5) # Brief pause to show success
except Exception as e:
progress.update(task, description=f"❌ Failed: {str(e)}")
time.sleep(0.5)
raise
@contextmanager
def step_progress(steps: List[str], title: str = "Processing"):
"""Multi-step progress with predefined steps."""
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(bar_width=30),
MofNCompleteColumn(),
console=console
) as progress:
task = progress.add_task(f"🔄 {title}", total=len(steps))
class StepProgressController:
def __init__(self, progress_instance, task_id):
self.progress = progress_instance
self.task = task_id
self.current_step = 0
def next_step(self):
if self.current_step < len(steps):
step_text = steps[self.current_step]
self.progress.update(
self.task,
description=f"🔄 {step_text}",
advance=1
)
self.current_step += 1
def complete(self, success_text: str = "Completed"):
self.progress.update(
self.task,
description=f"{success_text}",
completed=len(steps)
)
yield StepProgressController(progress, task)
def create_workflow_monitoring_display(run_id: str, workflow_name: str) -> Table:
"""Create a monitoring display for workflow execution."""
table = Table(show_header=False, box=box.ROUNDED)
table.add_column("Metric", style="bold cyan")
table.add_column("Value", justify="right")
table.add_row("Run ID", f"[dim]{run_id[:12]}...[/dim]")
table.add_row("Workflow", f"[yellow]{workflow_name}[/yellow]")
table.add_row("Status", "[orange]Running[/orange]")
table.add_row("Started", datetime.now().strftime("%H:%M:%S"))
return Panel.fit(
table,
title="🔄 Workflow Monitoring",
border_style="blue"
)
def create_fuzzing_progress_display(stats: Dict[str, Any]) -> Panel:
"""Create a rich display for fuzzing progress."""
# Main stats table
stats_table = Table(show_header=False, box=box.SIMPLE)
stats_table.add_column("Metric", style="bold")
stats_table.add_column("Value", justify="right", style="bold white")
stats_table.add_row("Executions", f"{stats.get('executions', 0):,}")
stats_table.add_row("Exec/sec", f"{stats.get('executions_per_sec', 0):.1f}")
stats_table.add_row("Crashes", f"[red]{stats.get('crashes', 0):,}[/red]")
stats_table.add_row("Coverage", f"{stats.get('coverage', 0):.1f}%")
# Progress bars
progress_table = Table(show_header=False, box=box.SIMPLE)
progress_table.add_column("Metric", style="bold")
progress_table.add_column("Progress", min_width=25)
# Execution rate progress (as percentage of target rate)
exec_rate = stats.get('executions_per_sec', 0)
target_rate = 1000 # Target 1000 exec/sec
exec_progress = min(100, (exec_rate / target_rate) * 100)
progress_table.add_row(
"Exec Rate",
create_progress_bar(exec_progress, color="green")
)
# Coverage progress
coverage = stats.get('coverage', 0)
progress_table.add_row(
"Coverage",
create_progress_bar(coverage, color="blue")
)
# Combine tables
combined = Table(show_header=False, box=None)
combined.add_column("Stats", ratio=1)
combined.add_column("Progress", ratio=1)
combined.add_row(stats_table, progress_table)
return Panel(
combined,
title="🎯 Fuzzing Progress",
border_style="green"
)
def create_progress_bar(percentage: float, color: str = "green", width: int = 20) -> Text:
"""Create a visual progress bar using Rich Text."""
filled = int((percentage / 100) * width)
bar = "" * filled + "" * (width - filled)
text = Text(bar, style=color)
text.append(f" {percentage:.1f}%", style="dim")
return text
def create_loading_animation(text: str) -> Live:
"""Create a loading animation with rotating spinner."""
frames = ["", "", "", "", "", "", "", "", "", ""]
frame_index = 0
def get_spinner_frame():
nonlocal frame_index
frame = frames[frame_index]
frame_index = (frame_index + 1) % len(frames)
return frame
panel = Panel(
f"{get_spinner_frame()} [bold blue]{text}[/bold blue]",
box=box.ROUNDED,
border_style="cyan"
)
return Live(panel, auto_refresh=True, refresh_per_second=10)
class WorkflowProgressTracker:
"""Advanced progress tracker for workflow execution."""
def __init__(self, workflow_name: str, run_id: str):
self.workflow_name = workflow_name
self.run_id = run_id
self.start_time = datetime.now()
self.phases = []
self.current_phase = None
def add_phase(self, name: str, description: str, estimated_duration: Optional[int] = None):
"""Add a phase to the workflow progress."""
self.phases.append({
"name": name,
"description": description,
"estimated_duration": estimated_duration,
"start_time": None,
"end_time": None,
"status": "pending"
})
def start_phase(self, phase_name: str):
"""Start a specific phase."""
for phase in self.phases:
if phase["name"] == phase_name:
phase["start_time"] = datetime.now()
phase["status"] = "running"
self.current_phase = phase_name
break
def complete_phase(self, phase_name: str, success: bool = True):
"""Complete a specific phase."""
for phase in self.phases:
if phase["name"] == phase_name:
phase["end_time"] = datetime.now()
phase["status"] = "completed" if success else "failed"
self.current_phase = None
break
def get_progress_display(self) -> Panel:
"""Get the current progress display."""
# Create progress table
table = Table(show_header=True, box=box.ROUNDED)
table.add_column("Phase", style="bold")
table.add_column("Status", justify="center")
table.add_column("Duration")
for phase in self.phases:
status_emoji = {
"pending": "",
"running": "🔄",
"completed": "",
"failed": ""
}
status_text = f"{status_emoji.get(phase['status'], '')} {phase['status'].title()}"
# Calculate duration
if phase["start_time"]:
end_time = phase["end_time"] or datetime.now()
duration = end_time - phase["start_time"]
duration_text = f"{duration.seconds}s"
else:
duration_text = "-"
table.add_row(
phase["description"],
status_text,
duration_text
)
total_duration = datetime.now() - self.start_time
title = f"🔄 {self.workflow_name} Progress (Run: {self.run_id[:8]}..., {total_duration.seconds}s)"
return Panel(
table,
title=title,
border_style="blue"
)
# Global progress manager instance
progress_manager = ProgressManager()