1037 lines
59 KiB
Python
1037 lines
59 KiB
Python
# File: cpp_python_debug/gui/main_window.py
|
|
# Provides the Tkinter GUI for interacting with the GDB session and automated profiles.
|
|
|
|
import tkinter as tk
|
|
from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu
|
|
import logging
|
|
import os
|
|
import json
|
|
import re
|
|
import threading
|
|
import subprocess
|
|
import sys
|
|
from typing import (
|
|
Optional,
|
|
Dict,
|
|
Any,
|
|
Callable,
|
|
List,
|
|
Tuple,
|
|
)
|
|
|
|
from ..core.gdb_controller import GDBSession
|
|
from ..core.output_formatter import save_to_json, save_to_csv
|
|
from ..core.config_manager import AppSettings
|
|
from ..core.profile_executor import (
|
|
ProfileExecutor,
|
|
ExecutionLogEntry,
|
|
)
|
|
from .config_window import ConfigWindow
|
|
from .profile_manager_window import ProfileManagerWindow
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from cpp_python_debug import _version as wrapper_version
|
|
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
|
|
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
|
|
except ImportError:
|
|
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
|
|
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
|
|
|
|
DEFAULT_VERSION = "0.0.0+unknown"
|
|
DEFAULT_COMMIT = "Unknown"
|
|
DEFAULT_BRANCH = "Unknown"
|
|
|
|
|
|
class GDBGui(tk.Tk):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.app_settings = AppSettings()
|
|
self.gui_log_handler: Optional[ScrolledTextLogHandler] = None
|
|
|
|
self.title(
|
|
f"GDB Debug GUI - {WRAPPER_APP_VERSION_STRING} - Settings: {os.path.basename(self.app_settings.config_filepath)} "
|
|
)
|
|
self.geometry(
|
|
self.app_settings.get_setting("gui", "main_window_geometry", "850x780")
|
|
)
|
|
|
|
self.gdb_session: Optional[GDBSession] = None
|
|
self.last_dumped_data: Any = None # Used by manual mode for saving
|
|
self.program_started_once: bool = False
|
|
|
|
self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...")
|
|
self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...")
|
|
|
|
self.exe_path_var = tk.StringVar(
|
|
value=self.app_settings.get_setting(
|
|
"general", "last_target_executable_path", ""
|
|
)
|
|
)
|
|
self.breakpoint_var = tk.StringVar(
|
|
value=self.app_settings.get_setting("general", "default_breakpoint", "main")
|
|
)
|
|
self.variable_var = tk.StringVar(
|
|
value=self.app_settings.get_setting(
|
|
"general", "default_variable_to_dump", ""
|
|
)
|
|
)
|
|
self.params_var = tk.StringVar(
|
|
value=self.app_settings.get_setting(
|
|
"general", "default_program_parameters", ""
|
|
)
|
|
)
|
|
|
|
self.profile_executor_instance: Optional[ProfileExecutor] = None
|
|
self.available_profiles_map: Dict[str, Dict[str, Any]] = {}
|
|
self.profile_exec_status_var = tk.StringVar(value="Select a profile to run.")
|
|
self.produced_files_tree: Optional[ttk.Treeview] = None
|
|
self.last_run_output_path: Optional[str] = None
|
|
self.profile_progressbar: Optional[ttk.Progressbar] = None
|
|
self.status_bar_widget: Optional[ttk.Label] = None
|
|
self.status_var: Optional[tk.StringVar] = None
|
|
|
|
self._create_menus()
|
|
self._create_widgets()
|
|
self._setup_logging_redirect_to_gui()
|
|
self._check_critical_configs_and_update_gui()
|
|
self._load_and_populate_profiles_for_automation_tab()
|
|
|
|
self.protocol("WM_DELETE_WINDOW", self._on_closing_window)
|
|
|
|
def _create_menus(self):
|
|
self.menubar = Menu(self)
|
|
self.config(menu=self.menubar)
|
|
options_menu = Menu(self.menubar, tearoff=0)
|
|
self.menubar.add_cascade(label="Options", menu=options_menu)
|
|
options_menu.add_command(
|
|
label="Configure Application...", command=self._open_config_window
|
|
)
|
|
options_menu.add_separator()
|
|
options_menu.add_command(label="Exit", command=self._on_closing_window)
|
|
profiles_menu = Menu(self.menubar, tearoff=0)
|
|
self.menubar.add_cascade(label="Profiles", menu=profiles_menu)
|
|
profiles_menu.add_command(
|
|
label="Manage Profiles...", command=self._open_profile_manager_window
|
|
)
|
|
|
|
def _open_config_window(self):
|
|
logger.debug("Opening configuration window.")
|
|
config_win = ConfigWindow(self, self.app_settings)
|
|
self.wait_window(config_win)
|
|
logger.debug("Configuration window closed.")
|
|
self._check_critical_configs_and_update_gui()
|
|
|
|
def _open_profile_manager_window(self):
|
|
logger.info("Opening Profile Manager window.")
|
|
profile_win = ProfileManagerWindow(self, self.app_settings)
|
|
self.wait_window(profile_win)
|
|
logger.info("Profile Manager window closed.")
|
|
self._load_and_populate_profiles_for_automation_tab()
|
|
|
|
def _check_critical_configs_and_update_gui(self):
|
|
logger.info(
|
|
"Checking critical configurations (GDB executable and Dumper script)."
|
|
)
|
|
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
|
|
dumper_script_path = self.app_settings.get_setting(
|
|
"general", "gdb_dumper_script_path"
|
|
)
|
|
|
|
gdb_ok = False
|
|
if gdb_exe_path and os.path.isfile(gdb_exe_path):
|
|
self.gdb_exe_status_var.set(f"GDB: {os.path.basename(gdb_exe_path)} (OK)")
|
|
gdb_ok = True
|
|
elif gdb_exe_path:
|
|
self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)")
|
|
else:
|
|
self.gdb_exe_status_var.set(
|
|
"GDB: Not Configured! Please set in Options > Configure."
|
|
)
|
|
|
|
if dumper_script_path and os.path.isfile(dumper_script_path):
|
|
self.gdb_dumper_status_var.set(
|
|
f"Dumper: {os.path.basename(dumper_script_path)} (OK)"
|
|
)
|
|
elif dumper_script_path:
|
|
self.gdb_dumper_status_var.set(
|
|
f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)"
|
|
)
|
|
else:
|
|
self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).")
|
|
|
|
if hasattr(self, "start_gdb_button"):
|
|
if gdb_ok and not (
|
|
self.profile_executor_instance
|
|
and self.profile_executor_instance.is_running
|
|
):
|
|
self.start_gdb_button.config(state=tk.NORMAL)
|
|
else:
|
|
self.start_gdb_button.config(state=tk.DISABLED)
|
|
|
|
if not gdb_ok:
|
|
self._reset_gui_to_stopped_state()
|
|
|
|
self.title(
|
|
f"GDB Debug GUI - {WRAPPER_APP_VERSION_STRING} - Settings: {os.path.basename(self.app_settings.config_filepath)}"
|
|
)
|
|
|
|
|
|
def _create_widgets(self):
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
|
self.columnconfigure(0, weight=1)
|
|
self.rowconfigure(0, weight=1)
|
|
|
|
main_frame.rowconfigure(0, weight=0)
|
|
main_frame.rowconfigure(1, weight=1)
|
|
main_frame.rowconfigure(2, weight=8)
|
|
main_frame.rowconfigure(3, weight=0)
|
|
main_frame.columnconfigure(0, weight=1)
|
|
|
|
self._create_config_status_widgets(main_frame)
|
|
self._create_mode_notebook_widgets(main_frame)
|
|
self._create_output_log_widgets(main_frame)
|
|
self._create_status_bar(main_frame)
|
|
|
|
|
|
def _create_config_status_widgets(self, parent_frame: ttk.Frame):
|
|
config_status_frame = ttk.LabelFrame(
|
|
parent_frame, text="Critical Configuration Status", padding=(10, 5, 10, 10)
|
|
)
|
|
config_status_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5, padx=0)
|
|
config_status_frame.columnconfigure(1, weight=1)
|
|
config_status_frame.columnconfigure(3, weight=1)
|
|
|
|
ttk.Label(config_status_frame, text="GDB:").grid(
|
|
row=0, column=0, sticky=tk.W, padx=(5, 0), pady=5
|
|
)
|
|
self.gdb_exe_status_label = ttk.Label(
|
|
config_status_frame,
|
|
textvariable=self.gdb_exe_status_var,
|
|
relief="sunken",
|
|
padding=(5, 2),
|
|
anchor=tk.W,
|
|
)
|
|
self.gdb_exe_status_label.grid(
|
|
row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10), pady=5
|
|
)
|
|
|
|
ttk.Label(config_status_frame, text="Dumper:").grid(
|
|
row=0, column=2, sticky=tk.W, padx=(5, 0), pady=5
|
|
)
|
|
self.gdb_dumper_status_label = ttk.Label(
|
|
config_status_frame,
|
|
textvariable=self.gdb_dumper_status_var,
|
|
relief="sunken",
|
|
padding=(5, 2),
|
|
anchor=tk.W,
|
|
)
|
|
self.gdb_dumper_status_label.grid(
|
|
row=0, column=3, sticky=(tk.W, tk.E), padx=(0, 10), pady=5
|
|
)
|
|
|
|
ttk.Button(
|
|
config_status_frame, text="Configure...", command=self._open_config_window
|
|
).grid(row=0, column=4, padx=(5, 5), pady=5, sticky=tk.E)
|
|
|
|
|
|
def _create_mode_notebook_widgets(self, parent_frame: ttk.Frame):
|
|
mode_notebook = ttk.Notebook(parent_frame)
|
|
mode_notebook.grid(row=1, column=0, columnspan=1, sticky="nsew", pady=5, padx=0)
|
|
|
|
manual_debug_frame = ttk.Frame(mode_notebook, padding="5")
|
|
mode_notebook.add(manual_debug_frame, text="Manual Debug")
|
|
self._populate_manual_debug_tab(manual_debug_frame)
|
|
|
|
self.automated_exec_frame = ttk.Frame(mode_notebook, padding="10") # type: ignore
|
|
mode_notebook.add(self.automated_exec_frame, text="Automated Profile Execution")
|
|
self._populate_automated_execution_tab(self.automated_exec_frame)
|
|
|
|
|
|
def _populate_manual_debug_tab(self, parent_tab_frame: ttk.Frame):
|
|
parent_tab_frame.columnconfigure(0, weight=1)
|
|
|
|
manual_target_settings_frame = ttk.LabelFrame(
|
|
parent_tab_frame, text="Target & Debug Session Settings", padding="10"
|
|
)
|
|
manual_target_settings_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5)
|
|
manual_target_settings_frame.columnconfigure(1, weight=1)
|
|
|
|
row_idx = 0
|
|
ttk.Label(manual_target_settings_frame, text="Target Executable:").grid(
|
|
row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
|
|
)
|
|
ttk.Entry(
|
|
manual_target_settings_frame, textvariable=self.exe_path_var, width=70
|
|
).grid(row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
|
ttk.Button(
|
|
manual_target_settings_frame,
|
|
text="Browse...",
|
|
command=self._browse_target_exe,
|
|
).grid(row=row_idx, column=2, padx=5, pady=2)
|
|
row_idx += 1
|
|
|
|
ttk.Label(manual_target_settings_frame, text="Program Parameters:").grid(
|
|
row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
|
|
)
|
|
ttk.Entry(manual_target_settings_frame, textvariable=self.params_var).grid(
|
|
row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2
|
|
)
|
|
row_idx += 1
|
|
|
|
ttk.Label(manual_target_settings_frame, text="Breakpoint Location:").grid(
|
|
row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
|
|
)
|
|
ttk.Entry(manual_target_settings_frame, textvariable=self.breakpoint_var).grid(
|
|
row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2
|
|
)
|
|
row_idx += 1
|
|
bp_help_text = "Examples: main, myfile.cpp:123, MyClass::myMethod"
|
|
ttk.Label(
|
|
manual_target_settings_frame,
|
|
text=bp_help_text,
|
|
foreground="gray",
|
|
font=("TkDefaultFont", 8),
|
|
).grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 5))
|
|
row_idx += 1
|
|
|
|
ttk.Label(manual_target_settings_frame, text="Variable/Expression:").grid(
|
|
row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
|
|
)
|
|
ttk.Entry(manual_target_settings_frame, textvariable=self.variable_var).grid(
|
|
row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2
|
|
)
|
|
|
|
manual_session_control_frame = ttk.LabelFrame(
|
|
parent_tab_frame, text="Session Control", padding="10"
|
|
)
|
|
manual_session_control_frame.grid(
|
|
row=1, column=0, sticky=(tk.W, tk.E), pady=(10, 5)
|
|
)
|
|
button_flow_frame = ttk.Frame(manual_session_control_frame)
|
|
button_flow_frame.pack(fill=tk.X, expand=True)
|
|
|
|
self.start_gdb_button = ttk.Button(
|
|
button_flow_frame,
|
|
text="1. Start GDB",
|
|
command=self._start_gdb_session_action,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.start_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
|
|
|
|
self.set_bp_button = ttk.Button(
|
|
button_flow_frame,
|
|
text="2. Set BP",
|
|
command=self._set_gdb_breakpoint_action,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.set_bp_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
|
|
|
|
self.run_button = ttk.Button(
|
|
button_flow_frame,
|
|
text="3. Run",
|
|
command=self._run_or_continue_gdb_action,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.run_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
|
|
|
|
self.dump_var_button = ttk.Button(
|
|
button_flow_frame,
|
|
text="4. Dump Var",
|
|
command=self._dump_gdb_variable_action,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.dump_var_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
|
|
|
|
self.stop_gdb_button = ttk.Button(
|
|
button_flow_frame,
|
|
text="Stop GDB",
|
|
command=self._stop_gdb_session_action,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.stop_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
|
|
|
|
manual_save_data_frame = ttk.LabelFrame(
|
|
parent_tab_frame, text="Save Dumped Data", padding="10"
|
|
)
|
|
manual_save_data_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
|
|
|
|
self.save_json_button = ttk.Button(
|
|
manual_save_data_frame,
|
|
text="Save as JSON",
|
|
command=lambda: self._save_dumped_data("json"),
|
|
state=tk.DISABLED,
|
|
)
|
|
self.save_json_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.save_csv_button = ttk.Button(
|
|
manual_save_data_frame,
|
|
text="Save as CSV",
|
|
command=lambda: self._save_dumped_data("csv"),
|
|
state=tk.DISABLED,
|
|
)
|
|
self.save_csv_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
|
|
def _populate_automated_execution_tab(self, parent_tab_frame: ttk.Frame) -> None:
|
|
parent_tab_frame.columnconfigure(0, weight=1)
|
|
parent_tab_frame.rowconfigure(0, weight=0)
|
|
parent_tab_frame.rowconfigure(1, weight=0)
|
|
parent_tab_frame.rowconfigure(2, weight=1)
|
|
parent_tab_frame.rowconfigure(3, weight=0)
|
|
|
|
auto_control_frame = ttk.LabelFrame(parent_tab_frame, text="Profile Execution Control", padding="10")
|
|
auto_control_frame.grid(row=0, column=0, sticky="ew", pady=5)
|
|
auto_control_frame.columnconfigure(1, weight=1)
|
|
|
|
ttk.Label(auto_control_frame, text="Select Profile:").grid(row=0, column=0, padx=(5,2), pady=5, sticky="w")
|
|
self.profile_selection_combo = ttk.Combobox(auto_control_frame, state="readonly", width=35, textvariable=tk.StringVar())
|
|
self.profile_selection_combo.grid(row=0, column=1, padx=(0,5), pady=5, sticky="ew")
|
|
|
|
self.run_profile_button = ttk.Button(auto_control_frame, text="Run Profile", command=self._run_selected_profile_action, state=tk.DISABLED)
|
|
self.run_profile_button.grid(row=0, column=2, padx=(0,2), pady=5, sticky="ew")
|
|
self.stop_profile_button = ttk.Button(auto_control_frame, text="Stop Profile", command=self._stop_current_profile_action, state=tk.DISABLED)
|
|
self.stop_profile_button.grid(row=0, column=3, padx=(0,5), pady=5, sticky="ew")
|
|
|
|
progress_status_frame = ttk.Frame(parent_tab_frame)
|
|
progress_status_frame.grid(row=1, column=0, sticky="ew", pady=(5,0))
|
|
progress_status_frame.columnconfigure(0, weight=1)
|
|
progress_status_frame.rowconfigure(0, weight=1)
|
|
progress_status_frame.rowconfigure(1, weight=0)
|
|
|
|
self.profile_exec_status_label_big = ttk.Label(progress_status_frame,
|
|
textvariable=self.profile_exec_status_var,
|
|
font=("TkDefaultFont", 10, "bold"),
|
|
anchor=tk.NW,
|
|
justify=tk.LEFT)
|
|
self.profile_exec_status_label_big.grid(row=0, column=0, sticky="new", padx=5, pady=(0,2))
|
|
|
|
def _configure_wraplength_for_status_label(event):
|
|
new_width = event.width - 15
|
|
if new_width > 20:
|
|
if hasattr(self, 'profile_exec_status_label_big') and self.profile_exec_status_label_big.winfo_exists():
|
|
self.profile_exec_status_label_big.config(wraplength=new_width)
|
|
progress_status_frame.bind("<Configure>", _configure_wraplength_for_status_label)
|
|
|
|
self.profile_progressbar = ttk.Progressbar(progress_status_frame, orient=tk.HORIZONTAL, mode='indeterminate')
|
|
# Note: Progressbar is gridded/removed by run/finish methods
|
|
|
|
produced_files_frame = ttk.LabelFrame(parent_tab_frame, text="Produced Files Log", padding="10")
|
|
produced_files_frame.grid(row=2, column=0, sticky="nsew", pady=(5,0))
|
|
produced_files_frame.columnconfigure(0, weight=1)
|
|
produced_files_frame.rowconfigure(0, weight=1)
|
|
|
|
self.produced_files_tree = ttk.Treeview(produced_files_frame,
|
|
columns=("timestamp", "breakpoint_spec", "variable", "file", "status", "details"),
|
|
show="headings", selectmode="browse")
|
|
self.produced_files_tree.grid(row=0, column=0, sticky="nsew")
|
|
|
|
self.produced_files_tree.heading("timestamp", text="Time", anchor=tk.W)
|
|
self.produced_files_tree.heading("breakpoint_spec", text="Breakpoint Spec", anchor=tk.W)
|
|
self.produced_files_tree.heading("variable", text="Variable", anchor=tk.W)
|
|
self.produced_files_tree.heading("file", text="File Produced", anchor=tk.W)
|
|
self.produced_files_tree.heading("status", text="Status", anchor=tk.W)
|
|
self.produced_files_tree.heading("details", text="Details", anchor=tk.W)
|
|
|
|
self.produced_files_tree.column("timestamp", width=130, minwidth=120, stretch=False)
|
|
self.produced_files_tree.column("breakpoint_spec", width=150, minwidth=100, stretch=True)
|
|
self.produced_files_tree.column("variable", width=150, minwidth=100, stretch=True)
|
|
self.produced_files_tree.column("file", width=180, minwidth=150, stretch=True)
|
|
self.produced_files_tree.column("status", width=80, minwidth=60, stretch=False)
|
|
self.produced_files_tree.column("details", width=180, minwidth=150, stretch=True)
|
|
|
|
tree_scrollbar_y = ttk.Scrollbar(produced_files_frame, orient=tk.VERTICAL, command=self.produced_files_tree.yview)
|
|
tree_scrollbar_y.grid(row=0, column=1, sticky="ns")
|
|
tree_scrollbar_x = ttk.Scrollbar(produced_files_frame, orient=tk.HORIZONTAL, command=self.produced_files_tree.xview)
|
|
tree_scrollbar_x.grid(row=1, column=0, sticky="ew")
|
|
self.produced_files_tree.configure(yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set)
|
|
|
|
folder_button_frame = ttk.Frame(parent_tab_frame)
|
|
folder_button_frame.grid(row=3, column=0, sticky="e", pady=(5,0))
|
|
self.open_output_folder_button = ttk.Button(folder_button_frame, text="Open Output Folder", command=self._open_last_run_output_folder, state=tk.DISABLED)
|
|
self.open_output_folder_button.pack(side=tk.RIGHT, padx=5, pady=0)
|
|
|
|
|
|
def _load_and_populate_profiles_for_automation_tab(self):
|
|
self.available_profiles_map.clear()
|
|
profiles_list = self.app_settings.get_profiles()
|
|
|
|
profile_display_names = []
|
|
for profile_item in profiles_list:
|
|
name = profile_item.get("profile_name")
|
|
if name:
|
|
self.available_profiles_map[name] = profile_item
|
|
profile_display_names.append(name)
|
|
|
|
sorted_names = sorted(profile_display_names)
|
|
self.profile_selection_combo["values"] = sorted_names
|
|
|
|
if sorted_names:
|
|
self.profile_selection_combo.set(sorted_names[0])
|
|
if not (self.profile_executor_instance and self.profile_executor_instance.is_running):
|
|
self.run_profile_button.config(state=tk.NORMAL)
|
|
self.profile_exec_status_var.set(f"Ready to run profile: {self.profile_selection_combo.get()}")
|
|
else:
|
|
self.profile_selection_combo.set("")
|
|
self.run_profile_button.config(state=tk.DISABLED)
|
|
self.profile_exec_status_var.set("No profiles. Create one via 'Profiles > Manage Profiles'.")
|
|
|
|
|
|
def _create_output_log_widgets(self, parent_frame: ttk.Frame):
|
|
output_log_notebook = ttk.Notebook(parent_frame)
|
|
output_log_notebook.grid(row=2, column=0, columnspan=1, sticky="nsew", pady=(5,0), padx=0)
|
|
log_text_height = 12
|
|
|
|
self.gdb_raw_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9))
|
|
output_log_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output")
|
|
|
|
self.parsed_json_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9))
|
|
output_log_notebook.add(self.parsed_json_output_text, text="Parsed JSON/Status Output") # MODIFIED TAB NAME
|
|
|
|
self.app_log_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9))
|
|
output_log_notebook.add(self.app_log_text, text="Application Log")
|
|
|
|
|
|
def _create_status_bar(self, parent_frame: ttk.Frame):
|
|
self.status_var = tk.StringVar(value="Ready. Configure GDB via Options menu if needed.")
|
|
self.status_bar_widget = ttk.Label(parent_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
|
|
self.status_bar_widget.grid(row=3, column=0, columnspan=1, sticky=(tk.W, tk.E), pady=(5,0), ipady=2, padx=0)
|
|
|
|
|
|
def _setup_logging_redirect_to_gui(self):
|
|
if not hasattr(self, "app_log_text") or not self.app_log_text:
|
|
logger.error("app_log_text widget not available for GUI logging setup.")
|
|
return
|
|
self.gui_log_handler = ScrolledTextLogHandler(self.app_log_text)
|
|
formatter = logging.Formatter('%(asctime)s [%(levelname)-7s] %(name)s: %(message)s', datefmt='%H:%M:%S')
|
|
self.gui_log_handler.setFormatter(formatter)
|
|
self.gui_log_handler.setLevel(logging.INFO)
|
|
logging.getLogger().addHandler(self.gui_log_handler)
|
|
|
|
|
|
def _browse_file(self, title: str, target_var: tk.StringVar, filetypes: Optional[List[Tuple[str, Any]]]=None):
|
|
current_path = target_var.get()
|
|
initial_dir = os.path.dirname(current_path) if current_path and os.path.exists(os.path.dirname(current_path)) else None
|
|
path = filedialog.askopenfilename(title=title, filetypes=filetypes or [("All files", "*.*")], initialdir=(initial_dir if initial_dir else None), parent=self)
|
|
if path:
|
|
target_var.set(path)
|
|
|
|
def _browse_target_exe(self):
|
|
self._browse_file("Select Target Application Executable", self.exe_path_var, [("Executable files", ("*.exe", "*")), ("All files", "*.*")])
|
|
|
|
|
|
def _update_gdb_raw_output(self, text: str, append: bool = True):
|
|
if not hasattr(self, "gdb_raw_output_text") or not self.gdb_raw_output_text.winfo_exists(): return
|
|
self.gdb_raw_output_text.config(state=tk.NORMAL)
|
|
if append: self.gdb_raw_output_text.insert(tk.END, str(text) + "\n")
|
|
else: self.gdb_raw_output_text.delete("1.0", tk.END); self.gdb_raw_output_text.insert("1.0", str(text))
|
|
self.gdb_raw_output_text.see(tk.END)
|
|
self.gdb_raw_output_text.config(state=tk.DISABLED)
|
|
|
|
def _update_parsed_json_output(self, data_to_display: Any):
|
|
# This method now handles both full JSON (manual mode) and status JSON (profile mode)
|
|
if not hasattr(self, "parsed_json_output_text") or not self.parsed_json_output_text.winfo_exists():
|
|
return
|
|
|
|
self.parsed_json_output_text.config(state=tk.NORMAL)
|
|
self.parsed_json_output_text.delete("1.0", tk.END)
|
|
|
|
if data_to_display is None:
|
|
self.parsed_json_output_text.insert("1.0", "<No data to display>")
|
|
elif isinstance(data_to_display, dict):
|
|
# Check if it's a status payload from the dumper (used in profile mode)
|
|
if "status" in data_to_display and ("filepath_written" in data_to_display or "message" in data_to_display):
|
|
status_text = f"Status: {data_to_display.get('status', 'N/A')}\n"
|
|
status_text += f"Variable: {data_to_display.get('variable_dumped', 'N/A')}\n"
|
|
status_text += f"File Written: {data_to_display.get('filepath_written', 'N/A')}\n"
|
|
status_text += f"Requested Format: {data_to_display.get('target_format_requested', 'N/A')}\n"
|
|
status_text += f"Message: {data_to_display.get('message', 'N/A')}\n"
|
|
if data_to_display.get("details"):
|
|
status_text += f"Details: {data_to_display.get('details')}\n"
|
|
self.parsed_json_output_text.insert("1.0", status_text)
|
|
else: # Assume it's full JSON data (manual mode)
|
|
try:
|
|
pretty_json = json.dumps(data_to_display, indent=2, ensure_ascii=False)
|
|
self.parsed_json_output_text.insert("1.0", pretty_json)
|
|
except Exception as e:
|
|
logger.error(f"Error pretty-printing JSON for GUI: {e}")
|
|
self.parsed_json_output_text.insert("1.0", f"Error displaying JSON: {e}\nRaw data: {str(data_to_display)}")
|
|
elif isinstance(data_to_display, list): # Could be full JSON that is a list
|
|
try:
|
|
pretty_json = json.dumps(data_to_display, indent=2, ensure_ascii=False)
|
|
self.parsed_json_output_text.insert("1.0", pretty_json)
|
|
except Exception as e:
|
|
logger.error(f"Error pretty-printing list for GUI: {e}")
|
|
self.parsed_json_output_text.insert("1.0", f"Error displaying list: {e}\nRaw data: {str(data_to_display)}")
|
|
else: # Primitive or other types
|
|
self.parsed_json_output_text.insert("1.0", str(data_to_display))
|
|
|
|
self.parsed_json_output_text.see("1.0")
|
|
self.parsed_json_output_text.config(state=tk.DISABLED)
|
|
|
|
|
|
def _update_status_bar(self, message: str, is_error: bool = False):
|
|
if hasattr(self, "status_var") and self.status_var is not None:
|
|
self.status_var.set(message)
|
|
# Optionally change color based on is_error, but status_bar_widget needs to be stored for this
|
|
# if self.status_bar_widget:
|
|
# self.status_bar_widget.config(foreground="red" if is_error else "black")
|
|
|
|
def _handle_gdb_operation_error(self, operation_name: str, error_details: Any):
|
|
error_message = f"Error during GDB operation '{operation_name}': {error_details}"
|
|
logger.error(error_message)
|
|
self._update_gdb_raw_output(f"ERROR: {error_message}\n", append=True)
|
|
self._update_status_bar(f"Error: {operation_name} failed.", is_error=True)
|
|
if self.winfo_exists():
|
|
messagebox.showerror("GDB Operation Error", error_message, parent=self)
|
|
|
|
def _start_gdb_session_action(self):
|
|
if self.profile_executor_instance and self.profile_executor_instance.is_running:
|
|
messagebox.showwarning("Profile Running", "An automated profile is running. Please stop it first.", parent=self)
|
|
return
|
|
|
|
gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path")
|
|
target_exe = self.exe_path_var.get()
|
|
gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path")
|
|
|
|
if not gdb_exe or not os.path.isfile(gdb_exe):
|
|
messagebox.showerror("Configuration Error", "GDB executable path not configured correctly.", parent=self)
|
|
self._check_critical_configs_and_update_gui(); return
|
|
if not target_exe or not os.path.exists(target_exe):
|
|
messagebox.showerror("Input Error", "Target executable path is required or not found.", parent=self)
|
|
return
|
|
|
|
dumper_options = self.app_settings.get_category_settings("dumper_options", {})
|
|
|
|
if self.gdb_session and self.gdb_session.is_alive():
|
|
messagebox.showwarning("Session Active", "A GDB session is already active. Stop it first.", parent=self)
|
|
return
|
|
|
|
self._update_status_bar("Starting GDB session..."); self._update_gdb_raw_output("Attempting to start GDB session...\n", append=False); self._update_parsed_json_output(None)
|
|
try:
|
|
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30)
|
|
quit_timeout_on_no_symbols = self.app_settings.get_setting("timeouts", "gdb_quit", 10)
|
|
self.gdb_session = GDBSession(gdb_path=gdb_exe, executable_path=target_exe, gdb_script_full_path=gdb_script, dumper_options=dumper_options)
|
|
self.gdb_session.start(timeout=startup_timeout)
|
|
self._update_gdb_raw_output(f"GDB session started for '{os.path.basename(target_exe)}'.\n")
|
|
|
|
if not self.gdb_session.symbols_found:
|
|
self._update_gdb_raw_output("ERROR: No debugging symbols found. Session terminated.\n", append=True)
|
|
if self.winfo_exists(): messagebox.showwarning("No Debug Symbols", f"No debug symbols in '{os.path.basename(target_exe)}'. Session aborted.", parent=self)
|
|
self._update_status_bar("GDB aborted: No debug symbols.", is_error=True)
|
|
if self.gdb_session.is_alive(): self.gdb_session.quit(timeout=quit_timeout_on_no_symbols)
|
|
self.gdb_session = None; self._reset_gui_to_stopped_state(); self._check_critical_configs_and_update_gui(); return
|
|
|
|
if gdb_script and os.path.isfile(gdb_script):
|
|
if self.gdb_session.gdb_script_sourced_successfully:
|
|
self._update_gdb_raw_output(f"Dumper script '{os.path.basename(gdb_script)}' sourced successfully.\n", append=True)
|
|
self._update_status_bar(f"GDB active. Dumper '{os.path.basename(gdb_script)}' loaded.")
|
|
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Loaded)")
|
|
else:
|
|
self._update_gdb_raw_output(f"Warning: Dumper script '{os.path.basename(gdb_script)}' FAILED to load.\n", append=True)
|
|
self._update_status_bar(f"GDB active. Dumper script load issue.", is_error=True)
|
|
if self.winfo_exists(): messagebox.showwarning("Dumper Script Issue", f"Dumper '{os.path.basename(gdb_script)}' failed to load. JSON dump affected.", parent=self)
|
|
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)")
|
|
elif gdb_script: # Path specified but not valid file
|
|
self._update_gdb_raw_output(f"Warning: Dumper script path '{gdb_script}' is invalid.\n", append=True)
|
|
self._update_status_bar(f"GDB active. Dumper script path invalid.", is_error=True)
|
|
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Path Invalid!)")
|
|
else:
|
|
self._update_gdb_raw_output("No dumper script. JSON dump via script unavailable.\n", append=True)
|
|
self._update_status_bar("GDB session active. No dumper script.")
|
|
self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).")
|
|
|
|
self.start_gdb_button.config(state=tk.DISABLED); self.set_bp_button.config(state=tk.NORMAL)
|
|
self.run_button.config(state=tk.DISABLED, text="3. Run Program"); self.dump_var_button.config(state=tk.DISABLED)
|
|
self.stop_gdb_button.config(state=tk.NORMAL)
|
|
if hasattr(self, 'run_profile_button'): self.run_profile_button.config(state=tk.DISABLED)
|
|
if hasattr(self, 'profile_selection_combo'): self.profile_selection_combo.config(state=tk.DISABLED)
|
|
self.program_started_once = False; self.last_dumped_data = None; self._disable_save_buttons()
|
|
|
|
except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific:
|
|
self._handle_gdb_operation_error("start session", e_specific); self.gdb_session = None
|
|
self._reset_gui_to_stopped_state(); self._check_critical_configs_and_update_gui()
|
|
except Exception as e:
|
|
logger.critical(f"MAIN_WINDOW CATCH-ALL for start GDB: {type(e).__name__}: '{e}'", exc_info=True)
|
|
self._handle_gdb_operation_error("start session (unexpected)", e); self.gdb_session = None
|
|
self._reset_gui_to_stopped_state(); self._check_critical_configs_and_update_gui()
|
|
|
|
|
|
def _set_gdb_breakpoint_action(self):
|
|
if not self.gdb_session or not self.gdb_session.is_alive():
|
|
messagebox.showerror("Error", "GDB session is not active.", parent=self); return
|
|
bp_location = self.breakpoint_var.get()
|
|
if not bp_location: messagebox.showerror("Input Error", "Breakpoint location cannot be empty.", parent=self); return
|
|
|
|
self._update_status_bar(f"Setting breakpoint at '{bp_location}'...")
|
|
try:
|
|
command_timeout = self.app_settings.get_setting("timeouts", "gdb_command")
|
|
output = self.gdb_session.set_breakpoint(bp_location, timeout=command_timeout)
|
|
self._update_gdb_raw_output(output, append=True)
|
|
bp_name_display = bp_location[:20] + "..." if len(bp_location) > 20 else bp_location
|
|
if "Breakpoint" in output and "not defined" not in output.lower() and "pending" not in output.lower():
|
|
self.set_bp_button.config(text=f"BP: {bp_name_display} (Set)"); self.run_button.config(state=tk.NORMAL)
|
|
self._update_status_bar(f"Breakpoint set at '{bp_location}'.")
|
|
elif "pending" in output.lower():
|
|
self.set_bp_button.config(text=f"BP: {bp_name_display} (Pend)"); self.run_button.config(state=tk.NORMAL)
|
|
self._update_status_bar(f"BP '{bp_location}' pending."); messagebox.showinfo("Breakpoint Pending", f"BP '{bp_location}' is pending.", parent=self)
|
|
else:
|
|
self._update_status_bar(f"Issue setting BP '{bp_location}'. Check GDB output.", is_error=True)
|
|
except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error(f"set breakpoint '{bp_location}'", e)
|
|
except Exception as e: self._handle_gdb_operation_error(f"set breakpoint '{bp_location}' (unexpected)", e)
|
|
|
|
|
|
def _run_or_continue_gdb_action(self):
|
|
if not self.gdb_session or not self.gdb_session.is_alive():
|
|
messagebox.showerror("Error", "GDB session is not active.", parent=self); return
|
|
self._update_parsed_json_output(None); self._disable_save_buttons()
|
|
try:
|
|
output = ""; run_timeout = self.app_settings.get_setting("timeouts", "program_run_continue")
|
|
dumper_is_valid_and_loaded = self.gdb_session.gdb_script_sourced_successfully
|
|
|
|
if not self.program_started_once:
|
|
params_str = self.params_var.get(); self._update_status_bar(f"Running program with params: '{params_str}'...")
|
|
self._update_gdb_raw_output(f"Executing: run {params_str}\n", append=True)
|
|
output = self.gdb_session.run_program(params_str, timeout=run_timeout)
|
|
else:
|
|
self._update_status_bar("Continuing program execution..."); self._update_gdb_raw_output("Executing: continue\n", append=True)
|
|
output = self.gdb_session.continue_execution(timeout=run_timeout)
|
|
|
|
self._update_gdb_raw_output(output, append=True)
|
|
dump_button_state = tk.NORMAL if dumper_is_valid_and_loaded else tk.DISABLED
|
|
|
|
if "Breakpoint" in output or re.search(r"Hit Breakpoint \d+", output, re.IGNORECASE):
|
|
self._update_status_bar("Breakpoint hit. Ready to dump variables."); self.dump_var_button.config(state=dump_button_state)
|
|
self.program_started_once = True; self.run_button.config(text="3. Continue")
|
|
elif "Program exited normally" in output or "exited with code" in output:
|
|
self._update_status_bar("Program exited."); self.dump_var_button.config(state=tk.DISABLED)
|
|
self.run_button.config(text="3. Run Program (Restart)"); self.program_started_once = False
|
|
elif "received signal" in output.lower() or "segmentation fault" in output.lower():
|
|
self._update_status_bar("Program signal/crash. Check GDB output.", is_error=True); self.dump_var_button.config(state=dump_button_state)
|
|
self.program_started_once = True; self.run_button.config(text="3. Continue (Risky)")
|
|
else:
|
|
self._update_status_bar("Program running/unknown state."); self.dump_var_button.config(state=dump_button_state)
|
|
self.program_started_once = True; self.run_button.config(text="3. Continue")
|
|
except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error("run/continue", e)
|
|
except Exception as e: self._handle_gdb_operation_error("run/continue (unexpected)", e)
|
|
|
|
|
|
def _dump_gdb_variable_action(self):
|
|
if not self.gdb_session or not self.gdb_session.is_alive():
|
|
messagebox.showerror("Error", "GDB session is not active.", parent=self); return
|
|
if not self.gdb_session.gdb_script_sourced_successfully:
|
|
messagebox.showwarning("Dumper Script Error", "GDB dumper script not loaded. JSON dump unavailable.", parent=self)
|
|
self._check_critical_configs_and_update_gui(); return
|
|
|
|
var_expr = self.variable_var.get()
|
|
if not var_expr: messagebox.showerror("Input Error", "Variable/Expression to dump cannot be empty.", parent=self); return
|
|
|
|
self._update_status_bar(f"Dumping '{var_expr}' to JSON..."); self._update_gdb_raw_output(f"Attempting JSON dump of: {var_expr}\n", append=True)
|
|
try:
|
|
dump_timeout = self.app_settings.get_setting("timeouts", "dump_variable")
|
|
# For manual mode, target_output_filepath is None, so full JSON comes to console
|
|
dumped_data = self.gdb_session.dump_variable_to_json(var_expr, timeout=dump_timeout, target_output_filepath=None)
|
|
|
|
self.last_dumped_data = dumped_data # Store for saving
|
|
self._update_parsed_json_output(dumped_data) # This will show the full JSON
|
|
|
|
if isinstance(dumped_data, dict) and ("_gdb_tool_error" in dumped_data or dumped_data.get("status") == "error"):
|
|
error_msg = dumped_data.get("details", dumped_data.get("message", dumped_data.get("_gdb_tool_error", "Unknown dumper error")))
|
|
self._update_status_bar(f"Error dumping '{var_expr}': {error_msg}", is_error=True); self._disable_save_buttons()
|
|
if "raw_gdb_output" in dumped_data : self._update_gdb_raw_output(f"-- Raw GDB for failed dump '{var_expr}' --\n{dumped_data['raw_gdb_output']}\n-- End --\n", append=True)
|
|
elif dumped_data is not None:
|
|
self._update_status_bar(f"Successfully dumped '{var_expr}'."); self._enable_save_buttons_if_data()
|
|
else:
|
|
self._update_status_bar(f"Dump of '{var_expr}' returned no data.", is_error=True); self._disable_save_buttons()
|
|
except (ConnectionError, TimeoutError) as e:
|
|
self._handle_gdb_operation_error(f"dump variable '{var_expr}'", e); self.last_dumped_data = None; self._disable_save_buttons(); self._update_parsed_json_output({"error": str(e)})
|
|
except Exception as e:
|
|
self._handle_gdb_operation_error(f"dump variable '{var_expr}' (unexpected)", e); self.last_dumped_data = None; self._disable_save_buttons(); self._update_parsed_json_output({"error": str(e)})
|
|
|
|
|
|
def _reset_gui_to_stopped_state(self):
|
|
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
|
|
gdb_is_ok = gdb_exe_path and os.path.isfile(gdb_exe_path)
|
|
is_prof_running = self.profile_executor_instance and self.profile_executor_instance.is_running
|
|
|
|
if hasattr(self, "start_gdb_button"): self.start_gdb_button.config(state=tk.NORMAL if gdb_is_ok and not is_prof_running else tk.DISABLED)
|
|
if hasattr(self, "set_bp_button"): self.set_bp_button.config(state=tk.DISABLED, text="2. Set Breakpoint")
|
|
if hasattr(self, "run_button"): self.run_button.config(state=tk.DISABLED, text="3. Run Program")
|
|
if hasattr(self, "dump_var_button"): self.dump_var_button.config(state=tk.DISABLED)
|
|
if hasattr(self, "stop_gdb_button"): self.stop_gdb_button.config(state=tk.DISABLED)
|
|
|
|
if hasattr(self, "run_profile_button") and hasattr(self, "profile_selection_combo"):
|
|
can_run_profile = gdb_is_ok and not is_prof_running and self.profile_selection_combo.get()
|
|
self.run_profile_button.config(state=tk.NORMAL if can_run_profile else tk.DISABLED)
|
|
self.profile_selection_combo.config(state="readonly" if not is_prof_running else tk.DISABLED)
|
|
|
|
if hasattr(self, "save_json_button"): self._disable_save_buttons()
|
|
self.program_started_once = False; self.last_dumped_data = None
|
|
if not is_prof_running and hasattr(self, "status_var") and self.status_var is not None:
|
|
self._update_status_bar("GDB session stopped or not active.")
|
|
|
|
|
|
def _stop_gdb_session_action(self):
|
|
if self.gdb_session and self.gdb_session.is_alive():
|
|
self._update_status_bar("Stopping GDB session...")
|
|
try:
|
|
kill_timeout = self.app_settings.get_setting("timeouts", "kill_program")
|
|
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit")
|
|
if self.program_started_once: # Try to kill inferior only if it was run
|
|
kill_output = self.gdb_session.kill_program(timeout=kill_timeout)
|
|
self._update_gdb_raw_output(f"Kill command output:\n{kill_output}\n", append=True)
|
|
self.gdb_session.quit(timeout=quit_timeout)
|
|
self._update_gdb_raw_output("GDB session quit command sent.\n", append=True)
|
|
except Exception as e: self._handle_gdb_operation_error("stop session", e)
|
|
finally:
|
|
self.gdb_session = None; self._reset_gui_to_stopped_state()
|
|
self._load_and_populate_profiles_for_automation_tab() # Re-enable profile controls
|
|
else: # If no session, just ensure GUI is reset
|
|
self._reset_gui_to_stopped_state()
|
|
self._load_and_populate_profiles_for_automation_tab()
|
|
|
|
|
|
def _enable_save_buttons_if_data(self):
|
|
if self.last_dumped_data and not (isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data):
|
|
if hasattr(self, "save_json_button"): self.save_json_button.config(state=tk.NORMAL)
|
|
if hasattr(self, "save_csv_button"): self.save_csv_button.config(state=tk.NORMAL)
|
|
else:
|
|
self._disable_save_buttons()
|
|
|
|
def _disable_save_buttons(self):
|
|
if hasattr(self, "save_json_button"): self.save_json_button.config(state=tk.DISABLED)
|
|
if hasattr(self, "save_csv_button"): self.save_csv_button.config(state=tk.DISABLED)
|
|
|
|
def _save_dumped_data(self, format_type: str):
|
|
if self.last_dumped_data is None or (isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data):
|
|
messagebox.showwarning("No Data", "No valid data to save.", parent=self); return
|
|
|
|
file_ext = f".{format_type.lower()}"; file_types = [(f"{format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")]
|
|
var_sugg = self.variable_var.get().replace(" ", "_").replace("*","ptr").replace("->","_").replace(":","_")
|
|
default_fname = f"{var_sugg}_dump{file_ext}" if var_sugg else f"gdb_dump{file_ext}"
|
|
|
|
filepath = filedialog.asksaveasfilename(defaultextension=file_ext, filetypes=file_types, title=f"Save Dumped Data as {format_type.upper()}", initialfile=default_fname, parent=self)
|
|
if not filepath: return
|
|
|
|
self._update_status_bar(f"Saving data as {format_type.upper()} to {os.path.basename(filepath)}...")
|
|
try:
|
|
if format_type == "json": save_to_json(self.last_dumped_data, filepath)
|
|
elif format_type == "csv":
|
|
data_for_csv = self.last_dumped_data
|
|
if isinstance(data_for_csv, dict) and not isinstance(data_for_csv, list): data_for_csv = [data_for_csv]
|
|
elif not isinstance(data_for_csv, list): data_for_csv = [{"value": data_for_csv}]
|
|
elif isinstance(data_for_csv, list) and data_for_csv and not all(isinstance(item, dict) for item in data_for_csv):
|
|
data_for_csv = [{"value": item} for item in data_for_csv]
|
|
save_to_csv(data_for_csv, filepath)
|
|
messagebox.showinfo("Save Successful", f"Data saved to:\n{filepath}", parent=self)
|
|
self._update_status_bar(f"Data saved to {os.path.basename(filepath)}.")
|
|
except Exception as e:
|
|
logger.error(f"Error saving data as {format_type} to {filepath}: {e}", exc_info=True)
|
|
messagebox.showerror("Save Error", f"Failed to save data to {filepath}:\n{e}", parent=self)
|
|
self._update_status_bar(f"Error saving data as {format_type}.", is_error=True)
|
|
|
|
def _gui_status_update(self, message: str) -> None: # For ProfileExecutor
|
|
if hasattr(self, "profile_exec_status_var") and self.profile_exec_status_var is not None:
|
|
self.profile_exec_status_var.set(message)
|
|
logger.info(f"ProfileExec Status: {message}")
|
|
|
|
def _gui_gdb_output_update(self, message: str) -> None: # For ProfileExecutor
|
|
self._update_gdb_raw_output(message, append=True)
|
|
|
|
def _gui_json_data_update(self, data: Any) -> None: # For ProfileExecutor (receives status JSON)
|
|
self._update_parsed_json_output(data) # This will now call the modified version
|
|
|
|
def _gui_add_execution_log_entry(self, entry: ExecutionLogEntry) -> None: # For ProfileExecutor
|
|
if self.produced_files_tree and self.winfo_exists():
|
|
try:
|
|
values = (entry.get("timestamp", ""), entry.get("breakpoint_spec", "N/A"),
|
|
entry.get("variable", "N/A"), entry.get("file_produced", "N/A"),
|
|
entry.get("status", "N/A"), entry.get("details", ""))
|
|
item_id = self.produced_files_tree.insert("", tk.END, values=values)
|
|
self.produced_files_tree.see(item_id)
|
|
except Exception as e: logger.error(f"Failed to add to produced_files_tree: {e}. Entry: {entry}")
|
|
|
|
def _clear_produced_files_tree(self) -> None:
|
|
if self.produced_files_tree:
|
|
for item in self.produced_files_tree.get_children(): self.produced_files_tree.delete(item)
|
|
|
|
def _run_selected_profile_action(self) -> None:
|
|
selected_profile_name = self.profile_selection_combo.get()
|
|
if not selected_profile_name: messagebox.showwarning("No Profile", "Please select a profile.", parent=self); return
|
|
if self.profile_executor_instance and self.profile_executor_instance.is_running:
|
|
messagebox.showwarning("Profile Running", "A profile is already running.", parent=self); return
|
|
if self.gdb_session and self.gdb_session.is_alive():
|
|
messagebox.showerror("GDB Active", "Manual GDB session active. Stop it first.", parent=self); return
|
|
|
|
profile_data = self.available_profiles_map.get(selected_profile_name)
|
|
if not profile_data: messagebox.showerror("Error", f"Cannot find data for profile '{selected_profile_name}'.", parent=self); return
|
|
|
|
self.profile_exec_status_var.set(f"STARTING PROFILE '{selected_profile_name}'...")
|
|
if self.profile_progressbar and hasattr(self.profile_exec_status_label_big, 'master'):
|
|
parent_frame = self.profile_exec_status_label_big.master # type: ignore
|
|
self.profile_progressbar.grid(row=1, column=0, columnspan=1, sticky="ew", padx=5, pady=(2,5), in_=parent_frame)
|
|
self.profile_progressbar.start(15)
|
|
|
|
if hasattr(self, 'run_profile_button'): self.run_profile_button.config(state=tk.DISABLED)
|
|
if hasattr(self, 'stop_profile_button'): self.stop_profile_button.config(state=tk.NORMAL)
|
|
if hasattr(self, 'profile_selection_combo'): self.profile_selection_combo.config(state=tk.DISABLED)
|
|
try:
|
|
if self.menubar.winfo_exists(): self.menubar.entryconfig("Profiles", state=tk.DISABLED); self.menubar.entryconfig("Options", state=tk.DISABLED)
|
|
except tk.TclError: logger.warning("TclError disabling menubar.")
|
|
if hasattr(self, 'start_gdb_button'): self.start_gdb_button.config(state=tk.DISABLED)
|
|
if hasattr(self, 'set_bp_button'): self.set_bp_button.config(state=tk.DISABLED)
|
|
if hasattr(self, 'run_button'): self.run_button.config(state=tk.DISABLED)
|
|
if hasattr(self, 'dump_var_button'): self.dump_var_button.config(state=tk.DISABLED)
|
|
if hasattr(self, 'stop_gdb_button'): self.stop_gdb_button.config(state=tk.DISABLED)
|
|
|
|
self.last_run_output_path = None;
|
|
if hasattr(self, 'open_output_folder_button'): self.open_output_folder_button.config(state=tk.DISABLED)
|
|
|
|
self.profile_executor_instance = ProfileExecutor(profile_data, self.app_settings,
|
|
status_update_callback=self._gui_status_update, gdb_output_callback=self._gui_gdb_output_update,
|
|
json_output_callback=self._gui_json_data_update, execution_log_callback=self._gui_add_execution_log_entry)
|
|
|
|
self._clear_produced_files_tree(); self._update_gdb_raw_output("", append=False); self._update_parsed_json_output(None)
|
|
|
|
executor_thread = threading.Thread(target=self._profile_executor_thread_target, daemon=True)
|
|
executor_thread.start()
|
|
|
|
def _profile_executor_thread_target(self):
|
|
if self.profile_executor_instance:
|
|
try: self.profile_executor_instance.run()
|
|
finally:
|
|
if self.winfo_exists(): self.after(0, self._on_profile_execution_finished)
|
|
|
|
def _on_profile_execution_finished(self):
|
|
if not self.winfo_exists(): logger.warning("Profile finish callback but window gone."); return
|
|
|
|
if self.profile_progressbar: self.profile_progressbar.stop(); self.profile_progressbar.grid_remove()
|
|
|
|
final_status_message = "Profile execution finished."
|
|
if self.profile_executor_instance:
|
|
if hasattr(self.profile_executor_instance, 'current_run_output_path'):
|
|
self.last_run_output_path = self.profile_executor_instance.current_run_output_path
|
|
if self.last_run_output_path and os.path.isdir(self.last_run_output_path) and hasattr(self, 'open_output_folder_button'):
|
|
self.open_output_folder_button.config(state=tk.NORMAL) # type: ignore
|
|
else:
|
|
if hasattr(self, 'open_output_folder_button'): self.open_output_folder_button.config(state=tk.DISABLED) # type: ignore
|
|
logger.warning(f"Output folder invalid: {self.last_run_output_path}")
|
|
else:
|
|
if hasattr(self, 'open_output_folder_button'): self.open_output_folder_button.config(state=tk.DISABLED) # type: ignore
|
|
|
|
# Determine a more precise final status message
|
|
current_gui_status = self.profile_exec_status_var.get()
|
|
if "STARTING PROFILE" in current_gui_status or "Requesting profile stop" in current_gui_status:
|
|
if hasattr(self.profile_executor_instance, 'profile_execution_summary'):
|
|
exec_final_status = self.profile_executor_instance.profile_execution_summary.get("status", "Unknown")
|
|
if "Error" in exec_final_status or "failed" in exec_final_status.lower() or "issues" in exec_final_status.lower():
|
|
final_status_message = f"Profile finished with issues: {exec_final_status}"
|
|
elif exec_final_status not in ["Initialized", "Pending", "Processing Dumps"]:
|
|
final_status_message = f"Profile run completed. Final state: {exec_final_status}"
|
|
# else keep default "Profile execution completed." if summary not available
|
|
elif "Error:" in current_gui_status or "failed" in current_gui_status.lower() or "issues" in current_gui_status.lower():
|
|
final_status_message = f"Profile finished with issues: {current_gui_status}" # Keep existing error
|
|
else: # If no explicit error and not starting phase, use current status
|
|
final_status_message = f"Profile run completed. Last status: {current_gui_status}"
|
|
|
|
self.profile_exec_status_var.set(final_status_message)
|
|
|
|
if hasattr(self, 'profile_selection_combo') and self.profile_selection_combo.get(): # type: ignore
|
|
if hasattr(self, 'run_profile_button'): self.run_profile_button.config(state=tk.NORMAL) # type: ignore
|
|
else:
|
|
if hasattr(self, 'run_profile_button'): self.run_profile_button.config(state=tk.DISABLED) # type: ignore
|
|
|
|
if hasattr(self, 'stop_profile_button'): self.stop_profile_button.config(state=tk.DISABLED) # type: ignore
|
|
if hasattr(self, 'profile_selection_combo'): self.profile_selection_combo.config(state="readonly") # type: ignore
|
|
|
|
try:
|
|
if self.menubar.winfo_exists(): self.menubar.entryconfig("Profiles", state=tk.NORMAL); self.menubar.entryconfig("Options", state=tk.NORMAL)
|
|
except tk.TclError as e: logger.warning(f"TclError re-enabling menubar: {e}")
|
|
|
|
self._check_critical_configs_and_update_gui() # Re-enable manual GDB start button if GDB config is OK
|
|
self.profile_executor_instance = None
|
|
logger.info("Profile execution GUI updates completed.")
|
|
|
|
|
|
def _stop_current_profile_action(self) -> None:
|
|
if self.profile_executor_instance and self.profile_executor_instance.is_running:
|
|
self.profile_exec_status_var.set("Requesting profile stop...")
|
|
self.profile_executor_instance.request_stop()
|
|
if hasattr(self, 'stop_profile_button'): self.stop_profile_button.config(state=tk.DISABLED)
|
|
else:
|
|
self.profile_exec_status_var.set("No profile currently running to stop.")
|
|
|
|
|
|
def _open_last_run_output_folder(self) -> None:
|
|
logger.info(f"Attempting to open output folder: '{self.last_run_output_path}'")
|
|
if not self.last_run_output_path or not os.path.isdir(self.last_run_output_path):
|
|
messagebox.showwarning("No Output Folder", "Output folder for the last run is not available or does not exist.", parent=self)
|
|
if hasattr(self, 'open_output_folder_button'): self.open_output_folder_button.config(state=tk.DISABLED)
|
|
return
|
|
try:
|
|
if sys.platform == "win32": os.startfile(self.last_run_output_path)
|
|
elif sys.platform == "darwin": subprocess.run(["open", self.last_run_output_path], check=True)
|
|
else: subprocess.run(["xdg-open", self.last_run_output_path], check=True)
|
|
except FileNotFoundError:
|
|
logger.error("File manager command not found.", exc_info=True)
|
|
messagebox.showerror("Error", f"Could not find file manager. Path: {self.last_run_output_path}", parent=self)
|
|
except subprocess.CalledProcessError as cpe:
|
|
logger.error(f"Command to open folder failed: {cpe}", exc_info=True)
|
|
messagebox.showerror("Error", f"Failed to open folder (code {cpe.returncode}): {self.last_run_output_path}\nError: {cpe.stderr.decode(errors='replace') if cpe.stderr else 'Unknown'}", parent=self)
|
|
except Exception as e:
|
|
logger.error(f"Failed to open output folder: {e}", exc_info=True)
|
|
messagebox.showerror("Error", f"Could not open folder: {self.last_run_output_path}\nError: {e}", parent=self)
|
|
|
|
|
|
def _on_closing_window(self):
|
|
logger.info("Window closing sequence initiated.")
|
|
active_profile_stop_requested = False
|
|
if self.profile_executor_instance and self.profile_executor_instance.is_running:
|
|
response = messagebox.askyesnocancel("Profile Running", "Profile running. Stop it and exit?", default=messagebox.CANCEL, parent=self)
|
|
if response is True: self._stop_current_profile_action(); active_profile_stop_requested = True
|
|
elif response is None: logger.info("User cancelled exit."); return
|
|
|
|
self.app_settings.set_setting("gui", "main_window_geometry", self.geometry())
|
|
self.app_settings.set_setting("general", "last_target_executable_path", self.exe_path_var.get())
|
|
self.app_settings.set_setting("general", "default_breakpoint", self.breakpoint_var.get())
|
|
self.app_settings.set_setting("general", "default_variable_to_dump", self.variable_var.get())
|
|
self.app_settings.set_setting("general", "default_program_parameters", self.params_var.get())
|
|
|
|
if not self.app_settings.save_settings() and self.winfo_exists():
|
|
messagebox.showwarning("Settings Error", "Could not save settings. Check logs.", parent=self)
|
|
|
|
should_destroy = True
|
|
if self.gdb_session and self.gdb_session.is_alive():
|
|
if self.winfo_exists() and messagebox.askokcancel("Quit GDB Session", "Manual GDB session active. Stop it and exit?", parent=self):
|
|
self._stop_gdb_session_action()
|
|
elif self.winfo_exists(): should_destroy = False; logger.info("User cancelled exit (manual GDB active).")
|
|
elif not self.winfo_exists(): self._stop_gdb_session_action() # If window is already gone, try to stop
|
|
|
|
if should_destroy:
|
|
logger.info("Proceeding with window destruction.")
|
|
if self.gui_log_handler: logging.getLogger().removeHandler(self.gui_log_handler); self.gui_log_handler.close(); self.gui_log_handler = None
|
|
if active_profile_stop_requested: logger.debug("Assuming profile executor thread will terminate.")
|
|
self.destroy(); logger.info("Tkinter window destroyed.")
|
|
else:
|
|
logger.info("Window destruction aborted.")
|
|
|
|
|
|
class ScrolledTextLogHandler(logging.Handler):
|
|
def __init__(self, text_widget: scrolledtext.ScrolledText):
|
|
super().__init__()
|
|
self.text_widget = text_widget
|
|
self._active = True
|
|
|
|
def emit(self, record):
|
|
if not self._active or not hasattr(self.text_widget, 'winfo_exists') or not self.text_widget.winfo_exists():
|
|
return
|
|
try:
|
|
log_entry = self.format(record)
|
|
self.text_widget.config(state=tk.NORMAL)
|
|
self.text_widget.insert(tk.END, log_entry + "\n")
|
|
self.text_widget.see(tk.END)
|
|
self.text_widget.config(state=tk.DISABLED)
|
|
except tk.TclError: self._active = False # Widget likely destroyed
|
|
except Exception: self._active = False |