Skip to content

Commit 27255c6

Browse files
committed
fix: prevent TclError when dashboard destroyed during bot operation
- Add _safe_after method that checks widget existence before scheduling callbacks - Add _alive flag and override destroy() to stop bot before frame destruction - Replace all self.after() calls with _safe_after() in DashboardFrame - Wrap _append_log with winfo_exists() check
1 parent 242fdda commit 27255c6

1 file changed

Lines changed: 51 additions & 18 deletions

File tree

  • src/opencode_telegram_bot

src/opencode_telegram_bot/gui.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,38 @@ def __init__(self, master: Any, settings: Settings) -> None:
261261
self._stop_event = threading.Event()
262262
self._bot_app = None
263263
self._bot_handler = None
264+
self._alive = True
264265

265266
self._build()
266267
self._start_status_poll()
267268

269+
def _safe_after(self, delay: int, func, *args, **kwargs) -> str | None:
270+
if not self._alive:
271+
return None
272+
try:
273+
if not self.winfo_exists():
274+
return None
275+
except Exception:
276+
return None
277+
def wrapper():
278+
if not self._alive:
279+
return
280+
try:
281+
if not self.winfo_exists():
282+
return
283+
except Exception:
284+
return
285+
func(*args, **kwargs)
286+
return self.after(delay, wrapper)
287+
288+
def destroy(self) -> None:
289+
self._alive = False
290+
if self.bot_running:
291+
self._stop_bot()
292+
import time
293+
time.sleep(0.5)
294+
super().destroy()
295+
268296
def _build(self) -> None:
269297
self.grid_columnconfigure(0, weight=1)
270298
self.grid_columnconfigure(1, weight=1)
@@ -357,11 +385,15 @@ def _build(self) -> None:
357385
_CTkButtonSecondary(btn_logs_frame, text="Export Logs", command=self._export_logs, height=36, width=120).pack(side="left")
358386

359387
def _log_callback(self, msg: str) -> None:
360-
self.after(0, lambda: self._append_log(msg))
388+
self._safe_after(0, self._append_log, msg)
361389

362390
def _append_log(self, msg: str) -> None:
363-
self.log_text.insert("end", msg + "\n")
364-
self.log_text.see("end")
391+
try:
392+
if self.winfo_exists():
393+
self.log_text.insert("end", msg + "\n")
394+
self.log_text.see("end")
395+
except Exception:
396+
pass
365397

366398
def _clear_logs(self) -> None:
367399
self.log_text.delete("1.0", "end")
@@ -382,8 +414,8 @@ def _start_status_poll(self) -> None:
382414
self._poll_status()
383415

384416
def _poll_status(self) -> None:
385-
self.after(0, self._do_poll)
386-
self.after(5000, self._poll_status)
417+
self._safe_after(0, self._do_poll)
418+
self._safe_after(5000, self._poll_status)
387419

388420
def _do_poll(self) -> None:
389421
def check():
@@ -393,9 +425,9 @@ def check():
393425
health = loop.run_until_complete(self.client.health())
394426
loop.close()
395427
status_val = health.get("healthy", health.get("status", "unknown"))
396-
self.after(0, lambda: self.server_label.configure(text=f"Server: {status_val}", text_color=COLORS["success"]))
428+
self._safe_after(0, lambda: self.server_label.configure(text=f"Server: {status_val}", text_color=COLORS["success"]))
397429
except Exception:
398-
self.after(0, lambda: self.server_label.configure(text="Server: Offline", text_color=COLORS["error"]))
430+
self._safe_after(0, lambda: self.server_label.configure(text="Server: Offline", text_color=COLORS["error"]))
399431

400432
threading.Thread(target=check, daemon=True).start()
401433

@@ -500,14 +532,14 @@ async def run():
500532
await app.initialize()
501533
await app.start()
502534
await app.updater.start_polling(drop_pending_updates=True)
503-
self.after(0, lambda: self.bot_label.configure(text="Bot: Running", text_color=COLORS["success"]))
504-
self.after(0, lambda: self._append_log("Bot polling started."))
535+
self._safe_after(0, lambda: self.bot_label.configure(text="Bot: Running", text_color=COLORS["success"]))
536+
self._safe_after(0, self._append_log, "Bot polling started.")
505537
while not self._stop_event.is_set():
506538
await asyncio.sleep(0.5)
507539
except Exception as exc:
508540
err_msg = f"Bot error: {exc}"
509-
self.after(0, lambda m=err_msg: self._append_log(m))
510-
self.after(0, lambda: self.bot_label.configure(text="Bot: Error", text_color=COLORS["error"]))
541+
self._safe_after(0, self._append_log, err_msg)
542+
self._safe_after(0, lambda: self.bot_label.configure(text="Bot: Error", text_color=COLORS["error"]))
511543
finally:
512544
try:
513545
if app.updater.running:
@@ -521,19 +553,19 @@ async def run():
521553
except Exception:
522554
pass
523555
scheduler.stop()
524-
self.after(0, lambda: self.bot_label.configure(text="Bot: Stopped", text_color=COLORS["text_secondary"]))
525-
self.after(0, lambda: self._append_log("Bot stopped."))
556+
self._safe_after(0, lambda: self.bot_label.configure(text="Bot: Stopped", text_color=COLORS["text_secondary"]))
557+
self._safe_after(0, self._append_log, "Bot stopped.")
526558
self.bot_running = False
527-
self.after(0, lambda: self.start_bot_btn.configure(text="Start Bot", fg_color=COLORS["accent"], text_color="#FFFFFF"))
559+
self._safe_after(0, lambda: self.start_bot_btn.configure(text="Start Bot", fg_color=COLORS["accent"], text_color="#FFFFFF"))
528560

529561
loop = asyncio.new_event_loop()
530562
asyncio.set_event_loop(loop)
531563
try:
532564
loop.run_until_complete(run())
533565
except Exception as exc:
534566
err_msg = f"Bot loop error: {exc}"
535-
self.after(0, lambda m=err_msg: self._append_log(m))
536-
self.after(0, lambda: self.bot_label.configure(text="Bot: Error", text_color=COLORS["error"]))
567+
self._safe_after(0, self._append_log, err_msg)
568+
self._safe_after(0, lambda: self.bot_label.configure(text="Bot: Error", text_color=COLORS["error"]))
537569
finally:
538570
loop.close()
539571

@@ -556,9 +588,10 @@ def fetch():
556588
asyncio.set_event_loop(loop)
557589
providers = loop.run_until_complete(self.client.get_config_providers())
558590
loop.close()
559-
self.after(0, lambda p=providers: self._render_models(p))
591+
self._safe_after(0, self._render_models, providers)
560592
except Exception as exc:
561-
self.after(0, lambda e=str(exc): self._append_log(f"Failed to load models: {e}"))
593+
err_msg = f"Failed to load models: {exc}"
594+
self._safe_after(0, self._append_log, err_msg)
562595

563596
threading.Thread(target=fetch, daemon=True).start()
564597

0 commit comments

Comments
 (0)