fix(ui): guard ProgressWindow.destroy() against double-destroy TclError#1403
Conversation
When progress reaches 100%, update_progress_bar() auto-destroys the Toplevel. The context manager __exit__ then calls destroy() again, raising TclError on the already-destroyed widget. Added winfo_exists() check before calling destroy(). Fixes ArduPilot#1401 Signed-off-by: Yash Goel <yashgoel249@gmail.com>
There was a problem hiding this comment.
Pull request overview
Guards ProgressWindow.destroy() against double-destroy scenarios in Tkinter to prevent TclError, and adds regression tests covering the crash path described in #1401.
Changes:
- Add a
winfo_exists()check before destroying the TkinterToplevel. - Add two tests to ensure calling
destroy()twice (including after auto-destroy at 100%) does not raise.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
ardupilot_methodic_configurator/frontend_tkinter_progress_window.py |
Adds existence guard before destroying the Tk widget to avoid TclError on double-destroy. |
tests/test_frontend_tkinter_progress_window.py |
Adds regression tests for double-destroy, including the “auto-destroy at 100% then manual destroy” scenario. |
| if self.progress_window.winfo_exists(): | ||
| self.progress_window.destroy() |
There was a problem hiding this comment.
winfo_exists() and destroy() can still raise tkinter.TclError if the underlying Tcl interpreter (or root) has already been destroyed (e.g., during app shutdown). To make destroy() fully idempotent and safe during teardown, consider wrapping the existence check + destroy call in a try/except TclError and treating that error as a no-op in this cleanup path.
| if self.progress_window.winfo_exists(): | |
| self.progress_window.destroy() | |
| """ | |
| Safely destroy the progress window, ignoring TclError during interpreter teardown. | |
| """ | |
| try: | |
| if ( | |
| hasattr(self, "progress_window") | |
| and self.progress_window is not None | |
| and self.progress_window.winfo_exists() | |
| ): | |
| self.progress_window.destroy() | |
| except tk.TclError: | |
| # Interpreter or widget may already be destroyed; ignore in teardown. | |
| pass |
| """ | ||
| User can safely destroy a progress window that was already destroyed. | ||
|
|
||
| GIVEN: A progress window that has been destroyed (e.g. by reaching 100%) | ||
| WHEN: destroy() is called again (e.g. by context manager __exit__) | ||
| THEN: No TclError is raised | ||
| """ | ||
| # First destroy | ||
| progress_window.destroy() |
There was a problem hiding this comment.
The docstring says the window was destroyed “e.g. by reaching 100%”, but the test destroys it manually. To keep the test self-descriptive, either adjust the GIVEN wording to “has already been destroyed” (without the 100% example) or change the setup to destroy via update_progress_bar(100, 100).
| assert not progress_window.progress_window.winfo_exists() | ||
|
|
||
| # Second destroy should not raise | ||
| progress_window.destroy() |
There was a problem hiding this comment.
This test currently only checks that the second destroy() call doesn’t raise. Adding a post-condition assertion (e.g., assert not progress_window.progress_window.winfo_exists()) would make the intended end state explicit and help catch regressions where destroy() might recreate/replace the widget reference.
| progress_window.destroy() | |
| progress_window.destroy() | |
| # Window should remain destroyed | |
| assert not progress_window.progress_window.winfo_exists() |
- Wrap winfo_exists + destroy in try/except TclError for interpreter teardown safety - Fix test docstring to match actual test behavior - Add post-condition winfo_exists() assertions after second destroy Signed-off-by: Yash Goel <yashgoel249@gmail.com>
|
Thanks, good catch. |
Summary
winfo_exists()guard inProgressWindow.destroy()to prevent TclError on double-destroyProblem
When a flight controller connection succeeds:
update_progress_bar(100, 100)auto-destroys the Toplevel (line 132-133)__exit__callsdestroy()againFlightControllerConnectionProgress.destroy()checksif self.progress_window:— alwaysTrue(Python object, not widget state)ProgressWindow.destroy()→self.progress_window.destroy()on already-destroyed Toplevel → TclErrorFix
Test plan
test_user_can_safely_destroy_progress_window_twiceverifies no crash on double-destroytest_user_sees_no_crash_when_progress_completes_then_destroysverifies the exact auto-destroy + manual destroy scenarioFixes #1401