Create the GTK main webview lazily (#106)

- Window create builds only GTK chrome; the main WebKitWebView materializes on first web use with the zero scheme and bridge registration riding along, so canvas apps never start WebKit processes on Linux
- The canvas smoke now asserts zero WebKit processes and drops the sandbox workaround it existed for; child-webview bridge responses no longer require a main webview to exist

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-07-10 17:31:31 -05:00
committed by GitHub
parent d1852d5dc2
commit edbb2045b2
3 changed files with 117 additions and 32 deletions
+27 -7
View File
@@ -11,6 +11,8 @@
# 3. gpu_nonblank=true (real pixels were presented)
# 4. widget-click "Add task" -> '4 open' (automation input mutates state)
# 5. automate screenshot renders a non-empty PNG
# 6. ZERO WebKit helper processes for the whole run (the main WebView is
# lazy; a canvas app must never boot the WebKit stack)
#
# Deliberately NOT `set -e` (same as windows-canvas-smoke.sh): grep exits 1
# on zero matches, and under `set -e` an assignment like `x=$(grep ...)` or
@@ -19,13 +21,10 @@
# goes through fail(), which dumps the snapshot and the app log.
set -u
# WebKitGTK's bubblewrap sandbox needs unprivileged user namespaces, which
# ubuntu-24.04 runners restrict via AppArmor; without this the web process
# dies launching xdg-dbus-proxy and the app never publishes an automation
# snapshot (reproduced in a local container: ready=true never lands; with
# the sandbox disabled the full smoke passes). The sandbox is not what
# this smoke tests.
export WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS="${WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS:-1}"
# No WebKit sandbox workaround: a canvas app's main WebView is created
# lazily and never materializes, so no WebKit helper processes start and
# the runner's user-namespace restrictions never come into play. The
# zero-WebKit assertion below keeps it that way.
# GTK_A11Y=none: under Xvfb there is no session bus providing org.a11y.Bus,
# and GTK4's a11y init blocks ~25 s on the GDBus name lookup before warning
@@ -77,6 +76,21 @@ fail() {
exit 1
}
# Canvas apps must never spawn WebKit: the window's main WebView is
# created lazily and nothing in this app materializes it, so any
# WebKitWebProcess/WebKitNetworkProcess during the run means an eager
# creation regressed (and with it launch latency, resident helper
# processes, and the sandbox trouble this smoke used to work around).
assert_no_webkit() {
local helpers
helpers=$(pgrep -af 'WebKit(Web|Network)Process' 2>/dev/null)
if [ -n "$helpers" ]; then
echo "-- WebKit helper processes found ($1):"
echo "$helpers" | sed 's/^/ /'
fail "canvas app spawned WebKit processes ($1)"
fi
}
# ---- build ----------------------------------------------------------------
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
(cd "$app_dir" && zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true) \
@@ -101,6 +115,8 @@ app_pid=$!
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"
assert_no_webkit "after first presented frame"
echo "== zero WebKit processes after first presented frame"
# ---- 4: automation widget-click mutates the model --------------------------
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
@@ -117,5 +133,9 @@ echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
test -s .zig-cache/native-sdk-automation/screenshot-inbox-canvas.png \
|| fail "screenshot PNG missing or empty"
# ---- 6: still zero WebKit processes at the end of the run -------------------
assert_no_webkit "at end of run"
echo "== zero WebKit processes at end of run"
echo "PASS: linux canvas smoke"
exit 0
+6 -5
View File
@@ -141,11 +141,12 @@ jobs:
version: 0.16.0
- name: Install GTK and Xvfb
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev xvfb
# Drives the gpu_surface software path against system WebKitGTK under
# Xvfb: snapshot ready, gpu_backend=software, gpu_nonblank=true,
# automation widget-click, and a rendered screenshot. Sandbox/a11y
# env, the widened cold-start readiness budget (shared runners stall
# ~27 s before the first runtime event), and failure forensics (dump
# Drives the gpu_surface software path under Xvfb: snapshot ready,
# gpu_backend=software, gpu_nonblank=true, automation widget-click,
# a rendered screenshot, and ZERO WebKit helper processes (the main
# WebView is lazy; canvas apps never boot WebKit). A11y env, the
# widened cold-start readiness budget (shared runners stall ~27 s
# before the first runtime event), and failure forensics (dump
# snapshot + app log) all live in the script.
- name: Build and drive ui-inbox headless
run: .github/scripts/linux-canvas-smoke.sh
+84 -20
View File
@@ -2487,7 +2487,11 @@ static void on_bridge_message(WebKitUserContentManager *manager, JSCValue *js_re
}
}
}
const char *uri = webkit_web_view_get_uri(source_webview);
/* source_webview is NULL only if the sending manager matched nothing
* (a message can only originate from a WebView this host created, so
* this is belt-and-braces); origin_for_uri already maps NULL to the
* inline origin. */
const char *uri = source_webview ? webkit_web_view_get_uri(source_webview) : NULL;
char *computed_origin = win->bridge_origin && strcmp(label, "main") == 0 ? g_strdup(win->bridge_origin) : native_sdk_origin_for_uri(uri);
host->bridge_callback(host->bridge_context, win->id, label, strlen(label), message, strlen(message), computed_origin, strlen(computed_origin));
g_free(computed_origin);
@@ -2508,6 +2512,51 @@ static void native_sdk_setup_bridge(native_sdk_gtk_window_t *win) {
webkit_user_script_unref(script);
}
/* The zero:// custom scheme lives on the WebKitWebContext (every WebView
* this host creates shares the default context) and WebKit aborts on a
* second registration, so the first WebView actually created window
* main or child, whichever an app reaches first registers it and the
* host-level flag makes every later creation a no-op. */
static void native_sdk_register_zero_scheme(native_sdk_gtk_host_t *host, WebKitWebView *web_view) {
if (!host || host->scheme_registered || !web_view) return;
webkit_web_context_register_uri_scheme(webkit_web_view_get_context(web_view), "zero", native_sdk_asset_scheme_request, host, NULL);
host->scheme_registered = 1;
}
/* Create-on-first-use for a window's main WebView (the AppKit host's
* ensureMainWebViewForWindowId:). Pure peek reads (event emission,
* bridge completion echoes, reorder passes, focus) keep checking
* win->web_view and skip absent WebViews a page that was never created
* has no listeners to miss. Paths that MATERIALIZE web content (load,
* frame/zoom/layer placement) ensure first, so webview-first apps behave
* exactly as before while canvas-first apps never boot the out-of-process
* WebKit stack (web + network helper processes) at all. */
static WebKitWebView *native_sdk_ensure_main_webview(native_sdk_gtk_window_t *win) {
if (!win) return NULL;
if (win->web_view) return win->web_view;
if (!win->stack_root) return NULL;
win->content_manager = webkit_user_content_manager_new();
WebKitWebView *wv = WEBKIT_WEB_VIEW(
g_object_new(WEBKIT_TYPE_WEB_VIEW,
"user-content-manager", win->content_manager,
NULL));
win->web_view = wv;
native_sdk_register_zero_scheme(win->host, wv);
native_sdk_setup_bridge(win);
g_signal_connect(wv, "decide-policy", G_CALLBACK(on_decide_policy), win);
/* The overlay's main-child slot, where the eager create used to put
* it: the overlay allocates its main child the full stack area on the
* next layout pass, so materializing into an already-shown window
* sizes correctly like any other GTK4 child insertion. The reorder
* pass settles the sibling (paint) order among child views exactly
* like any other webview mutation. */
gtk_overlay_set_child(GTK_OVERLAY(win->stack_root), GTK_WIDGET(wv));
native_sdk_reorder_overlays(win);
return wv;
}
static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk_host_t *host, uint64_t window_id, const char *title, const char *label, double x, double y, double width, double height, int restore_frame, int resizable, int titlebar_style, double min_width, double min_height) {
if (native_sdk_find_window(host, window_id)) return NULL;
@@ -2591,18 +2640,11 @@ static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk
g_signal_connect(bar, "map", G_CALLBACK(native_sdk_on_header_bar_mapped), win->host);
}
win->content_manager = webkit_user_content_manager_new();
WebKitWebView *wv = WEBKIT_WEB_VIEW(
g_object_new(WEBKIT_TYPE_WEB_VIEW,
"user-content-manager", win->content_manager,
NULL));
win->web_view = wv;
if (!host->scheme_registered) {
webkit_web_context_register_uri_scheme(webkit_web_view_get_context(wv), "zero", native_sdk_asset_scheme_request, host, NULL);
host->scheme_registered = 1;
}
native_sdk_setup_bridge(win);
/* The window's main WebView is NOT created here: it materializes on
* first use (native_sdk_ensure_main_webview), because instantiating
* a WebKitWebView boots the whole out-of-process WebKit stack web
* and network helper processes that a canvas-first app would carry
* forever under a view it never shows. */
win->root_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
/* Declared content min-size floor: the size request on the content
* box floors user resizes without inflating the default size (the
@@ -2617,10 +2659,13 @@ static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk
gtk_widget_set_visible(win->menu_bar, host->menu_model != NULL);
gtk_box_append(GTK_BOX(win->root_box), win->menu_bar);
/* The overlay expands to fill the window on its own (hexpand/vexpand
* on an expanding box slot), so it needs no main child to claim the
* stack area the lazy WebView slots in later without a relayout of
* anything else. */
win->stack_root = gtk_overlay_new();
gtk_widget_set_hexpand(win->stack_root, TRUE);
gtk_widget_set_vexpand(win->stack_root, TRUE);
gtk_overlay_set_child(GTK_OVERLAY(win->stack_root), GTK_WIDGET(wv));
gtk_box_append(GTK_BOX(win->root_box), win->stack_root);
gtk_window_set_child(win->gtk_window, win->root_box);
native_sdk_install_file_drop_target(win);
@@ -2629,7 +2674,6 @@ static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk
g_signal_connect(win->gtk_window, "notify::default-height", G_CALLBACK(on_resize), win);
g_signal_connect(win->gtk_window, "notify::is-active", G_CALLBACK(on_focus), win);
g_signal_connect(win->gtk_window, "close-request", G_CALLBACK(on_close_request), win);
g_signal_connect(win->web_view, "decide-policy", G_CALLBACK(on_decide_policy), win);
GtkEventController *shortcut_controller = gtk_event_controller_key_new();
gtk_event_controller_set_propagation_phase(shortcut_controller, GTK_PHASE_CAPTURE);
g_signal_connect(shortcut_controller, "key-pressed", G_CALLBACK(on_shortcut_key_pressed), win);
@@ -2848,7 +2892,11 @@ void native_sdk_gtk_load_webview(native_sdk_gtk_host_t *host, const char *source
void native_sdk_gtk_load_window_webview(native_sdk_gtk_host_t *host, uint64_t window_id, const char *source, size_t source_len, int source_kind, const char *asset_root, size_t asset_root_len, const char *asset_entry, size_t asset_entry_len, const char *asset_origin, size_t asset_origin_len, int spa_fallback) {
native_sdk_gtk_window_t *win = native_sdk_find_window(host, window_id);
if (!win || !win->web_view) return;
/* Loading a source is what materializes the window's lazy main
* WebView; the runtime never issues a load for a canvas-first app
* with the default empty source (sceneNeedsMainWebView), so a window
* that never reaches here never starts WebKit. */
if (!win || !native_sdk_ensure_main_webview(win)) return;
char *src = native_sdk_strndup(source, source_len);
if (!src) return;
@@ -2916,11 +2964,15 @@ void native_sdk_gtk_bridge_respond_window(native_sdk_gtk_host_t *host, uint64_t
void native_sdk_gtk_bridge_respond_webview(native_sdk_gtk_host_t *host, uint64_t window_id, const char *webview_label, size_t webview_label_len, const char *response, size_t response_len) {
native_sdk_gtk_window_t *win = native_sdk_find_window(host, window_id);
if (!win || !win->web_view) return;
if (!win) return;
char *label = webview_label_len > 0 ? native_sdk_strndup(webview_label, webview_label_len) : native_sdk_strndup("main", 4);
if (!label) return;
WebKitWebView *target = NULL;
if (strcmp(label, "main") == 0) {
/* Peek, never ensure: a completion can only answer a request a
* live page sent, so a still-lazy main WebView has nothing to
* deliver to (and a child webview's response must not be dropped
* just because the window's main WebView never materialized). */
target = win->web_view;
} else {
native_sdk_gtk_webview_t *webview = native_sdk_find_webview(win, label);
@@ -2949,6 +3001,8 @@ void native_sdk_gtk_bridge_respond_webview(native_sdk_gtk_host_t *host, uint64_t
void native_sdk_gtk_emit_window_event(native_sdk_gtk_host_t *host, uint64_t window_id, const char *name, size_t name_len, const char *detail_json, size_t detail_json_len) {
native_sdk_gtk_window_t *win = native_sdk_find_window(host, window_id);
/* Peek, never ensure: a still-lazy main WebView has no page and so
* no listeners to miss. */
if (!win || !win->web_view) return;
char *event_name = native_sdk_strndup(name, name_len);
char *detail = native_sdk_strndup(detail_json, detail_json_len);
@@ -3538,6 +3592,8 @@ int native_sdk_gtk_focus_view(native_sdk_gtk_host_t *host, uint64_t window_id, c
return 0;
}
if (label_copy && strcmp(label_copy, "main") == 0) {
/* Peek, never ensure: focusing cannot mean "start a web page",
* so a still-lazy main WebView is an honest failure. */
GtkWidget *widget = win->web_view ? GTK_WIDGET(win->web_view) : NULL;
free(label_copy);
if (!widget || !gtk_widget_get_visible(widget) || !gtk_widget_get_sensitive(widget)) return 0;
@@ -3607,6 +3663,11 @@ int native_sdk_gtk_create_webview(native_sdk_gtk_host_t *host, uint64_t window_i
free(url_copy);
return 0;
}
/* A child webview can be the FIRST WebView a canvas-first app ever
* creates (the window's main WebView stays lazy), so it must be able
* to claim the zero:// scheme registration itself before its first
* load resolves an asset URL. */
native_sdk_register_zero_scheme(host, web_view);
native_sdk_gtk_webview_t *webview = &win->webviews[win->webview_count++];
memset(webview, 0, sizeof(*webview));
@@ -3636,7 +3697,10 @@ int native_sdk_gtk_create_webview(native_sdk_gtk_host_t *host, uint64_t window_i
int native_sdk_gtk_set_webview_frame(native_sdk_gtk_host_t *host, uint64_t window_id, const char *label, size_t label_len, double x, double y, double width, double height) {
native_sdk_gtk_window_t *win = native_sdk_find_window(host, window_id);
char *label_copy = label_len > 0 ? native_sdk_strndup(label, label_len) : NULL;
if (label_copy && strcmp(label_copy, "main") == 0 && win && win->web_view && native_sdk_valid_webview_frame(x, y, width, height)) {
/* Placing the main WebView is a materializing operation (the AppKit
* host's setWebViewFrameInWindow: ensures the same way): the runtime
* only positions "main" for scenes that declare a main webview view. */
if (label_copy && strcmp(label_copy, "main") == 0 && native_sdk_valid_webview_frame(x, y, width, height) && native_sdk_ensure_main_webview(win)) {
GtkWidget *widget = GTK_WIDGET(win->web_view);
gtk_widget_set_halign(widget, GTK_ALIGN_START);
gtk_widget_set_valign(widget, GTK_ALIGN_START);
@@ -3676,7 +3740,7 @@ int native_sdk_gtk_navigate_webview(native_sdk_gtk_host_t *host, uint64_t window
int native_sdk_gtk_set_webview_zoom(native_sdk_gtk_host_t *host, uint64_t window_id, const char *label, size_t label_len, double zoom) {
native_sdk_gtk_window_t *win = native_sdk_find_window(host, window_id);
char *label_copy = label_len > 0 ? native_sdk_strndup(label, label_len) : NULL;
if (label_copy && strcmp(label_copy, "main") == 0 && win && win->web_view && zoom >= 0.25 && zoom <= 5.0) {
if (label_copy && strcmp(label_copy, "main") == 0 && zoom >= 0.25 && zoom <= 5.0 && native_sdk_ensure_main_webview(win)) {
webkit_web_view_set_zoom_level(win->web_view, zoom);
free(label_copy);
return 1;
@@ -3691,7 +3755,7 @@ int native_sdk_gtk_set_webview_zoom(native_sdk_gtk_host_t *host, uint64_t window
int native_sdk_gtk_set_webview_layer(native_sdk_gtk_host_t *host, uint64_t window_id, const char *label, size_t label_len, int layer) {
native_sdk_gtk_window_t *win = native_sdk_find_window(host, window_id);
char *label_copy = label_len > 0 ? native_sdk_strndup(label, label_len) : NULL;
if (label_copy && strcmp(label_copy, "main") == 0 && win && win->web_view) {
if (label_copy && strcmp(label_copy, "main") == 0 && native_sdk_ensure_main_webview(win)) {
free(label_copy);
win->main_webview_layer = layer;
native_sdk_reorder_overlays(win);