Skip to content

Commit 54d7443

Browse files
linesightclaude
andcommitted
linux: fix window-close hang, popup lifecycle, PDF frame crash
Three independent fixes for the CEF 146 Linux embedding path: 1. window_utils_linux: _on_configure — replace XResizeWindow+XSync with NotifyMoveOrResizeStarted()+SetBounds(). XSync() blocks the Python GIL on every GTK configure-event; when the GIL is held the GLib event loop stalls, the window manager marks the window as unresponsive, and sends a spurious WM_DELETE_WINDOW that closes the browser unexpectedly. SetBounds() already calls XConfigureWindow+XFlush internally, so the explicit XResizeWindow was redundant. NotifyMoveOrResizeStarted() is required before SetBounds() so the CEF compositor can prepare. 2. window_utils_linux: _on_delete / QuitMessageLoop — two related fixes: a. Add _linux_close_popup_browsers() called from _on_delete so that any JS-created popup browsers are queued for close in the same drain pass as the main browser, avoiding Shutdown()'s emergency force-close path. b. Always call gtk_main_quit() in _on_delete. When _on_delete returns False, GTK destroys the X11 parent window which also destroys CEF's embedded child window via the X11 parent-child relationship. CEF does not fire OnBeforeClose through its normal path after that, so QuitMessageLoop() would never be called and the GLib timer would spin MessageLoopWork() at 100% CPU indefinitely. The drain loop in _linux_message_loop() handles remaining cleanup after gtk_main() returns. c. Guard QuitMessageLoop()'s gtk_main_quit() with gtk_main_level() > 0 to suppress the "assertion 'main_loops != NULL' failed" GTK warning that fires when QuitMessageLoop() runs during the post-gtk_main() drain after _on_delete has already called gtk_main_quit(). 3. frame: GetPyFrame — replace the blanket assert on frameId/browserId with a targeted Exception for browserId==0 only. CEF 146 fires OnLoadStart with an empty frameId for internal frames created before the renderer frame is ready (e.g. the PDF-viewer extension frame). The assert was incorrectly blocking the frameId.empty() handling that already existed below it, causing an AssertionError → ExceptHook → abort when loading any PDF URL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 509f2ac commit 54d7443

3 files changed

Lines changed: 51 additions & 5 deletions

File tree

src/cefpython.pyx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,13 @@ def QuitMessageLoop():
959959
Debug("QuitMessageLoop()")
960960
IF UNAME_SYSNAME == "Linux":
961961
import ctypes as _ct
962-
_ct.CDLL("libgtk-3.so.0").gtk_main_quit()
962+
_gtk = _ct.CDLL("libgtk-3.so.0")
963+
# Only call gtk_main_quit() when a GTK main loop is actually running.
964+
# _on_delete() may have already called it (via gtk_main_quit directly),
965+
# and calling it a second time during the drain would generate a
966+
# spurious "assertion 'main_loops != NULL' failed" warning.
967+
if _gtk.gtk_main_level() > 0:
968+
_gtk.gtk_main_quit()
963969
with nogil:
964970
CefQuitMessageLoop()
965971

src/frame.pyx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ cdef PyFrame GetPyFrame(CefRefPtr[CefFrame] cefFrame):
2929
cdef PyFrame pyFrame
3030
cdef CefString frameId = cefFrame.get().GetIdentifier()
3131
cdef int browserId = cefFrame.get().GetBrowser().get().GetIdentifier()
32-
assert (not frameId.empty() and browserId), "frameId or browserId empty"
32+
if not browserId:
33+
raise Exception("GetPyFrame(): browserId is 0 (browser not yet initialised)")
34+
# frameId may be empty for internal frames that CEF creates before the
35+
# underlying renderer frame is ready (e.g. the PDF-viewer internal frame
36+
# on the first OnLoadStart). The code below already creates an incomplete
37+
# PyFrame for this case, so do not assert here.
3338
cdef str uniqueFrameId = GetUniqueFrameId(browserId, CefToPyString(frameId))
3439

3540
if frameId.empty():

src/window_utils_linux.pyx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,25 @@ def _linux_create_toplevel(title, width=800, height=600):
258258
}
259259

260260

261+
def _linux_close_popup_browsers(main_id):
262+
"""Queue CloseBrowser(True) for every tracked browser except main_id.
263+
264+
Called when the main GTK window delete-event fires so that popup browsers
265+
are shut down in the same event-loop pass. Without this, cef.Shutdown()
266+
can be called while popups are still alive, hitting the emergency
267+
force-close path in Shutdown() and producing a spurious gtk_main_quit()
268+
assertion warning when QuitMessageLoop fires later.
269+
"""
270+
for bid in list(g_pyBrowsers.keys()):
271+
if bid != main_id:
272+
pb = GetPyBrowserById(bid)
273+
if pb:
274+
try:
275+
pb.CloseBrowser(True)
276+
except Exception:
277+
pass
278+
279+
261280
def _linux_register_window_callbacks(browser, ws):
262281
"""Register resize and close callbacks for a standalone GTK toplevel.
263282
@@ -285,9 +304,13 @@ def _linux_register_window_callbacks(browser, ws):
285304
if _b:
286305
_chrome_xid = _b.GetWindowHandle()
287306
if _chrome_xid:
288-
_x11.XResizeWindow(_xdisp, _ct.c_ulong(_chrome_xid),
289-
_ct.c_uint(nw), _ct.c_uint(nh))
290-
_x11.XSync(_xdisp, _ct.c_int(0))
307+
# Notify CEF of the incoming resize before applying it so
308+
# the compositor can prepare (avoids blank frames).
309+
# SetBounds calls XConfigureWindow + XFlush internally;
310+
# the redundant XResizeWindow+XSync was removed because
311+
# XSync blocks the GIL and can make the WM consider the
312+
# window unresponsive (triggering spurious WM_DELETE_WINDOW).
313+
_b.NotifyMoveOrResizeStarted()
291314
_b.SetBounds(0, 0, nw, nh)
292315
return False
293316
_conf_cb = _ConfigureCb(_on_configure)
@@ -301,7 +324,19 @@ def _linux_register_window_callbacks(browser, ws):
301324
def _on_delete(_w, _ev, _ud):
302325
_b = _browser_ref[0]
303326
if _b:
327+
# Close any open popup browsers so the drain loop in
328+
# _linux_message_loop() can clean them up without hitting the
329+
# emergency force-close path in Shutdown().
330+
_linux_close_popup_browsers(_b.GetIdentifier())
304331
_b.CloseBrowser(True)
332+
# Always call gtk_main_quit() here. When _on_delete returns False,
333+
# GTK destroys the X11 parent window, which also destroys CEF's
334+
# embedded child window via X11's parent-child relationship. CEF
335+
# does not fire OnBeforeClose through its normal path after the X11
336+
# window is destroyed externally, so QuitMessageLoop() would never be
337+
# called and the GTK main loop would spin at 100% CPU. The drain
338+
# loop in _linux_message_loop() handles remaining CEF cleanup after
339+
# gtk_main() returns.
305340
_gtk.gtk_main_quit()
306341
return False
307342
_del_cb = _DeleteCb(_on_delete)

0 commit comments

Comments
 (0)