# File: cpp_python_debug/gui/main_window.py # Provides the Tkinter GUI for interacting with the GDB session. import tkinter as tk from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu import logging import os import json # For pretty-printing JSON in the GUI import re # Relative imports for modules within the same package from ..core.gdb_controller import GDBSession from ..core.output_formatter import save_to_json, save_to_csv from ..core.config_manager import AppSettings from .config_window import ConfigWindow # MODIFIED: Import ConfigWindow logger = logging.getLogger(__name__) class GDBGui(tk.Tk): def __init__(self): super().__init__() self.app_settings = AppSettings() self.gui_log_handler = None self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}") self.geometry(self.app_settings.get_setting("gui", "main_window_geometry", "850x650")) # Adjusted height slightly self.gdb_session = None self.last_dumped_data = None self.program_started_once = False # MODIFIED: StringVars for status display of critical paths self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...") self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...") # Tkinter StringVars for other input fields remain 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._create_menus() self._create_widgets() # Widgets creation first self._setup_logging_redirect_to_gui() # MODIFIED: Initial check of critical configurations self._check_critical_configs_and_update_gui() self.protocol("WM_DELETE_WINDOW", self._on_closing_window) # REMOVED: _initialize_gdb_dumper_script_path (handled by AppSettings and ConfigWindow) 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) def _open_config_window(self): logger.debug("Opening configuration window.") config_win = ConfigWindow(self, self.app_settings) self.wait_window(config_win) # Makes the config window modal logger.debug("Configuration window closed.") self._check_critical_configs_and_update_gui() # Refresh status and button states def _check_critical_configs_and_update_gui(self): """ Checks critical GDB and Dumper script configurations and updates GUI elements accordingly. Enables/disables session control buttons based on configuration validity. """ 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 dumper_ok = False # Dumper is optional, but its status is still relevant # Check GDB executable if gdb_exe_path and os.path.isfile(gdb_exe_path): # Check if it's a file, more robust than just exists self.gdb_exe_status_var.set(f"GDB: {os.path.basename(gdb_exe_path)} (OK)") gdb_ok = True elif gdb_exe_path: # Path is set but not a valid file self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)") else: # Path not set self.gdb_exe_status_var.set("GDB: Not Configured! Please set in Options > Configure.") # Check GDB Dumper script (optional) 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)") dumper_ok = True # Not strictly necessary for dumper_ok to be true for session start elif dumper_script_path: # Path is set but not a valid file self.gdb_dumper_status_var.set(f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)") else: # Path not set self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).") dumper_ok = True # Considered "OK" in the sense that it's not misconfigured if empty # Update button states if gdb_ok: self.start_gdb_button.config(state=tk.NORMAL) # Other buttons remain disabled until GDB session starts if self.gdb_session and self.gdb_session.is_alive(): # This case should ideally not happen here as _reset_gui_to_stopped_state # would have been called if GDB session was active and then config changed. # But as a safeguard: self.set_bp_button.config(state=tk.NORMAL) # self.run_button state depends on breakpoint # self.dump_var_button state depends on hitting breakpoint & dumper script self.stop_gdb_button.config(state=tk.NORMAL) else: # No active session, reset dependent buttons self.set_bp_button.config(state=tk.DISABLED) 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.DISABLED) else: # GDB not configured or invalid self._reset_gui_to_stopped_state() # This will disable all session buttons self.start_gdb_button.config(state=tk.DISABLED) # Explicitly ensure start is disabled # The dump_var_button's state also depends on whether the dumper script is OK # and if a breakpoint is hit. This is handled in _run_or_continue_gdb_action. # Here, we primarily care about enabling the GDB start. # Update main window title to reflect config file self.title(f"GDB Debug GUI - 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) # --- Configuration Status Frame --- # MODIFIED: Replaced GDB/Dumper path entries with status labels and a configure button config_status_frame = ttk.LabelFrame(main_frame, text="Critical Configuration Status", padding="10") config_status_frame.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5) config_status_frame.columnconfigure(1, weight=1) # Allow status label to expand row_idx = 0 ttk.Label(config_status_frame, text="GDB Executable:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Label(config_status_frame, textvariable=self.gdb_exe_status_var, relief="sunken", padding=(5,2), width=60).grid(row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=2) row_idx += 1 ttk.Label(config_status_frame, text="GDB Dumper Script:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Label(config_status_frame, textvariable=self.gdb_dumper_status_var, relief="sunken", padding=(5,2), width=60).grid(row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=2) row_idx += 1 ttk.Button(config_status_frame, text="Open Configuration...", command=self._open_config_window).grid( row=0, column=2, rowspan=row_idx, padx=10, pady=5, sticky=(tk.N, tk.S, tk.E, tk.W) # Span rows and fill vertically ) # --- Target and Runtime Configuration Frame --- # This frame now contains the rest of the previous "GDB Configuration" runtime_config_frame = ttk.LabelFrame(main_frame, text="Target & Debug Session Settings", padding="10") runtime_config_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5) runtime_config_frame.columnconfigure(1, weight=1) # Allow entry fields to expand row_idx = 0 # Reset row index for the new frame # Target Executable Path ttk.Label(runtime_config_frame, text="Target Executable:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(runtime_config_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(runtime_config_frame, text="Browse...", command=self._browse_target_exe).grid(row=row_idx, column=2, padx=5, pady=2) row_idx += 1 # Program Parameters ttk.Label(runtime_config_frame, text="Program Parameters:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(runtime_config_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 # Breakpoint ttk.Label(runtime_config_frame, text="Breakpoint Location:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(runtime_config_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" # Shortened ttk.Label(runtime_config_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 # Variable to Dump ttk.Label(runtime_config_frame, text="Variable/Expression:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(runtime_config_frame, textvariable=self.variable_var).grid(row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2) # --- Control Frame (Starts at row 2 now) --- control_frame = ttk.LabelFrame(main_frame, text="Session Control", padding="10") control_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) button_flow_frame = ttk.Frame(control_frame) button_flow_frame.pack(fill=tk.X, expand=True) self.start_gdb_button = ttk.Button(button_flow_frame, text="1. Start GDB Session", command=self._start_gdb_session_action, state=tk.DISABLED) # Start disabled self.start_gdb_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True) self.set_bp_button = ttk.Button(button_flow_frame, text="2. Set Breakpoint", command=self._set_gdb_breakpoint_action, state=tk.DISABLED) self.set_bp_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True) self.run_button = ttk.Button(button_flow_frame, text="3. Run Program", command=self._run_or_continue_gdb_action, state=tk.DISABLED) self.run_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True) self.dump_var_button = ttk.Button(button_flow_frame, text="4. Dump Variable", command=self._dump_gdb_variable_action, state=tk.DISABLED) self.dump_var_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True) self.stop_gdb_button = ttk.Button(button_flow_frame, text="Stop GDB Session", command=self._stop_gdb_session_action, state=tk.DISABLED) self.stop_gdb_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True) # --- Output and Log Tabs (Starts at row 3 now) --- output_notebook = ttk.Notebook(main_frame) output_notebook.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10) main_frame.rowconfigure(3, weight=1) # Allow notebook to expand vertically self.gdb_raw_output_text = scrolledtext.ScrolledText(output_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9)) output_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output") self.parsed_json_output_text = scrolledtext.ScrolledText(output_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9)) output_notebook.add(self.parsed_json_output_text, text="Parsed JSON Output") self.app_log_text = scrolledtext.ScrolledText(output_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9)) output_notebook.add(self.app_log_text, text="Application Log") # --- Save Frame (Starts at row 4 now) --- save_frame = ttk.LabelFrame(main_frame, text="Save Dumped Data", padding="10") save_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5) self.save_json_button = ttk.Button(save_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(save_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) # Status Bar (Starts at row 5 now) self.status_var = tk.StringVar(value="Ready. Please configure GDB path if needed via Options > Configure.") status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5,0), ipady=2) 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=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 path: target_var.set(path) # REMOVED: _browse_gdb_exe and _browse_gdb_script (handled in ConfigWindow) def _browse_target_exe(self): # This one remains as target exe is configured in main GUI 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): # No changes 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): # No changes 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", "") elif isinstance(data_to_display, (dict, 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 JSON for GUI: {e}") self.parsed_json_output_text.insert("1.0", f"Error displaying JSON: {e}\nRaw data: {str(data_to_display)}") else: 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): # No changes self.status_var.set(message) def _handle_gdb_operation_error(self, operation_name: str, error_details: any): # No changes 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) messagebox.showerror("GDB Operation Error", error_message) def _start_gdb_session_action(self): 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 is not configured correctly. Please check Options > Configure.", parent=self) self._check_critical_configs_and_update_gui() return if not target_exe: messagebox.showerror("Input Error", "Target executable path is required.", parent=self) return if not os.path.exists(target_exe): messagebox.showerror("File Not Found", f"Target executable not found: {target_exe}", parent=self) return if gdb_script and not os.path.isfile(gdb_script): messagebox.showwarning("Configuration Warning", f"GDB dumper script path is set to:\n'{gdb_script}'\nbut the file was not found or is invalid.\n\n" "JSON dumping via script will be unavailable. You can correct this in Options > Configure.", parent=self) gdb_script = None self.gdb_dumper_status_var.set(f"Dumper: '{self.app_settings.get_setting('general', 'gdb_dumper_script_path')}' (Not Found!)") if self.gdb_session and self.gdb_session.is_alive(): messagebox.showwarning("Session Active", "A GDB session is already active. Please 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) # Added default for safety # MODIFIED: Correctly get the dumper_options dictionary current_dumper_options = self.app_settings.get_category_settings("dumper_options", {}) logger.debug(f"Attempting to create GDBSession with dumper_options: type={type(current_dumper_options)}, value='{str(current_dumper_options)[:200]}...'") self.gdb_session = GDBSession( gdb_path=gdb_exe, executable_path=target_exe, gdb_script_full_path=gdb_script, dumper_options=current_dumper_options ) logger.debug("GDBSession instance created. Calling start()...") self.gdb_session.start(timeout=startup_timeout) logger.debug("GDBSession start() returned.") self._update_gdb_raw_output(f"GDB session started for '{os.path.basename(target_exe)}'.\n") if gdb_script and self.gdb_session.gdb_script_sourced_successfully: self._update_gdb_raw_output(f"GDB 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)") elif gdb_script: self._update_gdb_raw_output(f"Warning: GDB dumper script '{os.path.basename(gdb_script)}' specified but failed to load.\n", append=True) self._update_status_bar(f"GDB active. Dumper script issues (check logs).", is_error=True) self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)") messagebox.showwarning("Dumper Script Issue", f"The GDB dumper script '{os.path.basename(gdb_script)}' may have failed to load.\n" "JSON dumping might be affected. Check logs.", parent=self) else: self._update_gdb_raw_output("No GDB dumper script. JSON dump via script unavailable.\n", append=True) self._update_status_bar("GDB session active. No dumper script.") if not self.app_settings.get_setting("general", "gdb_dumper_script_path"): # Check specific setting for this message 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) self.program_started_once = False self.last_dumped_data = None self._disable_save_buttons() except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific: logger.error(f"Specific error during GDB start: {type(e_specific).__name__}: {e_specific}", exc_info=False) self._handle_gdb_operation_error("start session", e_specific) self.gdb_session = None self._reset_gui_to_stopped_state() except Exception as e: logger.critical(f"!!! MAIN_WINDOW CATCH-ALL: Unhandled exception type: {type(e).__name__}, message: '{e}'", exc_info=True) self._handle_gdb_operation_error("start session (unexpected from main_window catch-all)", e) self.gdb_session = None self._reset_gui_to_stopped_state() # _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"Breakpoint at '{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_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path") dumper_is_valid = dumper_script_path and os.path.isfile(dumper_script_path) and 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.DISABLED if dumper_is_valid: # Only enable dump if dumper is OK dump_button_state = tk.NORMAL 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) # Still might want to dump 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 dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path") if not dumper_script_path or not os.path.isfile(dumper_script_path) or not self.gdb_session.gdb_script_sourced_successfully: messagebox.showwarning("Dumper Script Error", "GDB dumper script is not available, not found, or failed to load.\n" "JSON dump cannot proceed. Check configuration and logs.", parent=self) self._check_critical_configs_and_update_gui() # Re-check to update dumper status display 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") dumped_data = self.gdb_session.dump_variable_to_json(var_expr, timeout=dump_timeout) self.last_dumped_data = dumped_data self._update_parsed_json_output(dumped_data) if isinstance(dumped_data, dict) and "_gdb_tool_error" in dumped_data: error_msg = dumped_data.get("details", dumped_data["_gdb_tool_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 output for failed dump of '{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): # Determine if GDB path is currently valid to correctly set start_gdb_button state gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") gdb_is_configured_correctly = gdb_exe_path and os.path.isfile(gdb_exe_path) if gdb_is_configured_correctly: self.start_gdb_button.config(state=tk.NORMAL) else: self.start_gdb_button.config(state=tk.DISABLED) self.set_bp_button.config(state=tk.DISABLED, text="2. Set Breakpoint") 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.DISABLED) self._disable_save_buttons() self.program_started_once = False self.last_dumped_data = None self._update_status_bar("GDB session stopped or not active.") # REMOVED: self._check_critical_configs_and_update_gui() # This was the source of recursion 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: 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() # This will also call _check_critical_configs else: messagebox.showinfo("Info", "GDB session is not active or already stopped.", parent=self) self._reset_gui_to_stopped_state() # Ensure GUI is reset and buttons updated def _enable_save_buttons_if_data(self): # No changes if self.last_dumped_data and not (isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data): self.save_json_button.config(state=tk.NORMAL) self.save_csv_button.config(state=tk.NORMAL) else: self._disable_save_buttons() def _disable_save_buttons(self): # No changes self.save_json_button.config(state=tk.DISABLED) self.save_csv_button.config(state=tk.DISABLED) def _save_dumped_data(self, format_type: str): # No changes beyond parent=self for messageboxes 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 has been dumped to save.", parent=self) return file_ext = f".{format_type.lower()}"; file_types = [(f"{format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")] var_name_suggestion = self.variable_var.get().replace(" ", "_").replace("*", "ptr").replace("->", "_") default_filename = f"{var_name_suggestion}_dump{file_ext}" if var_name_suggestion 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_filename) 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 _on_closing_window(self): logger.info("Window closing sequence initiated.") # MODIFIED: Only save settings that are still managed by main_window's UI self.app_settings.set_setting("gui", "main_window_geometry", self.geometry()) # GDB exe and dumper script paths are no longer in main_window StringVars directly tied to AppSettings here. # They are managed via ConfigWindow. 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()) save_success = self.app_settings.save_settings() if not save_success: messagebox.showwarning("Settings Error", "Could not save application settings. Check logs.", parent=self) should_destroy = True if self.gdb_session and self.gdb_session.is_alive(): if messagebox.askokcancel("Quit GDB Session", "A GDB session is active. Stop it and exit?", parent=self): logger.info("User chose to stop active GDB session and exit.") self._stop_gdb_session_action() else: logger.info("User cancelled exit while GDB session is active.") should_destroy = False if should_destroy: logger.info("Proceeding with window destruction.") if self.gui_log_handler: logger.debug("Removing GUI log handler.") logging.getLogger().removeHandler(self.gui_log_handler) self.gui_log_handler.close() self.gui_log_handler = None self.destroy() logger.info("Tkinter window destroyed.") else: logger.info("Window destruction aborted by user.") class ScrolledTextLogHandler(logging.Handler): # No changes 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 as e: print(f"ScrolledTextLogHandler TclError (widget likely destroyed): {e} - Record: {self.format(record)}") self._active = False except Exception as e: print(f"ScrolledTextLogHandler unexpected error: {e} - Record: {self.format(record)}") self._active = False def close(self): self._active = False super().close()