import tkinter as tk from tkinter import ttk, messagebox import os import json import sys import subprocess from datetime import datetime from typing import TYPE_CHECKING, List, Dict, Any, Optional # Importa i moduli necessari from cpp_python_debug.core.timeline_analyzer import create_timeline_from_session from cpp_python_debug.gui.timeline_window import TimelineWindow if TYPE_CHECKING: from cpp_python_debug.core.config_manager import AppSettings class DumpAnalysisTab(ttk.Frame): """ A tab for analyzing and visualizing past debug dump runs. """ def __init__(self, parent: ttk.Notebook, app_settings: "AppSettings", log_directory_path: str, **kwargs): super().__init__(parent, **kwargs) self.app_settings = app_settings self.parent_notebook = parent # Percorso della cartella principale dei dump di profilo self.profile_dumps_dir = os.path.join( log_directory_path, "profile_dumps" ) self._create_widgets() self.load_dump_runs() # Ricarica i run quando la tab diventa visibile self.bind("", lambda event: self.load_dump_runs()) def _create_widgets(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) # Frame per la tabella dei run runs_frame = ttk.LabelFrame(self, text="Available Dump Runs", padding="10") runs_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=10) runs_frame.columnconfigure(0, weight=1) runs_frame.rowconfigure(0, weight=1) # Tabella (Treeview) self.runs_tree = ttk.Treeview( runs_frame, columns=("profile_name", "start_time", "end_time", "status", "dumps"), show="headings", selectmode="browse" ) self.runs_tree.grid(row=0, column=0, sticky="nsew") # Headings self.runs_tree.heading("profile_name", text="Profile Name") self.runs_tree.heading("start_time", text="Start Time") self.runs_tree.heading("end_time", text="End Time") self.runs_tree.heading("status", text="Status") self.runs_tree.heading("dumps", text="Dumps") # Column widths self.runs_tree.column("profile_name", width=150, stretch=True) self.runs_tree.column("start_time", width=160, stretch=False) self.runs_tree.column("end_time", width=160, stretch=False) self.runs_tree.column("status", width=150, stretch=True) self.runs_tree.column("dumps", width=60, stretch=False, anchor="center") # Scrollbar scrollbar = ttk.Scrollbar(runs_frame, orient="vertical", command=self.runs_tree.yview) self.runs_tree.configure(yscrollcommand=scrollbar.set) scrollbar.grid(row=0, column=1, sticky="ns") self.runs_tree.bind("<>", self._on_run_selected) # Frame per i bottoni button_frame = ttk.Frame(self) button_frame.grid(row=1, column=0, sticky="se", padx=10, pady=(0, 10)) self.analyze_button = ttk.Button( button_frame, text="Analyze...", state=tk.DISABLED, command=self._on_analyze_clicked ) self.analyze_button.pack(side=tk.RIGHT, padx=5) self.open_folder_button = ttk.Button( button_frame, text="Open Folder", state=tk.DISABLED, command=self._open_selected_run_folder ) self.open_folder_button.pack(side=tk.RIGHT, padx=5) self.refresh_button = ttk.Button( button_frame, text="Refresh", command=self.load_dump_runs ) self.refresh_button.pack(side=tk.RIGHT) def load_dump_runs(self): """Scans the dump directory, parses summary files, and populates the treeview.""" # Clear existing items for item in self.runs_tree.get_children(): self.runs_tree.delete(item) self.analyze_button.config(state=tk.DISABLED) self.open_folder_button.config(state=tk.DISABLED) if not os.path.isdir(self.profile_dumps_dir): return run_folders = sorted( [d for d in os.listdir(self.profile_dumps_dir) if os.path.isdir(os.path.join(self.profile_dumps_dir, d))], reverse=True # Ordina dal più recente al più vecchio ) for folder_name in run_folders: run_path = os.path.join(self.profile_dumps_dir, folder_name) summary_file = self._find_summary_file(run_path) if not summary_file: continue try: with open(summary_file, 'r', encoding='utf-8') as f: summary_data = json.load(f) profile_name = summary_data.get("profile_name", "N/A") start_time = self._format_timestamp(summary_data.get("start_time")) end_time = self._format_timestamp(summary_data.get("end_time")) # Determine accurate status base_status = summary_data.get("status", "Unknown") has_errors = False if any(action.get('status', 'Success') != 'CompletedThisHit' for action in summary_data.get('actions_summary', [])): has_errors = True if not has_errors: if any(file.get('status', 'Success') != 'Success' for file in summary_data.get('files_produced_detailed', [])): has_errors = True status = base_status if has_errors and "Completed" in base_status: status = "Completed with Errors" elif has_errors: status = "Error" dump_count = len(summary_data.get("files_produced_detailed", [])) # Inserisce i dati nella tabella, passando il path come valore nascosto self.runs_tree.insert( "", tk.END, values=(profile_name, start_time, end_time, status, dump_count), iid=run_path # Usa il path come ID univoco della riga ) except (IOError, json.JSONDecodeError): continue # Salta le cartelle con sommari corrotti # Evidenzia il primo elemento (il più recente) children = self.runs_tree.get_children() if children: self.runs_tree.selection_set(children[0]) self.runs_tree.focus(children[0]) def _find_summary_file(self, dir_path: str) -> Optional[str]: for filename in os.listdir(dir_path): if 'summary' in filename and filename.endswith('.json'): return os.path.join(dir_path, filename) return None def _format_timestamp(self, ts_string: Optional[str]) -> str: if not ts_string: return "N/A" try: # Formato ISO 8601 con microsecondi dt_obj = datetime.fromisoformat(ts_string) return dt_obj.strftime('%Y-%m-%d %H:%M:%S') except (ValueError, TypeError): return ts_string # Ritorna la stringa originale se il parsing fallisce def _on_run_selected(self, event=None): """Enable the analyze button when a run is selected.""" if self.runs_tree.selection(): self.analyze_button.config(state=tk.NORMAL) self.open_folder_button.config(state=tk.NORMAL) else: self.analyze_button.config(state=tk.DISABLED) self.open_folder_button.config(state=tk.DISABLED) def _on_analyze_clicked(self): """Handles the click of the 'Analyze' button.""" selection = self.runs_tree.selection() if not selection: return session_path = selection[0] summary_file = self._find_summary_file(session_path) if not summary_file: messagebox.showerror("Error", "Could not find summary file for the selected run.", parent=self) return try: with open(summary_file, 'r', encoding='utf-8') as f: summary_data = json.load(f) except (IOError, json.JSONDecodeError) as e: messagebox.showerror("Error", f"Could not read or parse summary file:\n{e}", parent=self) return try: self.parent_notebook.config(cursor="watch") self.update_idletasks() timeline_df = create_timeline_from_session(session_path) self.parent_notebook.config(cursor="") if timeline_df.empty: messagebox.showinfo("Analysis Complete", "No valid dump data found in the selected run.", parent=self) return # Open the analysis window, now passing the full summary data TimelineWindow( self, timeline_df=timeline_df, summary_data=summary_data ) except Exception as e: self.parent_notebook.config(cursor="") messagebox.showerror("Analysis Error", f"An unexpected error occurred during analysis:\n{e}", parent=self) def _open_selected_run_folder(self): """Opens the selected run's directory in the system's file explorer.""" selection = self.runs_tree.selection() if not selection: return folder_path = selection[0] # The IID of the row is the session path try: if os.path.isdir(folder_path): if sys.platform == "win32": os.startfile(os.path.normpath(folder_path)) elif sys.platform == "darwin": subprocess.run(["open", folder_path], check=True) else: subprocess.run(["xdg-open", folder_path], check=True) else: messagebox.showwarning("Directory Not Found", f"The directory no longer exists:\n{folder_path}", parent=self) except Exception as e: messagebox.showerror("Error Opening Folder", f"Could not open the folder:\n{e}", parent=self)