Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions .github/scripts/linux-canvas-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) \
Expand All @@ -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)"
Expand All @@ -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
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 84 additions & 20 deletions src/platform/linux/gtk_host.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Loading