Skip to content

Fix macOS SIGSEGV crash in live webcam mode (tkinter thread-safety)#1720

Open
llulioscesar wants to merge 4 commits intohacksider:mainfrom
llulioscesar:fix/macos-tkinter-thread-safety-crash
Open

Fix macOS SIGSEGV crash in live webcam mode (tkinter thread-safety)#1720
llulioscesar wants to merge 4 commits intohacksider:mainfrom
llulioscesar:fix/macos-tkinter-thread-safety-crash

Conversation

@llulioscesar
Copy link
Copy Markdown

@llulioscesar llulioscesar commented Mar 30, 2026

Summary

  • Fix SIGSEGV crash (Tk_FreeGCTkButtonWorldChangedConfigureButton) that occurs on macOS when starting live webcam mode
  • Replace direct tkinter widget updates from background threads with ROOT.after(0, ...) to schedule UI changes on the main Tk event loop
  • Remove ROOT.update() call from update_status() which re-enters the Tk event loop from background threads

Problem

On macOS (tested on Apple Silicon M2 Max, macOS 26.3.1), clicking "Live" in the webcam preview causes an immediate crash with:

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Thread 18 Crashed:
0 libtcl9tk9.0.dylib Tk_FreeGC + 24
1 libtcl9tk9.0.dylib TkButtonWorldChanged + 104
2 libtcl9tk9.0.dylib ConfigureButton + 2200

This happens because update_status(), update_pop_status(), and update_pop_live_status() call widget.configure() directly from background threads. Tkinter is not thread-safe on macOS, and modifying widgets from non-main threads causes segfaults in Tk's graphics context management.

Fix

  • Use ROOT.after(0, callback) to schedule all widget updates on the main Tk thread
  • Add null/existence guards to prevent crashes if widgets are destroyed before the callback executes
  • Remove ROOT.update() from update_status() (unnecessary with ROOT.after() and dangerous from background threads)

Test plan

  • Launch python run.py on macOS
  • Select a source face image
  • Click "Live" → should no longer crash
  • Verify status messages still update in the UI
  • Test on Linux/Windows to confirm no regression

Summary by Sourcery

Route tkinter status label updates through the main Tk event loop to prevent macOS crashes when updating the UI from background threads.

Bug Fixes:

  • Prevent SIGSEGV crashes on macOS live webcam mode caused by updating tkinter widgets from background threads.

Enhancements:

  • Schedule status and popup label updates via the Tk root event loop and add existence checks to avoid updating destroyed widgets.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 30, 2026

Reviewer's Guide

Refactors tkinter status update helpers to marshal all widget configuration calls onto the main Tk thread via ROOT.after, preventing macOS SIGSEGV crashes caused by updating widgets from background threads, and adds existence guards while removing a risky ROOT.update() call.

Sequence diagram for thread-safe status updates via ROOT.after

sequenceDiagram
    actor User
    participant BackgroundThread
    participant ui_module
    participant ROOT
    participant TkMainLoop
    participant status_label

    User->>BackgroundThread: trigger_long_running_task
    BackgroundThread->>ui_module: update_status(text)
    activate ui_module
    ui_module->>ui_module: translated = _(text)
    alt ROOT_is_not_none
        ui_module->>ROOT: after(0, _do)
        ROOT->>TkMainLoop: schedule_callback(_do)
        activate TkMainLoop
        TkMainLoop->>ui_module: execute _do()
        activate ui_module
        alt status_label_is_not_none
            ui_module->>status_label: configure(text=translated)
        end
        deactivate ui_module
        deactivate TkMainLoop
    else ROOT_is_none
        ui_module-->>BackgroundThread: no_ui_update
        deactivate ui_module
    end
Loading

Sequence diagram for thread-safe popup live status updates

sequenceDiagram
    actor User
    participant BackgroundThread
    participant ui_module
    participant ROOT
    participant TkMainLoop
    participant POPUP_LIVE
    participant popup_status_label_live

    User->>BackgroundThread: start_live_webcam
    BackgroundThread->>ui_module: update_pop_live_status(text)
    activate ui_module
    ui_module->>ui_module: translated = _(text)
    alt ROOT_is_not_none
        ui_module->>ROOT: after(0, _do)
        ROOT->>TkMainLoop: schedule_callback(_do)
        activate TkMainLoop
        TkMainLoop->>ui_module: execute _do()
        activate ui_module
        alt popup_status_label_live_and_POPUP_LIVE_valid
            ui_module->>POPUP_LIVE: winfo_exists()
            POPUP_LIVE-->>ui_module: exists
            ui_module->>popup_status_label_live: configure(text=translated)
        else popup_or_label_invalid
            ui_module-->>TkMainLoop: skip_update
        end
        deactivate ui_module
        deactivate TkMainLoop
    else ROOT_is_none
        ui_module-->>BackgroundThread: no_ui_update
        deactivate ui_module
    end
Loading

Flow diagram for guarded popup status update logic

flowchart TD
    A["update_pop_status(text) called"] --> B["translated = _(text)"]
    B --> C{"ROOT is not None"}
    C -- "No" --> Z["Return without scheduling UI update"]
    C -- "Yes" --> D["Call ROOT.after(0, _do)"]
    D --> E["Later on Tk main thread: execute _do()"]
    E --> F{"popup_status_label is not None"}
    F -- "No" --> Z
    F -- "Yes" --> G{"POPUP is not None"}
    G -- "No" --> Z
    G -- "Yes" --> H{"POPUP.winfo_exists() is True"}
    H -- "No" --> Z
    H -- "Yes" --> I["popup_status_label.configure(text=translated)"]
    I --> Z["End"]
Loading

File-Level Changes

Change Details Files
Make status label updates thread-safe by scheduling them on the main Tk event loop instead of calling widget.configure directly from background threads.
  • Store the translated status text in a local variable before scheduling the update
  • Wrap the label.configure call in an inner _do() callback
  • Guard the callback with a None-check on the status_label reference
  • Use ROOT.after(0, _do) when ROOT is available to enqueue the UI update on the main thread
modules/ui.py
Harden popup status updates (normal and live) against race conditions and destroyed widgets, while also moving them to the main Tk event loop.
  • Translate the text once and capture it in a local variable for each update function
  • Introduce inner _do() callbacks for popup_status_label and popup_status_label_live
  • Add guards ensuring the popup windows exist (POPUP/POPUP_LIVE not None and winfo_exists() is true) before configuring labels
  • Use ROOT.after(0, _do) when ROOT is available to perform the updates on the main thread
modules/ui.py
Remove direct re-entry into the Tk event loop from background threads to avoid unsafe concurrent Tk operations.
  • Delete the ROOT.update() call from update_status() as it is no longer needed with ROOT.after and is unsafe from non-main threads
modules/ui.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • When scheduling callbacks with ROOT.after, consider also checking ROOT.winfo_exists() (or handling TclError) to avoid crashes if the root window is destroyed before the callback is queued.
  • Even with existence checks before configure(), there is still a race if the widget is destroyed between the check and the configure; consider wrapping the configure() calls in a small try/except TclError to make this fully robust against teardown races.
  • The three update_* functions now share a very similar pattern (translate → inner _do → ROOT.after); extracting a small helper to schedule safe label updates on the main thread would reduce duplication and keep future changes to this behavior in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When scheduling callbacks with ROOT.after, consider also checking ROOT.winfo_exists() (or handling TclError) to avoid crashes if the root window is destroyed before the callback is queued.
- Even with existence checks before configure(), there is still a race if the widget is destroyed between the check and the configure; consider wrapping the configure() calls in a small try/except TclError to make this fully robust against teardown races.
- The three update_* functions now share a very similar pattern (translate → inner _do → ROOT.after); extracting a small helper to schedule safe label updates on the main thread would reduce duplication and keep future changes to this behavior in one place.

## Individual Comments

### Comment 1
<location path="modules/ui.py" line_range="760-761" />
<code_context>
+        if status_label is not None:
+            status_label.configure(text=translated)
+
+    if ROOT is not None:
+        ROOT.after(0, _do)


</code_context>
<issue_to_address>
**issue:** Consider guarding against a destroyed ROOT before calling `after` to avoid potential `TclError`.

If `ROOT` has been destroyed but the reference is still non-`None`, `ROOT.after` may raise `TclError`. To harden this, also check `ROOT.winfo_exists()` before scheduling the callback, mirroring the popup checks.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…d-safety

On macOS, calling tkinter widget.configure() from background threads causes
a segfault in Tk_FreeGC -> TkButtonWorldChanged -> ConfigureButton.

This commit replaces direct widget updates in update_status(),
update_pop_status(), and update_pop_live_status() with ROOT.after(0, ...)
to schedule UI changes on the main Tk event loop thread.

Also removes ROOT.update() from update_status() which re-enters the Tk
event loop from background threads, another source of crashes.
Addresses review feedback: if ROOT has been destroyed but the reference
is still non-None, ROOT.after() may raise TclError. Now all three
functions check winfo_exists() before scheduling, consistent with the
popup widget guards.
@llulioscesar llulioscesar force-pushed the fix/macos-tkinter-thread-safety-crash branch from 9ee8a2e to 9694b90 Compare March 30, 2026 13:46
The display loop checked PREVIEW.state() == "withdrawn" on its first
iteration, but PREVIEW.deiconify() had not yet completed, causing the
preview to close immediately. Add a small delay (100ms) before the
first frame check to let the window finish appearing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant