diff --git a/spdd/prompt/7-20260516-0719-[Feat]-Swing-Lightweight-Webview-Embedding.md b/spdd/prompt/7-20260516-0719-[Feat]-Swing-Lightweight-Webview-Embedding.md index ffb11fe..1b08764 100644 --- a/spdd/prompt/7-20260516-0719-[Feat]-Swing-Lightweight-Webview-Embedding.md +++ b/spdd/prompt/7-20260516-0719-[Feat]-Swing-Lightweight-Webview-Embedding.md @@ -315,6 +315,26 @@ generated_at: 2026-05-16T07:19:13-07:00 also makes the pump thread the one that creates the default context. WebKit symbols are already resolved at `JNI_OnLoad`, so the call is always safe there. +- **Xlib threads are never switched on late.** Xlib requires + `XInitThreads()` to be the first Xlib call in a process: on + libX11 1.7.x (Ubuntu 22.04) a display — and its resource + database — opened before it has no locks, and once the call is + made Xlib starts locking every display, so the lock-less one is + locked through a NULL mutex. The jDeploy launcher initialises + GTK in-process for its splash before the JVM starts; the pump's + `gtk_init` then reuses that display, and the first cursor WebKit + loads (the I-beam on a text-field focus: Xcursor → `XGetDefault` + → `XrmQGetResource`) kills the process with SIGSEGV. So + `GtkPump::ensure_started` calls `XInitThreads()` only when no + GDK display is open yet in the process — checked with + `gdk_display_get_default()` before `gtk_init`. When the host has + already initialised GTK, the pump leaves Xlib exactly as the + host set it up and reuses the host's display (if the host made + Xlib thread-safe first, as the jDeploy launcher now does, the + display is already locked; if not, it stays lock-less, which is + the state the process already ran in). On libX11 ≥ 1.8 every + display is thread-safe by default and both branches are + equivalent. - **DevTools call is single-shot.** No state added on the Java side. The native `webview_offscreen_open_devtools` returns 0 if `enable_developer_extras` is FALSE on the @@ -451,7 +471,9 @@ generated_at: 2026-05-16T07:19:13-07:00 exported via `Java_..._webview_1offscreen_1execute_1editing_1command`. `embed::GtkPump::ensure_started` holds the never-released reference on the default `WebKitWebContext` (see Approach, - issue #64). + issue #64), and calls `XInitThreads()` only when no GDK display + is open yet (see Approach, "Xlib threads are never switched on + late"). - `demos/WebViewExitSmokeTest/src/ca/weblite/webview/demos/WebViewExitSmokeTest.java` and `run-linux-exit-smoketest.sh` — regression smoke test for issue #64: creates a LIGHTWEIGHT component, loads a `data:` page, @@ -1197,6 +1219,15 @@ Files: messages (e.g. WebKit `load-failed`) still print unconditionally. ## S · Safeguards +- **Never call `XInitThreads()` after a GDK display exists + (never-relax).** `GtkPump::ensure_started` makes the call only + when `gdk_display_get_default()` is NULL before `gtk_init`. + Calling it unconditionally re-introduces the SIGSEGV in + `pthread_mutex_lock` (Xcursor → `XGetDefault`) on libX11 1.7.x + whenever the host process initialised GTK first — every + jDeploy-launched app with a launcher older than the fix. + *Added 2026-10-02, from a defect: Ubuntu 22.04 / libX11 1.7.5 / + WebKitGTK 2.50.4, core dump from the affected VM.* - **The default `WebKitWebContext` is never finalized.** The pump-thread reference taken in `GtkPump::ensure_started` is intentionally leaked — never unref it, and never route views diff --git a/spdd/prompt/8-20260516-0719-[Feat]-Native-Library-Loading-And-Packaging.md b/spdd/prompt/8-20260516-0719-[Feat]-Native-Library-Loading-And-Packaging.md index ddac28e..15064eb 100644 --- a/spdd/prompt/8-20260516-0719-[Feat]-Native-Library-Loading-And-Packaging.md +++ b/spdd/prompt/8-20260516-0719-[Feat]-Native-Library-Loading-And-Packaging.md @@ -498,9 +498,24 @@ Files: `src_c/webkit_loader.h`, `src_c/webkit_loader.cpp`, - On any failure — either library not `dlopen`able, or a required symbol missing — fail with a clear message naming **both** candidate SONAMEs of the library that could not be - satisfied. Runs from `JNI_OnLoad`; returning `JNI_ERR` makes - `System.load` throw with that message (same load-time failure - point as before, now diagnosable on both 4.0 and 4.1 hosts). + satisfied, **each with its own `dlerror()` text** (the 4.1 + attempt's reason is captured before the 4.0 attempt overwrites + it). Without the first reason, a host whose 4.1 library is + installed but fails to load (a missing dependency, a broken + install) reads exactly like a host with no WebKitGTK at all. + The message buffer is large enough (1024 bytes) to hold both + reasons. + - Runs from `JNI_OnLoad`, which returns `JNI_ERR`. The JVM then + throws a **generic** `UnsatisfiedLinkError` ("unsupported JNI + version 0xFFFFFFFF") — it does not carry the loader's message. + So, before returning `JNI_ERR`, the loader both prints the + message to stderr and publishes it to Java as the system + property `ca.weblite.webview.nativeLoadError` (via + `vm->GetEnv` and `System.setProperty`; any JNI failure while + publishing is cleared and ignored, so publishing can never + change the outcome). On a successful load the property is never + set. This is the one way an embedder can tell the user *why* + the WebView is unavailable rather than only *that* it is. 3. Call-site routing (`webkit_shim.h`): included by `webview.h` and `webview_embed.cpp` immediately after the WebKit/JSC/GTK headers, @@ -749,8 +764,12 @@ Files: `pom.xml`, `.github/workflows/maven-release.yml` neither `libwebkit2gtk-4.1.so.0` nor `libwebkit2gtk-4.0.so.37` (respectively the two JavaScriptCore SONAMEs) can be `dlopen`ed, or a required symbol is missing, `JNI_OnLoad` returns `JNI_ERR` - and `System.load` throws `UnsatisfiedLinkError` with a message - naming both candidate SONAMEs. This replaces the old + and `System.load` throws `UnsatisfiedLinkError`. The loader's own + message — naming both candidate SONAMEs, each with its own + `dlerror()` reason — goes to stderr and to the system property + `ca.weblite.webview.nativeLoadError`, because the JVM's exception + text is generic. The property name is public contract: embedders + read it to explain the failure, so it is never renamed. This replaces the old dynamic-linker error — which named only the 4.1 SONAME and made a 4.0 host look broken — with a message that makes the missing-runtime case obvious on either host. diff --git a/src_c/webkit_loader.cpp b/src_c/webkit_loader.cpp index b0367f4..1f19a7e 100644 --- a/src_c/webkit_loader.cpp +++ b/src_c/webkit_loader.cpp @@ -29,25 +29,35 @@ const char *const kJsc40 = "libjavascriptcoregtk-4.0.so.18"; void *g_webkit_handle = nullptr; void *g_jsc_handle = nullptr; -char g_err[512] = {0}; +char g_err[1024] = {0}; // Resolve everything exactly once. Returns true on success; on failure writes // a diagnostic to g_err and returns false. Called only through the C++11 // thread-safe function-local static in webkit_loader_ensure(). bool do_init() { + // dlerror() only reports the most recent failure, so each attempt's reason + // is copied out before the next dlopen overwrites it. Without the first + // reason, a 4.1 library that is installed but fails to load reads exactly + // like a host with no WebKitGTK at all. + char first_err[384] = {0}; + const char *e = nullptr; + // 1. WebKit: prefer 4.1, fall back to 4.0. bool is_41 = true; g_webkit_handle = dlopen(kWebKit41, RTLD_NOW | RTLD_GLOBAL); if (!g_webkit_handle) { + e = dlerror(); + std::snprintf(first_err, sizeof(first_err), "%s", e ? e : "unknown error"); g_webkit_handle = dlopen(kWebKit40, RTLD_NOW | RTLD_GLOBAL); is_41 = false; } if (!g_webkit_handle) { + e = dlerror(); std::snprintf(g_err, sizeof(g_err), "swingwebview: unable to load the WebKitGTK runtime — tried " - "'%s' then '%s' (%s). Install libwebkit2gtk-4.1-0 (Ubuntu " - "22.04+) or libwebkit2gtk-4.0-37 (Ubuntu 20.04).", - kWebKit41, kWebKit40, dlerror()); + "'%s' (%s) then '%s' (%s). Install libwebkit2gtk-4.1-0 " + "(Ubuntu 22.04+) or libwebkit2gtk-4.0-37 (Ubuntu 20.04).", + kWebKit41, first_err, kWebKit40, e ? e : "unknown error"); return false; } @@ -56,13 +66,17 @@ bool do_init() { const char *jsc_secondary = is_41 ? kJsc40 : kJsc41; g_jsc_handle = dlopen(jsc_primary, RTLD_NOW | RTLD_GLOBAL); if (!g_jsc_handle) { + e = dlerror(); + std::snprintf(first_err, sizeof(first_err), "%s", e ? e : "unknown error"); g_jsc_handle = dlopen(jsc_secondary, RTLD_NOW | RTLD_GLOBAL); } if (!g_jsc_handle) { + e = dlerror(); std::snprintf(g_err, sizeof(g_err), "swingwebview: unable to load the JavaScriptCore runtime — " - "tried '%s' then '%s' (%s).", - kJsc41, kJsc40, dlerror()); + "tried '%s' (%s) then '%s' (%s).", + jsc_primary, first_err, jsc_secondary, + e ? e : "unknown error"); return false; } @@ -114,14 +128,52 @@ bool webkit_loader_ensure(char *errbuf, size_t errlen) { return ok; } -// Resolve at library load time. Returning JNI_ERR makes System.load throw an -// UnsatisfiedLinkError carrying our diagnostic — the same load-time failure -// point as the old hard DT_NEEDED, but now naming both candidate SONAMEs. +namespace { + +// Publish the loader's diagnostic as the system property +// ca.weblite.webview.nativeLoadError. Returning JNI_ERR makes the JVM throw a +// generic UnsatisfiedLinkError ("unsupported JNI version 0xFFFFFFFF") that +// does not carry our message, so this property is how an embedder learns WHY +// the WebView is unavailable. Best effort: any JNI failure here is cleared and +// ignored, so publishing can never change the outcome of the load. +void publish_load_error(JavaVM *vm, const char *msg) { + JNIEnv *env = nullptr; + if (!vm || vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != + JNI_OK || !env) { + return; + } + jclass system = env->FindClass("java/lang/System"); + jmethodID set_property = + system ? env->GetStaticMethodID( + system, "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;") + : nullptr; + jstring key = + set_property ? env->NewStringUTF("ca.weblite.webview.nativeLoadError") + : nullptr; + jstring value = key ? env->NewStringUTF(msg) : nullptr; + if (value) { + jobject previous = + env->CallStaticObjectMethod(system, set_property, key, value); + if (previous) env->DeleteLocalRef(previous); + } + if (env->ExceptionCheck()) env->ExceptionClear(); + if (value) env->DeleteLocalRef(value); + if (key) env->DeleteLocalRef(key); + if (system) env->DeleteLocalRef(system); +} + +} // namespace + +// Resolve at library load time. Returning JNI_ERR makes System.load fail at +// the same load-time point as the old hard DT_NEEDED. The JVM's exception text +// is generic, so the diagnostic — both candidate SONAMEs, each with its own +// dlerror() reason — goes to stderr and to ca.weblite.webview.nativeLoadError. extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) { - (void)vm; - char err[512]; + char err[1024]; if (!webkit_loader_ensure(err, sizeof(err))) { std::fprintf(stderr, "%s\n", err); + publish_load_error(vm, err); return JNI_ERR; } return JNI_VERSION_1_6; diff --git a/src_c/webkit_loader.h b/src_c/webkit_loader.h index 6be224b..e687ee7 100644 --- a/src_c/webkit_loader.h +++ b/src_c/webkit_loader.h @@ -176,8 +176,10 @@ extern WkFns g_wk; // Resolve the WebKit/JSC runtime once (idempotent, thread-safe). Returns true // on success. On failure, if errbuf/errlen are non-null, copies a diagnostic -// message naming the candidate SONAMEs. Invoked from JNI_OnLoad; callers do -// not normally need it directly. +// message naming the candidate SONAMEs, each with its own dlerror() reason +// (pass a buffer of at least 1024 bytes). Invoked from JNI_OnLoad, which also +// publishes the message as the system property +// ca.weblite.webview.nativeLoadError; callers do not normally need it directly. bool webkit_loader_ensure(char *errbuf, size_t errlen); #endif // WEBVIEW_GTK || __linux__ diff --git a/src_c/webview_embed.cpp b/src_c/webview_embed.cpp index 141578e..23449d2 100644 --- a/src_c/webview_embed.cpp +++ b/src_c/webview_embed.cpp @@ -526,13 +526,27 @@ class GtkPump { std::condition_variable ready_cv; bool ready = false; thread = std::thread([&] { - // X11 must be told we're going to use it from multiple threads. - XInitThreads(); // Embedding via XReparentWindow requires a real X11 GdkDisplay // on both sides. Force the X11 backend in case GTK would // otherwise pick Wayland or some other backend (e.g. in a // Parallels / virtual desktop session that exposes both). + // Must precede the first gdk_display_manager_get(), which the + // gdk_display_get_default() check below performs; it makes no + // Xlib call. gdk_set_allowed_backends("x11"); + // X11 must be told we're going to use it from multiple threads -- + // but only if no display exists yet (Canvas 7, "Xlib threads are + // never switched on late"). Xlib requires XInitThreads() to be + // the first Xlib call: on libX11 1.7.x a display opened before it + // has no locks, and calling it afterwards makes Xlib lock that + // display through a NULL mutex. The jDeploy launcher initialises + // GTK for its splash before the JVM starts, and gtk_init below + // would reuse that display -- the first cursor WebKit then loads + // (Xcursor -> XGetDefault) crashes the process. When the host + // already opened a GDK display, leave Xlib as the host set it up. + if (gdk_display_get_default() == nullptr) { + XInitThreads(); + } // Use the simple input-method module rather than the system // default (ibus / fcitx / etc.). On the offscreen embed // path the system IMs were observed to commit special-key