Skip to content

Commit 5029683

Browse files
linesightclaude
andcommitted
Add Linux CEF 146 native-windowed embedding support
CEF 146 on Ubuntu 24 (GNOME/Wayland) requires several coordinated fixes to embed the browser in a native X11 window without a separate GUI toolkit in user code. This commit implements all of them. Embedding / window management - WindowInfo.SetAsChild() on Linux now substitutes the X11 root window as CEF's parent to avoid the Xwayland cross-client MatchError, and stores the real parent XID in _linux_embed_info for deferred reparenting. - _linux_schedule_xembed() polls until Chrome's window is IsViewable, then XUnmapWindow + 100 ms GLib timer + XReparentWindow into the real parent. Called from CreateBrowserSync() on both the direct and pending-browser paths so the reparent always fires regardless of when Initialize() returns. - When CreateBrowserSync() is called with no parent on Linux, the library now auto-creates a GTK toplevel, embeds the browser, and registers configure-event (resize) and delete-event (close) handlers — making hello_world.py fully platform-agnostic. Platform init helpers (window_utils_linux.pyx) - _linux_gtk_init(): sets GDK_BACKEND=x11 before gtk_init() so GDK opens an X11/Xwayland display connection regardless of the desktop session. - _linux_apply_initialize_defaults(): auto-applies CEF 146 switches (ozone-platform=x11, disable-zygote, disable-gpu, disable-features=…) so apps don't need to set them manually. - _linux_setup_profile(): pre-seeds Chrome profile files to prevent the kProfileCreationFlow keepalive from blocking OnContextInitialized. - _linux_message_loop(): runs gtk_main() with a 10 ms GLib timer driving CefDoMessageLoopWork(), satisfying CEF's Ozone X11 GLib-loop requirement. Subprocess fixes - Strip --pseudonymization-salt-handle from argv in subprocess/main.cpp (Chrome 130+ passes it to non-zygote subprocesses without pre-populating GlobalDescriptors[7], causing an immediate crash). - Strip both --pseudonymization-salt-handle and --change-stack-guard-on-fork in OnBeforeChildProcessLaunch via _StripPseudonymizationSaltHandle(). - subprocess/CMakeLists.txt: add BROWSER_PROCESS define and set BUILD_RPATH/INSTALL_RPATH=$ORIGIN so the subprocess finds libcef.so from its installed directory on nosuid filesystems (VMware HGFS). Other fixes - cefpython3/__init__.py: set GDK_BACKEND=x11 at import time, before libcef.so is loaded, as the earliest safe point. - x11.cpp: add null guards in SetX11WindowBounds/SetX11WindowTitle; document the XEMBED side-effect of CefBrowser_GetGtkWindow(). - cef_command_line.pxd: expose Reset, GetProgram/SetProgram, GetSwitches, GetArguments, AppendArgument needed by _StripPseudonymizationSaltHandle. - linux.pxd: expose g_main_context_iteration for GLib pump in Initialize. - tools/build.py: fix CEF glob for numbered dirs, write dev .pth file, pass PYTHONPATH when running unittests. - examples/hello_world.py: remove all Linux-specific GTK/env boilerplate; now identical in structure to the Windows/Mac usage pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bc1cfad commit 5029683

15 files changed

Lines changed: 711 additions & 59 deletions

File tree

cefpython3/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
os.environ["CEFPYTHON3_PATH"] = package_dir
2121

2222
if platform.system() == "Linux":
23+
# Force GDK to use X11/XWayland backend before libcef.so loads and
24+
# initialises GDK. Without this, GDK picks the Wayland backend on
25+
# GNOME/Wayland sessions, making gdk_x11_window_get_xid() return 0.
26+
os.environ.setdefault("GDK_BACKEND", "x11")
2327
_ld = os.environ.get("LD_LIBRARY_PATH", "")
2428
os.environ["LD_LIBRARY_PATH"] = (
2529
package_dir + os.pathsep + _ld if _ld else package_dir

examples/hello_world.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,18 @@
88
# Setting DPI awareness programmatically via a call to cef.DpiAware.EnableHighDpiSupport
99
# is problematic in Python, may not work and can cause display glitches.
1010

11+
import sys
12+
1113
from cefpython3 import cefpython as cef
1214
import platform
13-
import sys
1415
from packaging.version import Version as parse_version
1516

1617

1718
def main():
1819
check_versions()
19-
sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error
20-
switches = {}
21-
if sys.platform.startswith("linux"):
22-
# cefpython does not ship a chrome-sandbox (setuid) binary.
23-
# Disable both the setuid and namespace sandboxes so Chrome runs
24-
# subprocesses without sandboxing. Unlike --no-sandbox, these two
25-
# flags do NOT suppress the Mojo IPC bootstrap fd registration
26-
# (GlobalDescriptors key 7), so subprocesses can still communicate.
27-
switches["disable-setuid-sandbox"] = ""
28-
switches["disable-namespace-sandbox"] = ""
29-
# /dev/shm may be too small in VMs and containers.
30-
switches["disable-dev-shm-usage"] = ""
31-
# Suppress the GNOME Keyring unlock prompt on desktop sessions.
32-
switches["password-store"] = "basic"
33-
# Virtual GPU hardware (e.g. VMware) may not expose the DMA-BUF / GBM
34-
# interface required by Chrome's GPU process. Keep the GPU in-process.
35-
switches["disable-gpu"] = ""
36-
switches["disable-gpu-compositing"] = ""
37-
switches["in-process-gpu"] = ""
38-
# Keep storage and network services in-process to reduce subprocess
39-
# spawning overhead.
40-
switches["disable-features"] = "StorageServiceOutOfProcess"
41-
switches["enable-features"] = "NetworkServiceInProcess"
42-
cef.Initialize(switches=switches)
20+
sys.excepthook = cef.ExceptHook # shut down all CEF processes on error
21+
settings = {}
22+
cef.Initialize(settings=settings)
4323
cef.CreateBrowserSync(url="https://www.google.com/",
4424
window_title="Hello World!")
4525
cef.MessageLoop()
@@ -54,7 +34,8 @@ def check_versions():
5434
print("[hello_world.py] Python {ver} {arch}".format(
5535
ver=platform.python_version(),
5636
arch=platform.architecture()[0]))
57-
assert parse_version(cef.__version__) >= parse_version("57.0"), "CEF Python v57.0+ required to run this"
37+
assert parse_version(cef.__version__) >= parse_version("57.0"), \
38+
"CEF Python v57.0+ required to run this"
5839

5940

6041
if __name__ == '__main__':

src/cefpython.pyx

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ cdef py_bool g_cef_initialized = False
307307
cdef py_bool g_context_initialized = False
308308
cdef list g_pending_browsers = []
309309

310+
IF UNAME_SYSNAME == "Linux":
311+
# Keeps ctypes callback objects alive for the duration of gtk_main() and
312+
# any pending one-shot GLib timers (Xwayland XReparentWindow scheduling).
313+
g_linux_reparent_callbacks = []
314+
310315
cdef dict g_globalClientCallbacks = {}
311316

312317
# -----------------------------------------------------------------------------
@@ -571,6 +576,18 @@ def Initialize(applicationSettings=None, commandLineSwitches=None, **kwargs):
571576
if "use-angle" not in g_commandLineSwitches:
572577
g_commandLineSwitches["use-angle"] = "gl"
573578

579+
IF UNAME_SYSNAME == "Linux":
580+
# Initialize GTK so GDK has a display connection before CefInitialize.
581+
_linux_gtk_init()
582+
# Auto-apply switches/settings required for CEF 146 on Linux/Xwayland.
583+
# Uses setdefault so user-supplied values are never overwritten.
584+
_linux_apply_initialize_defaults(application_settings,
585+
g_commandLineSwitches)
586+
# Pre-seed Chrome profile files to prevent the profile-picker keepalive
587+
# from blocking OnContextInitialized (Chrome 146).
588+
if application_settings.get("cache_path"):
589+
_linux_setup_profile(application_settings["cache_path"])
590+
574591
cdef CefRefPtr[CefApp] cefApp = <CefRefPtr[CefApp]?>new CefPythonApp()
575592

576593
IF UNAME_SYSNAME == "Windows":
@@ -658,13 +675,23 @@ def Initialize(applicationSettings=None, commandLineSwitches=None, **kwargs):
658675
# a null browser from CefBrowserHost::CreateBrowserSync().
659676
# Use a generous ceiling (30s) for CI environments where utility
660677
# subprocesses (storage service) crash and delay context initialization.
678+
# On Linux, also call g_main_context_iteration() explicitly so that GLib
679+
# sources (X11 fd events, IPC pipes) are dispatched even if
680+
# CefDoMessageLoopWork() uses only non-blocking GLib iteration internally.
661681
if ret:
662-
for _ in range(3000):
663-
with nogil:
664-
CefDoMessageLoopWork()
665-
if g_context_initialized:
666-
break
667-
time.sleep(0.01)
682+
# On Linux, skip this pump entirely: the Ozone X11 backend needs
683+
# gtk_main() (a blocking GLib main loop) running before
684+
# OnContextInitialized can fire. The external caller (hello_world.py,
685+
# test harnesses) must enter gtk_main() immediately after Initialize()
686+
# and drive the loop via the GLib timer callback.
687+
# On Windows/macOS, pump up to 30 s as before.
688+
IF UNAME_SYSNAME != "Linux":
689+
for _ in range(3000):
690+
with nogil:
691+
CefDoMessageLoopWork()
692+
if g_context_initialized:
693+
break
694+
time.sleep(0.01)
668695
if not g_context_initialized:
669696
Debug("CefInitialize() WARNING: OnContextInitialized not received"
670697
" within 30 seconds")
@@ -710,12 +737,13 @@ def CreateBrowserSync(windowInfo=None,
710737
if not g_context_initialized:
711738
Debug("CreateBrowserSync(): OnContextInitialized not yet received,"
712739
" pumping message loop")
713-
for _ in range(3000):
714-
with nogil:
715-
CefDoMessageLoopWork()
716-
if g_context_initialized:
717-
break
718-
time.sleep(0.01)
740+
IF UNAME_SYSNAME != "Linux":
741+
for _ in range(3000):
742+
with nogil:
743+
CefDoMessageLoopWork()
744+
if g_context_initialized:
745+
break
746+
time.sleep(0.01)
719747
if not g_context_initialized:
720748
Debug("CreateBrowserSync() deferred until OnContextInitialized")
721749
g_pending_browsers.append({
@@ -773,6 +801,18 @@ def CreateBrowserSync(windowInfo=None,
773801
elif not isinstance(windowInfo, WindowInfo):
774802
raise Exception("CreateBrowserSync() failed: windowInfo: invalid object")
775803

804+
# On Linux, when no parent window is given, auto-create a GTK toplevel
805+
# so callers need no GTK-specific code (same API as Windows/Mac).
806+
_linux_toplevel_state = None
807+
IF UNAME_SYSNAME == "Linux":
808+
if windowInfo.windowType == "child" and windowInfo.parentWindowHandle == 0:
809+
_linux_toplevel_state = _linux_create_toplevel(
810+
window_title or "CEF Browser")
811+
windowInfo.SetAsChild(
812+
_linux_toplevel_state['xid'],
813+
[0, 0, _linux_toplevel_state['width'],
814+
_linux_toplevel_state['height']])
815+
776816
if window_title and windowInfo.parentWindowHandle == 0:
777817
windowInfo.windowName = window_title
778818

@@ -871,6 +911,12 @@ def CreateBrowserSync(windowInfo=None,
871911
MacSetWindowTitle(cefBrowser,
872912
PyStringToChar(windowInfo.windowName))
873913

914+
IF UNAME_SYSNAME == "Linux":
915+
if windowInfo._linux_embed_info:
916+
_linux_schedule_xembed(pyBrowser, windowInfo._linux_embed_info)
917+
if _linux_toplevel_state is not None:
918+
_linux_register_window_callbacks(pyBrowser, _linux_toplevel_state)
919+
874920
return pyBrowser
875921

876922
def MessageLoop():
@@ -880,8 +926,11 @@ def MessageLoop():
880926
global g_MessageLoop_called
881927
g_MessageLoop_called = True
882928

883-
with nogil:
884-
CefRunMessageLoop()
929+
IF UNAME_SYSNAME == "Linux":
930+
_linux_message_loop()
931+
ELSE:
932+
with nogil:
933+
CefRunMessageLoop()
885934

886935
def MessageLoopWork():
887936
# Perform a single iteration of CEF message loop processing.
@@ -907,6 +956,9 @@ def SingleMessageLoop():
907956

908957
def QuitMessageLoop():
909958
Debug("QuitMessageLoop()")
959+
IF UNAME_SYSNAME == "Linux":
960+
import ctypes as _ct
961+
_ct.CDLL("libgtk-3.so.0").gtk_main_quit()
910962
with nogil:
911963
CefQuitMessageLoop()
912964

src/client_handler/x11.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ void SetX11WindowBounds(CefRefPtr<CefBrowser> browser,
3636
int x, int y, int width, int height) {
3737
::Window xwindow = browser->GetHost()->GetWindowHandle();
3838
::Display* xdisplay = cef_get_xdisplay();
39+
if (!xdisplay || !xwindow) return;
3940
XWindowChanges changes = {0};
4041
changes.x = x;
4142
changes.y = y;
@@ -48,12 +49,21 @@ void SetX11WindowBounds(CefRefPtr<CefBrowser> browser,
4849
void SetX11WindowTitle(CefRefPtr<CefBrowser> browser, char* title) {
4950
::Window xwindow = browser->GetHost()->GetWindowHandle();
5051
::Display* xdisplay = cef_get_xdisplay();
52+
if (!xdisplay || !xwindow) return;
5153
XStoreName(xdisplay, xwindow, title);
5254
}
5355

5456
GtkWindow* CefBrowser_GetGtkWindow(CefRefPtr<CefBrowser> browser) {
5557
// TODO: Should return NULL when using the Views framework
5658
// -- REWRITTEN FOR CEF PYTHON USE CASE --
59+
//
60+
// WARNING (CEF 146 Ozone X11): gtk_plug_new_for_display() below sends an
61+
// XEMBED_EMBEDDED_NOTIFY to the browser's X11 window, which causes GTK to
62+
// call XReparentWindow and move the browser window into a new GtkSocket.
63+
// This breaks embedded-window positioning. Only call this function when
64+
// showing a transient dialog (file chooser, print dialog) where the browser
65+
// window displacement is acceptable or the dialog is temporary.
66+
//
5767
// X11 window handle
5868
::Window xwindow = browser->GetHost()->GetWindowHandle();
5969
// X11 display
@@ -99,7 +109,7 @@ GtkWindow* CefBrowser_GetGtkWindow(CefRefPtr<CefBrowser> browser) {
99109
XImage* CefBrowser_GetImage(CefRefPtr<CefBrowser> browser) {
100110
::Display* display = cef_get_xdisplay();
101111
if (!display) {
102-
LOG(ERROR) << "XOpenDisplay failed in CefBrowser_GetImage";
112+
LOG(ERROR) << "cef_get_xdisplay() returned NULL in CefBrowser_GetImage";
103113
return NULL;
104114
}
105115
::Window browser_window = browser->GetHost()->GetWindowHandle();

src/extern/cef/cef_command_line.pxd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ include "compile_time_constants.pxi"
99

1010
from cef_string cimport CefString
1111
from libcpp cimport bool as cpp_bool
12+
from libcpp.vector cimport vector as cpp_vector
13+
from libcpp.map cimport map as cpp_map
1214

1315
cdef extern from "include/cef_command_line.h":
1416
cdef cppclass CefCommandLine:
@@ -17,3 +19,9 @@ cdef extern from "include/cef_command_line.h":
1719
CefString GetCommandLineString()
1820
cpp_bool HasSwitch(const CefString& name)
1921
CefString GetSwitchValue(const CefString& name)
22+
void Reset()
23+
CefString GetProgram()
24+
void SetProgram(const CefString& program)
25+
void GetSwitches(cpp_map[CefString, CefString]& switches)
26+
void GetArguments(cpp_vector[CefString]& arguments)
27+
void AppendArgument(const CefString& argument)

src/extern/linux.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ cdef extern from "gtk/gtk.h" nogil:
66
ctypedef void* GtkWidget
77
cdef GtkWidget* gtk_plug_new(unsigned long socket_id)
88
cdef void gtk_widget_show(GtkWidget* widget)
9+
ctypedef void* GMainContext
10+
int g_main_context_iteration(GMainContext* context, int may_block)
911

1012
ctypedef char* XPointer
1113
ctypedef struct XImage:

0 commit comments

Comments
 (0)