diff --git a/.gitignore b/.gitignore index 2ac3a95..befd31c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,8 @@ /assets/*.pcm /assets/*.ac3 +# Claude Code's per-project MCP server config: machine-local, since it carries +# absolute paths to whatever tooling the checkout happens to sit next to. +/.mcp.json + .DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index a6dc381..28ae18a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,4 +33,10 @@ if (TARGET_WEBOS) add_subdirectory(media/smp) add_subdirectory(media/ndl) add_subdirectory(media/lgnc) + add_subdirectory(web/libcbe) + add_subdirectory(web/cbe) + add_subdirectory(web/cbe-webos3) + add_subdirectory(web/cbe-webos6) + add_subdirectory(web/neva) + add_subdirectory(web/hybrid) endif () diff --git a/README.md b/README.md index 7612638..535b76b 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,22 @@ Small, readable sample apps for LG webOS native homebrew. Each one does a single thing and is meant to be read top to bottom, without a framework in the way. -The media samples feed raw elementary streams straight into the TV's hardware decoder, with -SDL2 owning the window and the remote control. There are two entirely different ways to -reach that decoder, and all of them get samples: **starfish-media-pipeline** -(`libplayerAPIs`), **NDL**, and **LGNC** (the LG NetCast Open API). +Most of them are media samples: they feed raw elementary streams straight into the TV's +hardware decoder, with SDL2 owning the window and the remote control. There are three +entirely different ways to reach that decoder, and all of them get samples: +**starfish-media-pipeline** (`libplayerAPIs`), **NDL**, and **LGNC** (the LG NetCast Open +API). + +The `web/` samples are the odd ones out. They link `libcbe.so` - the TV's own Chromium, the +engine every web app on the box already runs inside - and put a real web view in a *native* +app, with no WAM and no web app package. There is no SDK for that library; the headers were +reconstructed from firmware symbol tables and from the vtables of WAM's own subclasses, and +`web/cbe/README.md` writes down how. `web/hybrid` uses that to solve a real problem: a +native app that needs the user to sign in on somebody else's web page, and needs the token +that comes back in the redirect URL, without shipping a browser to do it. It runs SDL2 with +a Nuklear UI and the web view in one process - `WebOSMain()` never returns, so SDL is pumped +from Chromium's message loop rather than its own - and reads the answer straight out of the +navigation. ## What is here @@ -21,6 +33,13 @@ media/ esplayer/ libndl-directmedia2, NDL_Esplayer* - webOS 2.x to 3.4 directmedia/ libNDL_directmedia, NDL_Direct* - webOS 3.5+, built for API v1 and v2 lgnc/ liblgncopenapi, LGNC_DIRECT* - webOS 1 to 4, one binary for all of them +web/ + libcbe/ reconstructed libcbe headers and the link stub, shared by both + cbe/ the smallest thing that puts a web page on screen + cbe-webos3/ the same, against webOS 3's older and different libcbe + cbe-webos6/ and again for webOS 6+, where the API changes a third time + neva/ the same, through neva_app_runtime - the API LG published + hybrid/ a native sign-in flow: SDL2 + Nuklear, a web login page, one process ``` The same two files play through all three stacks. Comparing the three `main.c` files is the @@ -104,11 +123,12 @@ and skips everything else. ### Icons -`assets/icons/.png` carries white artwork on transparency - the API name, a play -mark, and which variant it is - and no colour at all. Each sample passes a Material 500 colour to `webos_add_ipk`, which +`assets/icons/.png` carries white artwork on transparency - the API name, a mark (a +play triangle for the media samples, a globe for the web ones), and which variant it is - +and no colour at all. Each sample passes a Material 500 colour to `webos_add_ipk`, which writes it to both `iconColor` and `bgColor` in `appinfo.json`, and webOS paints that behind the glyph. Families are grouped by hue: blues for starfish, greens for NDL, orange for -LGNC. +LGNC, blue and purple for the web samples. Both fields are set deliberately: `bgColor` is the tile background, while `iconColor` fills behind the icon itself - without it the launcher's default shows through the glyph's @@ -181,6 +201,11 @@ hard to diagnose from the TV side. | `media/smp/webos5` | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): exported window accepted, full load / play / feed / EOS / unload, 300 video + 470 audio units on both. Those runs predate the `Play()` ordering fix, which all SMP samples share - re-run pending | | `media/ndl/directmedia` (v2) | 5+ | **verified on hardware** - 65UP7560 (webOS 6.5.2) and OLED77C5 (webOS 10.3.1): 300 video + 469 PCM chunks on both | | `media/ndl/directmedia` (v1) | 3.5 - 4.x | built and symbol-verified, needs a 2017-2019 set to test | +| `web/hybrid` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the sign-in flow runs end to end - native panel, web login form, and back with the username read out of the intercepted redirect URL. The OK key is verified too, injected with `com.webos.service.networkinput/test/sendKeyCode`; leaving the web view by remote is not, since keys go to whichever window is up | +| `web/neva` | webOS 6+ | **verified on hardware** - 65UP7560, starfish 6.5.2: page loads and renders full-screen, app takes the foreground. The one API here with published upstream headers, and `-verify` clean to 11.2 | +| `web/cbe-webos6` | webOS 6 | **verified on hardware** - 65UP7560, starfish 6.5.2: the page loads and renders full-screen and the app takes the foreground. A third shape of the API - 61-slot delegate, 92-byte `WebViewBase`, `WebOSMain` as a class | +| `web/cbe-webos3` | webOS 3 | **unfinished, parked** - 43UH6100, starfish 3.4.0: the reconstructed webOS 3 ABI is right and the page loads and paints, with every delegate callback firing, but the window never reaches the screen - LSM keeps the previous app foreground. webOS 3 has no `Activate()` and the equivalent has not been found | +| `web/cbe` | webOS 4.0 | **verified on hardware** - 49LK5900, webOS 4.4.3: the window registers with LSM and SAM as the foreground card, and a display capture shows the page rendered full-screen at 1920x1080. Input and lifecycle are not implemented | | `media/smp/webos1` | 1.x | not written yet - and there is no webOS 1 hardware here to validate it against, so it would ship untestable | ## What a `Play()` that returns true does not tell you diff --git a/assets/icons/web-cbe-webos3.png b/assets/icons/web-cbe-webos3.png new file mode 100644 index 0000000..a15f1c8 Binary files /dev/null and b/assets/icons/web-cbe-webos3.png differ diff --git a/assets/icons/web-cbe-webos6.png b/assets/icons/web-cbe-webos6.png new file mode 100644 index 0000000..f9fc0a3 Binary files /dev/null and b/assets/icons/web-cbe-webos6.png differ diff --git a/assets/icons/web-cbe.png b/assets/icons/web-cbe.png new file mode 100644 index 0000000..517296b Binary files /dev/null and b/assets/icons/web-cbe.png differ diff --git a/assets/icons/web-hybrid.png b/assets/icons/web-hybrid.png new file mode 100644 index 0000000..f5dd61f Binary files /dev/null and b/assets/icons/web-hybrid.png differ diff --git a/assets/icons/web-neva.png b/assets/icons/web-neva.png new file mode 100644 index 0000000..9a55686 Binary files /dev/null and b/assets/icons/web-neva.png differ diff --git a/assets/make-icon.py b/assets/make-icon.py index 6a7320a..8e79604 100644 --- a/assets/make-icon.py +++ b/assets/make-icon.py @@ -5,8 +5,8 @@ each sample sets to its own Material 500 value (the COLOR argument to webos_add_ipk). The PNG carries only white artwork on transparency: - SMP <- which media API - |> <- play mark + SMP <- which API + |> <- play mark, or a globe for the web samples w4 <- which variant of it Colour alone was not enough to tell eight tiles apart, and the two lines say which API and @@ -21,7 +21,7 @@ SIZE = 80 SS = 8 # supersample, for clean edges -# target name (minus the "media-" prefix) -> (API, variant) +# target name (minus the "media-" prefix) -> (API, variant[, mark]) ICONS = { "smp-acb-webos2": ("SMP", "w2"), "smp-acb-webos3": ("SMP", "w3"), @@ -31,6 +31,11 @@ "ndl-directmedia-v1": ("NDL", "DM1"), "ndl-directmedia-v2": ("NDL", "DM2"), "lgnc": ("LGNC", "1-4"), + "web-cbe": ("CBE", "w4", "globe"), + "web-cbe-webos3": ("CBE", "w3", "globe"), + "web-cbe-webos6": ("CBE", "w6+", "globe"), + "web-neva": ("NEVA", "w6+", "globe"), + "web-hybrid": ("SDL+", "w4", "globe"), } @@ -40,7 +45,26 @@ def centred(d, text, font, cy, n): text, font=font, fill=(255, 255, 255, 255)) -def make(path, api, variant): +def play_mark(d, cx, cy, h): + w = h * 0.87 + d.polygon([(cx - w / 2, cy - h / 2), (cx - w / 2, cy + h / 2), (cx + w / 2, cy)], + fill=(255, 255, 255, 255)) + + +def globe_mark(d, cx, cy, h): + """A ring with one meridian and one parallel - enough to read as "web" at 80px.""" + r = h / 2 + t = max(2, int(h * 0.075)) + d.ellipse([cx - r, cy - r, cx + r, cy + r], outline=(255, 255, 255, 255), width=t) + d.ellipse([cx - r * 0.45, cy - r, cx + r * 0.45, cy + r], + outline=(255, 255, 255, 255), width=t) + d.line([cx - r, cy, cx + r, cy], fill=(255, 255, 255, 255), width=t) + + +MARKS = {"play": play_mark, "globe": globe_mark} + + +def make(path, api, variant, mark="play"): n = SIZE * SS img = Image.new("RGBA", (n, n), (0, 0, 0, 0)) d = ImageDraw.Draw(img) @@ -49,11 +73,7 @@ def make(path, api, variant): centred(d, api, ImageFont.truetype(FONT, int(n * (0.19 if len(api) > 3 else 0.23))), n * 0.16, n) - cx, cy = n * 0.53, n * 0.50 - h = n * 0.30 - w = h * 0.87 - d.polygon([(cx - w / 2, cy - h / 2), (cx - w / 2, cy + h / 2), (cx + w / 2, cy)], - fill=(255, 255, 255, 255)) + MARKS[mark](d, n * 0.53 if mark == "play" else n * 0.50, n * 0.50, n * 0.30) centred(d, variant, ImageFont.truetype(FONT, int(n * 0.21)), n * 0.85, n) @@ -64,7 +84,7 @@ def make(path, api, variant): here = os.path.dirname(os.path.abspath(__file__)) out_dir = os.path.join(here, "icons") os.makedirs(out_dir, exist_ok=True) - for name, (api, variant) in ICONS.items(): + for name, spec in ICONS.items(): out = os.path.join(out_dir, f"{name}.png") - make(out, api, variant) - print(f"{out} {api}/{variant}") + make(out, *spec) + print(f"{out} {spec[0]}/{spec[1]}") diff --git a/cmake/appinfo.json.in b/cmake/appinfo.json.in index 41a686d..e8b0145 100644 --- a/cmake/appinfo.json.in +++ b/cmake/appinfo.json.in @@ -7,5 +7,6 @@ "title": "@IPK_TITLE@", "icon": "icon.png", "iconColor": "@IPK_COLOR@", + "noSplashOnLaunch": true, "bgColor": "@IPK_COLOR@" } diff --git a/web/cbe-webos3/CMakeLists.txt b/web/cbe-webos3/CMakeLists.txt new file mode 100644 index 0000000..0abaf28 --- /dev/null +++ b/web/cbe-webos3/CMakeLists.txt @@ -0,0 +1,33 @@ +# web/cbe, built against webOS 3's libcbe. +# +# Not a flag flip. Between webOS 3 and 4 the library changed its string ABI *and* +# its API: WebViewBase takes its size in the constructor with no Initialize(), +# WebAppWindowBase has no InitWindow(), and the delegate has a different slot +# order with LoadStarted carrying a URL where webOS 4 grew a separate +# DidStartNavigation. So this is a sibling sample rather than a build variant, +# the way media/smp/acb and media/smp/webos5 are siblings. +# +# Verified against a 43UH6100 running starfish 3.4.0. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +add_executable(web-cbe-webos3 main.cpp luna_register.c) +target_include_directories(web-cbe-webos3 PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_include_directories(web-cbe-webos3 SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) +target_link_directories(web-cbe-webos3 PRIVATE ${GLIB_LIBRARY_DIRS}) +# cbe-stub-webos3 carries the include path, -fno-rtti and the old string ABI. +target_link_libraries(web-cbe-webos3 PRIVATE cbe-stub-webos3 ${GLIB_LIBRARIES} ${CMAKE_DL_LIBS}) +target_compile_definitions(web-cbe-webos3 PRIVATE APP_LOG_NAME="web-cbe-webos3") + +set_target_properties(web-cbe-webos3 PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-cbe-webos3 + APPID org.webosbrew.sample.web.cbe3 + TITLE "CBE WebView (webOS 3)" + COLOR "#26A69A" + WEBOS_VERSIONS ">=3.4, <4") diff --git a/web/cbe-webos3/README.md b/web/cbe-webos3/README.md new file mode 100644 index 0000000..655f533 --- /dev/null +++ b/web/cbe-webos3/README.md @@ -0,0 +1,412 @@ +# web/cbe, on webOS 3 + +> **Status: unfinished, and parked.** The reconstructed ABI is correct and demonstrated - +> the page loads and every delegate callback fires - but the window never reaches the +> screen. The failure is localised (see "Where the gap actually is") and the dead ends are +> written down so nobody repeats them. Picking this up again means interactive Ghidra work +> on `weboswayland::WaylandDisplay`, not more black-box probing. +> +> If you want a *working* embedded web view, use `web/cbe` on webOS 4, or the +> `neva_app_runtime` API on webOS 6 and newer - the latter has public upstream headers and +> needs no reverse engineering at all. + +The same twenty lines of "make a window, make a web view, load a URL" as `web/cbe`, against +the libcbe that shipped on webOS 3. Everything that differs is the library moving underneath +it, and there is more of that than the version numbers suggest. + +Measured on a 43UH6100 running **starfish 3.4.0**. + +## The API is different, not just the ABI + +| | webOS 3 | webOS 4 | +|---|---|---| +| `std::string` | pre-C++11 (`RKSs`) | `__cxx11` | +| construction | `WebViewBase(int w, int h)` | `WebViewBase()` then `Initialize(...)` | +| window size | no `InitWindow()`; `Resize()` | `InitWindow(w, h)` | +| delegate slots | 24, own order | 24, different order | +| navigation start | `LoadStarted(url)` | `DidStartNavigation(url, bool)` | +| browser control | slots 18/19 | slots 19/20 | +| absent here | `Initialize`, `UpdatePreferences`, `SetTrustLevel`, `Activate` | - | + +The delegate came out of webOS 3's own `libWebAppMgr.so`, the same way the webOS 4 one did: +`BlinkWebView`'s vtable is the only authority for slot order. `DidFirstNonBlankPaint` at slot +2 exists only on this generation, and `LoadProgressChanged` takes a URL besides the progress. + +`-verify` earns its keep here: `SetTrustLevel` and `UpdatePreferences` were copied over from +the webOS 4 sample, build fine, and are simply absent on webOS 3. + +## Two things that stop it starting + +**`--ozone-platform=weboswayland`**, not `wayland`. The wrong name gets as far as +`DesktopFactoryWayland` and then aborts; the right one logs `Ozone: WebOSFactoryWayland`. + +**`CDM_LIB_PATH` must be set.** webOS 3's `WebOSMain` does + +```cpp +std::string cdm(getenv("CDM_LIB_PATH")); // no null check +cdm += "/libwidevinecdmadapter.so"; +``` + +so an unset variable aborts the process before any of your code runs, with + +``` +terminate called after throwing an instance of 'std::logic_error' + what(): basic_string::_S_construct null not valid +``` + +and nothing else to go on. WAM inherits the variable from its own environment; a plain +native app does not, so the sample sets it to `/usr/lib`. That was found by pulling the +64 MB library, resolving the crash address to `WebOSMain+0x21f8`, and reading the literal the +`getenv` call loads - there was no other way to see it. + +## State: loads and paints, but does not reach the screen + +Verified working: the process starts, the page loads, and every delegate callback fires with +the recovered signatures - + +``` +[cbe] title 'Example Domain' +[cbe] document loaded +[cbe] first non-blank paint +[cbe] progress 100% +[cbe] finished example.com/ +``` + +so the reconstructed ABI is right: the vtable slots line up, the old-ABI strings arrive +intact, and the web view is rendering. + +**What does not work is compositing.** LSM keeps the previous app foreground, and with the +splash disabled the screen shows the TV's own no-signal wallpaper - so the surface is not +merely behind something, it is not there. + +The search so far, all of it negative, and worth writing down so it is not repeated: + +| tried | result | +|---|---| +| `SetWindowProperty("appId", ...)` | no effect - and it is the only property WAM sets besides the key-access ones | +| `SetWindowHostState(FULLSCREEN)`, before and after `Show()` | no effect | +| `SetHiddenState(false)`, `SetOpacity(1.0f)` after `Show()` | no effect | +| `--app-id=` (the switch exists in libcbe) | no effect | +| `webos::Platform::Get()` then `SetFullscreen(true)` | returns **nil** - that singleton is not constructed in a plain embedder | +| dropping `Resize()`, which WAM never calls here | **worse**: the delegate stops firing and Wayland reports `proxy already has listener` | + +Two things were learned rather than guessed. `noSplashOnLaunch` in `appinfo.json` matters: +without it SAM's launch splash covers the screen indefinitely and hides what is really +happening, which is what made this look like a compositing bug with a picture on top of it. +And `WebAppWaylandWindow::show()` in webOS 3's WAM turns out to be nothing but +`onStageActivated()` - pure WAM bookkeeping, no libcbe calls - followed by +`WebAppWindowBase::Show()`, so WAM is not doing anything special that the sample omits. + +### What the window itself reports + +The clearest symptom, from the diagnostics the sample prints: + +``` +[diag] before Resize: display=1920x1080 native=0x690b0 state=0 +[diag] after Resize: display=1920x1080 native=0x690b0 state=0 +[diag] after Show: native=0x690b0 state=0 +``` + +The window object is real - a non-null native handle and the right panel size - but +`GetWindowHostState()` stays `0` (`NATIVE_WINDOW_DEFAULT`) through +`SetWindowHostState(NATIVE_WINDOW_FULLSCREEN)` and through `Show()`. The compositor never +acknowledges the state, which is a better description of the failure than "the window does +not appear": the surface exists and is being drawn into, and LSM is simply not treating it +as an app window. + +The enum is not the problem - WAM passes literal `3` for fullscreen, matching +`NATIVE_WINDOW_FULLSCREEN` here - and neither is the call sequence. Disassembling +`WebAppWayland::raise()`, webOS 3's equivalent of webOS 4's `Activate()`, shows it makes +exactly one libcbe call, `SetWindowHostState(3)`, which the sample already does. + +Two more switches turn out to be load-bearing rather than decorative: **`--webos-wam` is +required** - without it the process exits before writing a line of log - while `--app-id`, +which also exists in the library, changes nothing either way. + +The app does reach the Luna bus: `ls-monitor -l` shows two client-only connections owned by +the executable, without a service name. So libcbe's own LS2 client is running. + +### There *is* a standalone embedder on webOS 3 + +An earlier version of this file said there was not, on the strength of scanning `/usr/bin` +and `/usr/palm/applications`. That was wrong, and the way to find it is to ask SAM what is +running rather than to search the filesystem: + +```sh +luna-send -n 1 -f luna://com.webos.applicationManager/running '{}' +``` + +`com.webos.app.browser` is there, `appType: native_builtin`, with a live pid - and +`/proc//exe` points at +`/mnt/otncabi/usr/palm/applications/com.webos.app.browser/chrome`, which does link libcbe. +It is under `/mnt/otncabi`, which is why the earlier search missed it. + +Its command line is the reference this sample has been missing, and two switches in it +contradict what was being used here: + +``` +--ozone-platform=wayland (not weboswayland) +--webos-launch-json={"@system_native_app":true,"preload":"partial", + "nid":"com.webos.app.browser","launchHidden":true} +--in-process-gpu --ignore-gpu-blacklist --gpu-no-context-lost +--disable-gpu-watchdog --enable-accelerated-compositing +--set-maximized --window-size=1920,1080 +``` + +There is no `--webos-wam` at all. `weboswayland` is WAM's backend, and a non-WAM app is +evidently expected to use plain `wayland` and identify itself through +`--webos-launch-json`, whose `nid` is the app id. + +Adopting it does not work yet, and bisecting says exactly which part is fatal: with +everything else from the browser adopted - the launch-json handling below, +`CHROMIUM_BROWSER=yes`, `BROWSER_NAME=Chromium38`, its GPU switches - the sample still runs +fine on `weboswayland`, and switching that one flag to `--ozone-platform=wayland` kills it +before Chromium writes a single log line. The log is zero bytes and the crash lands under +`__vsnprintf_chk` inside libcbe. + +So `--ozone-platform=wayland` is the blocker, on its own, and the jail is not it: this +sample is jailed too, under `/var/palm/jail/org.webosbrew.sample.web.cbe3`. + +#### Why `wayland` cannot work here: two platforms, two window APIs + +Stepping through the startup one `puts()` at a time puts the crash in +`new SampleWindow()` - the `webos::WebAppWindowBase` constructor - and it is not timing: +delaying window creation by three seconds crashes identically. + +That is the whole answer, and the rest of the evidence lines up behind it: + +* webOS 3's libcbe contains **two** ozone platforms, `ozonewayland` (registered as + `wayland`) and `weboswayland`. webOS 4's contains only `ozonewayland`. +* On the TV, `weboswayland` is passed by exactly one process, `/usr/bin/WebAppMgr`. The + native browser passes `wayland`. +* The browser's binary does not reference `WebAppWindowBase` **at all** - it builds its UI + from `Browser::Init(content::BrowserContext*, aura::Window*)` and the Views stack. + +So the two are a matched pair. `WebAppWindowBase` is WAM's windowing API and belongs to +`weboswayland`; `wayland` is for Views-based apps like the browser, which use a different +and much larger API. Constructing a `WebAppWindowBase` under `wayland` segfaults inside +libcbe before the constructor returns, because that object has no backend there. + +That also explains why `web/cbe` works on webOS 4 with the identical code: LG collapsed the +two platforms into one by then, so `wayland` and `WebAppWindowBase` are the same world. On +webOS 3 they are not, and a standalone embedder has to pick a side: + +* **WAM's side** (`weboswayland` + `WebAppWindowBase`) - what this sample does. Everything + works except the browser-side compositor ever producing a frame. +* **The browser's side** (`wayland` + Views) - a working standalone embedder exists, but it + is a different API surface entirely, and none of the reconstructed headers here apply + to it. + +#### How far the `wayland` backend gets before that + +Two of the browser's remaining differences turn out to be required, and finding the first +one needed the crash report rather than the log - because the log was the casualty. + +**`FONTCONFIG_PATH` and `FONTCONFIG_FILE` must be set.** Without them the process dies with +a zero-byte log, and the backtrace lands in `__vsnprintf_chk` called from libcbe. Reading +the disassembly at that address shows a varargs logging helper: an `__snprintf_chk` for the +prefix, then `__vsnprintf_chk` for the message. **libcbe crashes inside its own logger**, +which is why nothing is ever written - whatever it was trying to report is lost with it. +Pointing the two variables at the system `/etc/fonts` is enough; the browser points them at +its own bundled copy. + +With those set, the `wayland` backend gets much further - through Ozone init, SAM +registration and the Luna lifecycle subscription - before dying again: + +``` +[0824/183136:INFO:desktop_factory_wayland.cc(17)] Ozone: DesktopFactoryWayland +... +[luna] registerNativeApp(org.webosbrew.sample.web.cbe3) -> 0 +[0100/000000:ERROR:zygote_linux.cc(622)] write: Broken pipe +``` + +That last line is a forked child noticing its parent has gone. The parent's own crash is a +virtual call through a garbage vtable pointer, in what the disassembly shows to be a +set-delegate helper - store the pointer at `this+64`, then immediately call slot 10 on it: + +```asm +str r1, [r0, #64] ; this->delegate = arg +cbz r1, done +ldr r2, [r1] ; r2 = arg->vptr +ldr r2, [r2, #40] ; <- SIGSEGV, vtable slot 10 +blx r2 +``` + +So the `wayland` path wants a delegate that a WAM-shaped embedder never has to supply. It +is not this sample's reconstructed vtables at fault: on `weboswayland` every delegate +callback arrives correctly. + +`--no-zygote` is also required - removing it, as the browser does, regresses to a zero-byte +log again. + +Two pieces of the browser's setup were adopted and kept, because they are right regardless: + +* **SAM hands a native app its launch parameters as a bare JSON argument**, and the browser + turns that into `--webos-launch-json=` rather than forwarding it - libcbe would otherwise + see `{"nid":...}` where it expects a URL. The sample now does the same. +* `CHROMIUM_BROWSER=yes` and `BROWSER_NAME=Chromium38`, which the browser has in its + environment and a plain native app does not. + +### Where the gap actually is + +Everything observable on both sides of the comparison now matches, and the list is worth +having so nobody re-checks it: + +| | this sample | WAM | +|---|---|---| +| Wayland requests sent | identical but for `xinput_extension.register_input` | | +| threads | `Chrome_InProcGp`, `WaylandDisplayP`, and the rest | same set | +| environment | identical after adding `CHROMIUM_BROWSER`, `BROWSER_NAME`, fontconfig | | +| SAM registration | `registered`, listed in `/running` | | +| window object | 1920x1080, native pointer, handle 1 | | +| web contents | non-null, attached | | +| delegate callbacks | all fire, including `DidFirstNonBlankPaint` | | + +That last row is the one that localises it. **`DidFirstNonBlankPaint` fires, so the renderer +is painting.** What never happens is the browser-side compositor turning that into a frame: +`CreateAcceleratedSurface` is never reached, so there is no EGL surface, so no buffer, so no +`wl_surface.attach`. + +So the gap is between "renderer has painted" and "browser compositor asks the GPU for an +output surface for widget 1". Two libcbe stubs sit near that path and are logged every run: + +``` +ERROR:webos_view.cc(102)] Not implemented ... content::WebContents* WebOSView::GetActiveWebContents() const +ERROR:display.cc(305)] Not implemented ... weboswayland::WaylandDisplay::SetWidgetState(... SHOW ...) +``` + +`GetActiveWebContents()` returning nothing from the view that is supposed to host the +contents looked like the answer, and it is worth writing down why it is not - along with +the technique, because that is the reusable part. + +#### Finding a stub's callers in a stripped 65 MB binary + +libcbe has no `.symtab`, and these functions are local, so the only handle is the +`NOTIMPLEMENTED` string. ARM Thumb reaches it PC-relatively, as a literal `V` plus an +`add rX, pc` at address `P`, where `V = target - (P + 4)`. So scan `.text` for words whose +implied `P` lands within a few KB *and* decodes as `add rX, pc` (`0x4478`-`0x447f`): + +```python +V = struct.unpack_from('vptr +ldr r3, [r3, #356] ; slot 89 +blx r3 +``` + +So libcbe wants to call one method on the active `WebContents` and skips it because the +accessor is a stub. **We hold that pointer** - `WebViewBase::GetWebContents()` returns it - +so the call libcbe skipped can be made by hand through the same vtable slot. Doing that +runs cleanly and changes nothing: no window, host state still 0. Whatever slot 89 is, it is +not what starts compositing, and the stub is a red herring. + +Chromium's own verbose logging is no help: `--v=1` adds nothing, because `VLOG` is compiled +out of this build. + +**So can the WAM side ever show a window?** On the evidence, yes: WAM produces buffers on +exactly this backend, through exactly this API, in the browser process, on the same TV. What +has not been found is what triggers the first frame. LSM's `state_changed` arrives only +*after* a buffer - the working SDL sample shows `attach`, `damage`, `commit`, and only then +`state_changed(3)` and `exposed` - so the host state reading back 0 is a consequence of +having no buffer, not a cause. + +**Where that leaves it.** The sample stays on `weboswayland`, which is the only backend its +API exists on. Getting a window there means finding what makes the browser-side compositor +ask the GPU for an output surface; getting one the browser's way means reconstructing +`Browser`, `content::BrowserContext` and the Views classes instead, which is a much larger +job than the embedding API this directory is about. + +### Registering with SAM: necessary, and still not sufficient + +A native webOS app has to tell SAM it is running. libcbe does not do it - it opens its own +Luna connections but never registers the app - and on webOS 3 nothing else will either: WAM's +binary registers *itself* as `com.palm.webappmanager` before handing over to `WebOSMain`. + +SDL-webOS does it in `SDL_webOSRegisterApp()`, and `luna_register.c` here is the same call +with the same library, minus SDL: + +```c +HLunaServiceCall("luna://com.webos.applicationManager/registerNativeApp", + "{\"id\":\"org.webosbrew.sample.web.cbe3\"}", &ctx); +``` + +`libhelpers.so.2` is already on the TV, so it is `dlopen`ed rather than linked - one function +is not worth a NEEDED entry and something for `-verify` to check. `ctx.multiple = 1` keeps +the subscription open, which is how relaunch and close events arrive later. Version 1 of the +native lifecycle interface is `registerNativeApp`; version 2 would be `registerApp`, and the +sample's appinfo declares neither, which means 1. + +It works: + +``` +[luna] lifecycle: {"message":"registered","returnValue":true} +[luna] registerNativeApp(org.webosbrew.sample.web.cbe3) -> 0 +``` + +and the window still does not appear. So registration is a thing this sample was missing and +should have been doing, and it is not what the compositor is waiting for. + +### The live lead is registration. libcbe contains +`palm://com.webos.applicationManager/registerNativeApp` and a `webos::LunaServices` class +whose `Initialize(const base::FilePath&)` is an instance method needing a +`webos::LunaServices(webos::Platform*)` - and `Platform` is the browser application's layer, +built by `ChromeMain` rather than by `WebOSMain`. So on this generation the Luna +registration that a native app needs may simply live on the browser's side of the library +and not the embedder's. That is a hypothesis, not a finding. + +### A conclusion worth considering + +Both in-firmware users of libcbe on webOS 3 bring their own window management: WAM wraps +`WebOSMain` in `WebAppWayland`, and the browser does not use `WebOSMain` at all - it uses +`ChromeMain`, which is what constructs `webos::Platform` and its Luna side. There may +therefore be no supported standalone-embedder path on this generation, and `WebOSMain` alone +may be expected to yield a rendering web view whose *window* somebody else owns. webOS 4, +where the same sample works unchanged, would then be the generation that fixed it. + +That is a hypothesis with four pieces of evidence behind it - `Platform::Get()` returning +nil, `ChromeMain` existing beside `WebOSMain`, `--webos-wam` being mandatory, and WAM being +the only binary on the TV that links the library at all - and it should be tested rather +than believed. + +### Chasing `PlatformDelegate`, and what it cost + +`webos::PlatformDelegate`'s vtable is exported, so its *shape* is recoverable even though +nothing in the firmware implements it: two destructor slots, then nine slots that are all +`__cxa_pure_virtual` in the base. Eleven virtuals, no names, no signatures. + +A stub delegate with that shape is enough for `Runtime::Initialize(PlatformDelegate*)` to +accept it, and both it and `InitializePlatform(base::FilePath)` then return cleanly. Neither +changes anything: the host state still reads back 0 and LSM still shows the previous app. +The delegate is never called during startup, so the unknown signatures never come up - which +also means initialising it is not what the window is waiting for. + +One more idea, also dead: asserting `Show()` and `SetWindowHostState(FULLSCREEN)` from +`DidFirstNonBlankPaint()`, on the theory that the compositor might ignore a state set on a +surface that has never committed a buffer. It does not - `host-state=0` after the first +frame too. + +### `webos::Runtime` is worth knowing about either way. Unlike `Platform`, **its singleton is +alive in a plain embedder** - `Runtime::Get()` returns a real pointer - and it carries +`SetWindowSize()`, `InitializePlatform(const base::FilePath&)` and +`Initialize(webos::PlatformDelegate*)`. `SetWindowSize` and `InitializePlatform` were both +called successfully and changed nothing, so the remaining candidate on that path is +`Initialize(PlatformDelegate*)`, which needs a delegate whose interface has not been +reconstructed. `base::FilePath` is declarable, for what it is worth: libcbe exports its +`std::string` constructor and destructor, and its layout is that one member. + +So this is honest work-in-progress. The hard half - the ABI - is done and demonstrated, and +the failure is now located precisely: no frame is ever committed to the window's Wayland +surface. The next person should start there - comparing this log against the same capture +from `web/cbe` on a webOS 4 set, where the identical code does present - rather than at the +windowing API, which the protocol trace shows is being driven correctly. diff --git a/web/cbe-webos3/luna_register.c b/web/cbe-webos3/luna_register.c new file mode 100644 index 0000000..aea79f9 --- /dev/null +++ b/web/cbe-webos3/luna_register.c @@ -0,0 +1,80 @@ +// Registering the app with SAM, which is what a native webOS app has to do +// before the system treats it as running. +// +// libcbe does not do this for you: it opens its own Luna connections but never +// registers the app, and on webOS 3 nothing else will do it either - WAM's +// binary registers *itself* as com.palm.webappmanager before handing over to +// WebOSMain. SDL does it in SDL_webOSRegisterApp(), and this is the same call +// with the same library, minus SDL. +// +// dlopen rather than a link stub: libhelpers is one function here, and keeping +// it out of the ELF's NEEDED list means `-verify` has nothing new to check. + +#include "luna_register.h" + +#include +#include +#include + +// From webosbrew's libhelpers, via SDL-webOS's SDL_webos_helpers_sym.h. +typedef struct LSHandle LSHandle; +typedef struct LSMessage LSMessage; +typedef struct HContext HContext; +typedef int (*HLSFilterFunc)(LSHandle *sh, LSMessage *reply, HContext *ctx); + +struct HContext { + HLSFilterFunc callback; + void *userdata; + void *unknown; + int multiple; /* subscription rather than one-shot */ + int pub; /* public or private bus */ + unsigned long ret_token; +}; + +static int (*HLunaServiceCall)(const char *uri, const char *payload, HContext *context); +static const char *(*HLunaServiceMessage)(LSMessage *message); + +static HContext g_lifecycle; + +static int LifecycleCb(LSHandle *sh, LSMessage *reply, HContext *ctx) +{ + (void)sh; + (void)ctx; + if (HLunaServiceMessage != NULL) { + printf("[luna] lifecycle: %s\n", HLunaServiceMessage(reply)); + } + return 1; +} + +int luna_register_app(const char *app_id) +{ + char payload[256]; + void *lib; + int rc; + + lib = dlopen("libhelpers.so.2", RTLD_LAZY); + if (lib == NULL) { + printf("[luna] dlopen libhelpers.so.2: %s\n", dlerror()); + return -1; + } + HLunaServiceCall = dlsym(lib, "HLunaServiceCall"); + HLunaServiceMessage = dlsym(lib, "HLunaServiceMessage"); + if (HLunaServiceCall == NULL) { + printf("[luna] no HLunaServiceCall in libhelpers\n"); + return -1; + } + + snprintf(payload, sizeof(payload), "{\"id\":\"%s\"}", app_id); + + memset(&g_lifecycle, 0, sizeof(g_lifecycle)); + g_lifecycle.callback = LifecycleCb; + g_lifecycle.multiple = 1; /* stays subscribed for relaunch/close events */ + g_lifecycle.pub = 1; + + // nativeLifeCycleInterfaceVersion 1 is registerNativeApp; version 2 would be + // registerApp. The sample's appinfo declares neither, which means 1. + rc = HLunaServiceCall("luna://com.webos.applicationManager/registerNativeApp", + payload, &g_lifecycle); + printf("[luna] registerNativeApp(%s) -> %d\n", app_id, rc); + return rc; +} diff --git a/web/cbe-webos3/luna_register.h b/web/cbe-webos3/luna_register.h new file mode 100644 index 0000000..b7353d7 --- /dev/null +++ b/web/cbe-webos3/luna_register.h @@ -0,0 +1,16 @@ +#ifndef WEB_CBE_WEBOS3_LUNA_REGISTER_H_ +#define WEB_CBE_WEBOS3_LUNA_REGISTER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +// Registers this process with SAM as the running instance of app_id. Returns 0 +// on success. See luna_register.c for why libcbe does not do this itself. +int luna_register_app(const char *app_id); + +#ifdef __cplusplus +} +#endif + +#endif // WEB_CBE_WEBOS3_LUNA_REGISTER_H_ diff --git a/web/cbe-webos3/main.cpp b/web/cbe-webos3/main.cpp new file mode 100644 index 0000000..4a8ede3 --- /dev/null +++ b/web/cbe-webos3/main.cpp @@ -0,0 +1,226 @@ +// The webOS 3 build of web/cbe: the smallest thing that puts a page on screen, +// against the older libcbe. +// +// The shape is identical to web/cbe/main.cpp - hand the process to WebOSMain, +// get called back on the browser UI thread through the default GMainContext, +// build a window and a web view - and every difference is the API moving under +// it between webOS 3 and 4: +// +// * WebViewBase takes its size in the constructor; there is no Initialize(). +// * The window has no InitWindow() either; Resize() gives it a size. +// * The delegate has its own slot order, and LoadStarted carries a URL where +// webOS 4 has a separate DidStartNavigation. +// * All of it is the pre-C++11 std::string ABI. + +#include + +#include +#include +#include +#include +#include +#include + +#include "luna_register.h" +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +extern "C" int WebOSMain(int argc, const char** argv); + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.cbe3"; +const char kUrl[] = "https://example.com/"; + +class SampleWindow; +SampleWindow* g_window; + +class SampleWebView : public webos::WebViewBase { + public: + SampleWebView(int w, int h) : webos::WebViewBase(w, h) {} + + void LoadProgressChanged(double progress, const std::string&) override { + printf("[cbe] progress %3.0f%%\n", progress * 100); + } + void DidFirstFrameFocused() override {} + void DidFirstNonBlankPaint() override { + puts("[cbe] first non-blank paint"); + // Try asserting the window state once a frame actually exists - the + // compositor may ignore it on a surface that has never committed a buffer. + + } + void LoadVisuallyCommitted() override { puts("[cbe] visually committed"); } + void TitleChanged(const std::string& title) override { + printf("[cbe] title '%s'\n", title.c_str()); + } + void NavigationHistoryChanged() override {} + void Close() override { puts("[cbe] close requested"); } + bool DecidePolicyForResponse(bool, int status, const std::string& url, + const std::string&) override { + printf("[cbe] response %d %s\n", status, url.c_str()); + return false; + } + // Where a redirect would be caught on this generation. + void LoadStarted(const std::string& url) override { + printf("[cbe] load started %s\n", url.c_str()); + } + void LoadFinished(const std::string& url) override { + printf("[cbe] finished %s\n", url.c_str()); + } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[cbe] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void LoadStopped(const std::string&) override { puts("[cbe] load stopped"); } + void RenderProcessCreated(int pid) override { printf("[cbe] renderer pid %d\n", pid); } + void RenderProcessGone() override { puts("[cbe] renderer gone"); } + void DocumentLoadFinished() override { puts("[cbe] document loaded"); } +}; + +class SampleWindow : public webos::WebAppWindowBase { + public: + bool event(WebOSEvent*) override { return false; } +}; + +SampleWebView* g_webview; +std::string g_app_path; + +gboolean CreateWebApp(gpointer) { + // Before anything else: tell SAM this process is the app. + luna_register_app(kAppId); + + + + g_window = new SampleWindow(); + // No InitWindow on webOS 3. WAM never calls Resize either, but dropping it + // here stops the delegate firing at all and the Wayland connection starts + // complaining "proxy already has listener", so it is doing something the + // constructor alone does not. + g_window->Resize(1920, 1080); + g_window->SetScaleFactor(1.0f); + g_window->SetOpacity(1.0f); + g_window->SetHiddenState(false); + // LSM ignores a surface with no window type. WAM sets this on every window it + // creates, and it is the one property the earlier attempts were missing - see + // its Wayland trace in README.md. + g_window->SetWindowProperty("_WEBOS_WINDOW_TYPE", "_WEBOS_WINDOW_TYPE_CARD"); + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowProperty("title", "CBE WebView"); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + + g_webview = new SampleWebView(1920, 1080); // no Initialize either + // Neither SetTrustLevel nor UpdatePreferences exists on webOS 3 - `-verify` + // reports them undefined against a 3.4 dump, which is the cheapest way to find + // out that a call you copied from the webOS 4 sample is not portable. + g_webview->SetAppId(kAppId); + g_webview->SetAllowLocalResourceLoad(true); + g_webview->SetLocalStorageEnabled(true); + g_webview->SetVisible(true); + // Chromium does not paint a page it believes is hidden, and nothing sets this + // for us - libcbe leaves the visibility state at its default. + g_webview->SetVisibilityState(webos::WebViewBase::VISIBILITY_VISIBLE); + + // Show() is a no-op on this generation - see README - so the window state is + // set directly. Neither maps the surface; libcbe reaches SetWidgetState(SHOW) + // internally regardless, and that is the stub. + void* contents = g_webview->GetWebContents(); + g_window->AttachWebContents(contents); + + g_window->Show(); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + // Everything here reads back valid except the host state, which stays 0 + // because the compositor never acknowledges a surface that has no buffer. + printf("[cbe] window %dx%d native=%p handle=%u contents=%p host-state=%d\n", + g_window->DisplayWidth(), g_window->DisplayHeight(), + g_window->GetNativeWindow(), g_window->GetWindowHandle(), contents, + (int)g_window->GetWindowHostState()); + + printf("[cbe] loading %s\n", kUrl); + g_webview->LoadUrl(kUrl); + return G_SOURCE_REMOVE; +} + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + if (IsBrowserProcess(argc, argv)) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, NULL, _IOLBF, 0); + + const char* slash = strrchr(argv[0], '/'); + g_app_path = slash ? std::string(argv[0], slash - argv[0]) : std::string("."); + const bool browser = IsBrowserProcess(argc, argv); + + std::vector args; + args.push_back(argv[0]); + if (browser) { + // "weboswayland", not "wayland" - webOS 3's Ozone registers it under that + // name, and the wrong one gets as far as constructing a std::string from a + // null and aborting with basic_string::_S_construct. Taken from WAM's own + // WAM_SWITCHES on the device. + // weboswayland, and it has to be: WebAppWindowBase only exists on that + // backend. Constructing one under --ozone-platform=wayland segfaults inside + // libcbe before the constructor returns. See README - webOS 3 ships two + // ozone platforms and this API belongs to one of them. + args.push_back("--ozone-platform=weboswayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); + args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + // Required: without --webos-wam the process exits before writing a log line. + args.push_back("--webos-wam"); + args.push_back("--noerrdialogs"); + // Page console.log lands in the app's log with this. --v=1 adds nothing: + // verbose logging is compiled out of this build. + args.push_back("--enable-logging=stderr"); + // webOS 3's GPU path needs more setup than webOS 4's. Without these the + // command buffer fails to initialise - "Could not send + // GpuCommandBufferMsg_Initialize". From WAM's own WAM_SWITCHES. + args.push_back("--enable-gpu-rasterization"); + args.push_back("--enable-impl-side-painting"); + args.push_back("--ignore-gpu-blacklist"); + args.push_back("--enable-threaded-compositing"); + args.push_back("--num-raster-threads=2"); + args.push_back("--ui-use-prepare-shader-program"); + args.push_back("--ui-disable-opaque-shader-program"); + args.push_back("--disable-low-res-tiling"); + } + // SAM hands a native app its launch parameters as a bare JSON argument. The + // TV's own browser turns that into --webos-launch-json and does not forward + // the raw form; libcbe would otherwise treat it as a URL. + for (int i = 1; i < argc; ++i) { + if (browser && argv[i][0] == '{') { + args.push_back(std::string("--webos-launch-json=") + argv[i]); + } else { + args.push_back(argv[i]); + } + } + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + // The native browser has these; a plain native app does not. + setenv("CHROMIUM_BROWSER", "yes", 1); + setenv("BROWSER_NAME", "Chromium38", 1); + setenv("FONTCONFIG_PATH", "/etc/fonts", 1); + setenv("FONTCONFIG_FILE", "/etc/fonts/fonts.conf", 1); + + // webOS 3's WebOSMain does std::string(getenv("CDM_LIB_PATH")) with no null + // check and appends "/libwidevinecdmadapter.so" to it, so an unset variable + // aborts the process before anything of ours runs - + // "basic_string::_S_construct null not valid", thrown from inside WebOSMain. + // WAM gets it from its own environment; a plain native app does not. + if (!getenv("CDM_LIB_PATH")) setenv("CDM_LIB_PATH", "/usr/lib", 1); + if (browser) g_idle_add(CreateWebApp, NULL); + + return WebOSMain(static_cast(cargv.size()), cargv.data()); +} diff --git a/web/cbe-webos6/CMakeLists.txt b/web/cbe-webos6/CMakeLists.txt new file mode 100644 index 0000000..5c14522 --- /dev/null +++ b/web/cbe-webos6/CMakeLists.txt @@ -0,0 +1,35 @@ +# web/cbe, built against the libcbe that ships on webOS 6 and newer. +# +# One binary for 6.4 through 11.2: the webos:: API stops moving after 6.4, so +# unlike the webOS 3 and 4 generations this range needs no further variants. +# WAM still subclasses this API on these sets, which is what made the 61-slot +# delegate recoverable - see ../libcbe/webos6/webos/webview_base.h. +# +# Verified against a 65UP7560 running starfish 6.5.2. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +add_library(cbe-stub-webos6 SHARED ../libcbe/webos6/cbe_stub.cpp) +set_target_properties(cbe-stub-webos6 PROPERTIES OUTPUT_NAME cbe + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/stub") +target_include_directories(cbe-stub-webos6 PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../libcbe/webos6") +target_compile_options(cbe-stub-webos6 PUBLIC -fno-rtti) + +add_executable(web-cbe-webos6 main.cpp) +target_include_directories(web-cbe-webos6 SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) +target_link_directories(web-cbe-webos6 PRIVATE ${GLIB_LIBRARY_DIRS}) +target_link_libraries(web-cbe-webos6 PRIVATE cbe-stub-webos6 ${GLIB_LIBRARIES}) +target_compile_definitions(web-cbe-webos6 PRIVATE APP_LOG_NAME="web-cbe-webos6") + +set_target_properties(web-cbe-webos6 PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-cbe-webos6 + APPID org.webosbrew.sample.web.cbe6 + TITLE "CBE WebView (webOS 6+)" + COLOR "#5C6BC0" + WEBOS_VERSIONS ">=6, <7") diff --git a/web/cbe-webos6/README.md b/web/cbe-webos6/README.md new file mode 100644 index 0000000..db12bed --- /dev/null +++ b/web/cbe-webos6/README.md @@ -0,0 +1,90 @@ +# web/cbe, on webOS 6 and newer + +**Verified on a 65UP7560 (starfish 6.5.2):** the page loads and renders full-screen, and +the app becomes the foreground app. + +Measured on a **65UP7560 running starfish 6.5.2** (Chromium 79). + +## A third shape of the same API + +`webos::WebViewBase` exists from webOS 3 to 11.2, but it is not one API. This generation +differs from webOS 4 in every part that the calling code touches: + +| | webOS 4 | webOS 6+ | +|---|---|---| +| entry point | `WebOSMain(argc, argv)`, a free function | `webos::WebOSMain(delegate).Run(argc, argv)` | +| view constructor | `WebViewBase()` | `WebViewBase(bool, int, int)` | +| `Initialize` | 5 strings + `int, int, bool` | see below | +| delegate slots | 24 | **61** | +| `sizeof(WebViewBase)` | 8 | **92** | +| `sizeof(WebAppWindowBase)` | 8 | 8 | + +**The object size is load-bearing.** libcbe's `WebViewBase` constructor writes as far as +offset 90, and WAM's `BlinkWebView` allocates 116 bytes with its own fields starting at 92 - +so the base is 92. Declare a subclass any smaller and libcbe writes past the allocation; the +process then dies inside `malloc` much later, nowhere near the cause. That is what the +`reserved_` member is for, and why it is not cosmetic. + +## `Initialize` has two overloads, and they split the range + +| release | 10-arg `Initialize` | 6-arg | constructor | +|---|---|---|---| +| 5.3.1 | yes | no | `WebViewBase(int, int)` | +| **6.4** | **yes** | **yes** | `WebViewBase(bool, int, int)` | +| 7.4 - 11.2 | no | yes | `WebViewBase(bool, int, int)` | + +webOS 6 is a transition release carrying both. This sample uses the 10-argument form, +because that is the one WAM calls at all three of its call sites on 6.5 - which caps it at +webOS 6. A 7-and-up variant is the same code with the 6-argument form, and that form does +verify clean all the way to 11.2. + +## Where the layout came from + +WAM still subclasses the `webos::` API on this generation - `libWebAppMgr.so` makes 122 +calls into it and carries both `BlinkWebView` and `WebAppWaylandWindow` - so the same +technique as webOS 4 applies, and `BlinkWebView`'s vtable gives all 61 slots with names for +most. Slots WAM fills with local functions are `UnknownNN` placeholders: they must exist and +hold their position, but their signatures are not recoverable from the vtable alone. + +`webos::WebAppWindowBase`'s vtable is more interesting - it mixes `webos::` methods with +`neva_app_runtime::WebAppWindowDelegate` ones, so by webOS 6 the old API sits *on top of* +the new runtime rather than beside it. + +### `neva_app_runtime` is not the shortcut it looks like + +It is the API with public upstream headers (`webosose/chromium87`, +`src/neva/app_runtime/public`), and webOS 6.5 exports 218 of its symbols. But: + +* **nothing in the firmware uses it** - WAM is on `webos::`, so there is no reference + implementation and no vtable to recover names from; +* **the upstream headers do not match.** LG's build is Chromium 79, between the public + chromium68 and chromium87 trees, and its `WebViewBase` vtable has **40** slots where + chromium87 declares 32 virtuals and chromium68 declares 26. So the headers are a naming + reference, not the layout - the same position as `webos::`, but without WAM to check + against. + +That is why this sample stays on `webos::`. + +## The slot that has a return value + +Most delegate slots return `void`, so a placeholder with an empty body is harmless: the +caller ignores whatever is in `r0`. **Slot 53 is not one of them.** It is +`GetWebContents()`, it is virtual, and libcbe calls it through the vtable *during* +`Initialize()`. Declared as a `void` placeholder, it hands libcbe whatever happened to be in +`r0` as a `WebContents*`, and the process dies inside `Initialize` with a backtrace that +points at libcbe rather than at the mistake. + +It is declared pure in the delegate and overridden in `WebViewBase` **with no body**, so the +slot resolves to libcbe's own `_ZN5webos11WebViewBase14GetWebContentsEv` at link time - the +app inherits the real implementation instead of shadowing it. + +Finding it took a slot tracer: replacing every one of the 61 overrides with a body that +prints its own index and touches no argument. Exactly one line came out - + +``` +[slot] 53 +``` + +- which named the culprit immediately, where reading arguments had only produced garbage. +The same trick is worth reaching for on any of these reconstructed vtables: **a slot that +returns a pointer cannot be stubbed, and a tracer finds it in one run.** diff --git a/web/cbe-webos6/main.cpp b/web/cbe-webos6/main.cpp new file mode 100644 index 0000000..3264d6d --- /dev/null +++ b/web/cbe-webos6/main.cpp @@ -0,0 +1,147 @@ +// Embedding the TV's Chromium on webOS 6 and newer. +// +// Same shape as web/cbe: hand the process to Chromium, get called back on its +// browser UI thread through the default GMainContext, build a window and a web +// view. Everything that differs is the API moving between generations - see +// ../libcbe/webos6/webos/webview_base.h for the full list, but the three that +// change the code are: +// +// * the entry point is a class - webos::WebOSMain(delegate).Run(argc, argv) - +// where webOS 3 and 4 had a free WebOSMain() function; +// * WebViewBase takes (bool, int, int) and Initialize takes five strings and +// a bool, dropping the width/height pair webOS 4 passed there; +// * the delegate is 61 slots instead of 24. +// +// One binary covers webOS 6.4 through 11.2: the API stops moving after 6.4. + +#include + +#include +#include +#include +#include +#include +#include + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.cbe6"; +const char kUrl[] = "https://example.com/"; + +class SampleWebView : public webos::WebViewBase { + public: + SampleWebView(int w, int h) : webos::WebViewBase(false, w, h) {} + + void OnLoadProgressChanged(double progress) override { + printf("[cbe] progress %3.0f%%\n", progress * 100); + } + void TitleChanged(const std::string& title) override { + printf("[cbe] title '%s'\n", title.c_str()); + } + void LoadStarted() override { puts("[cbe] load started"); } + void LoadFinished(const std::string& url) override { + printf("[cbe] finished %s\n", url.c_str()); + } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[cbe] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void DocumentLoadFinished() override { puts("[cbe] document loaded"); } + void LoadVisuallyCommitted() override { puts("[cbe] visually committed"); } + void DidFirstMeaningfulPaint() override { puts("[cbe] first meaningful paint"); } + void RenderProcessCreated(int pid) override { printf("[cbe] renderer pid %d\n", pid); } + void RenderProcessGone(bool crashed) override { + printf("[cbe] renderer gone (crashed=%d)\n", (int)crashed); + } + void DidStartNavigation(const std::string& url, bool) override { + printf("[cbe] navigate %s\n", url.c_str()); + } +}; + +class SampleWindow : public webos::WebAppWindowBase { + public: + bool event(WebOSEvent*) override { return false; } +}; + +SampleWindow* g_window; +SampleWebView* g_webview; +std::string g_app_path; + +gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); + g_window->InitWindow(1920, 1080); + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + + g_webview = new SampleWebView(1920, 1080); + // WAM passes the same register for all five trailing arguments, so they are + // zeros there - width and height included. The size comes from the + // constructor on this generation, not from here. + g_webview->Initialize(kAppId, g_app_path, "default", "", "", 0, 0, false, + webos::WebViewBase::WEBVIEW_MODE_NORMAL, false); + g_webview->SetAppId(kAppId); + g_webview->SetAllowLocalResourceLoad(true); + g_webview->SetLocalStorageEnabled(true); + g_webview->SetVisible(true); + g_webview->UpdatePreferences(); + + g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->Show(); + g_window->Activate(); + + printf("[cbe] loading %s\n", kUrl); + g_webview->LoadUrl(kUrl); + return G_SOURCE_REMOVE; +} + +// WebOSMain wants a delegate. WAM's has a single slot; nothing here needs it. +class SampleMainDelegate : public webos::WebOSMainDelegate { + public: + void AboutToCreateContentBrowserClient() override {} +}; + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + const bool browser = IsBrowserProcess(argc, argv); + // SAM points a launched app's stdout at /dev/null. + if (browser) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, NULL, _IOLBF, 0); + + const char* slash = strrchr(argv[0], '/'); + g_app_path = slash ? std::string(argv[0], slash - argv[0]) : std::string("."); + + std::vector args; + args.push_back(argv[0]); + if (browser) { + args.push_back("--ozone-platform=wayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); + args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + args.push_back("--enable-logging=stderr"); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + if (browser) g_idle_add(CreateWebApp, NULL); + + static SampleMainDelegate delegate; + webos::WebOSMain main_runner(&delegate); + return main_runner.Run(static_cast(cargv.size()), cargv.data()); +} diff --git a/web/cbe/CMakeLists.txt b/web/cbe/CMakeLists.txt new file mode 100644 index 0000000..dddf118 --- /dev/null +++ b/web/cbe/CMakeLists.txt @@ -0,0 +1,39 @@ +# The smallest thing that puts a web page on screen: hand the process to +# Chromium, get called back, load a URL. +# +# webOS 4 only, and both bounds are build variants rather than walls - the same +# situation as media/smp/common, which is built four times for four generations. +# Below, webOS 3's libcbe is the pre-C++11 std::string ABI throughout and predates +# the WebViewBase() + Initialize() split, so it wants a legacy variant. Above, +# webOS 5 replaced the free WebOSMain() with a webos::WebOSMain class and changed +# the constructor again. The hard floor is 3.4: webOS 1 and 2 have no libcbe. +# +# The upper bound is starfish 4.9, not 5. The firmware dumps carry one entry per +# major release, and webOS 4 has two of them: starfish 4.4.2 is webOS 4.0 (the +# 2018 sets, which this targets) and starfish 4.10.0 is webOS 4.5 (2019), where +# Initialize() takes a ninth argument. -verify does not catch that - it reports +# All OK for 4.10.0 despite the symbol being absent from the whole dump - so this +# range comes from reading the symbol tables. See README.md. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +add_executable(web-cbe main.cpp) +target_include_directories(web-cbe SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) +target_link_directories(web-cbe PRIVATE ${GLIB_LIBRARY_DIRS}) +target_link_libraries(web-cbe PRIVATE cbe-stub ${GLIB_LIBRARIES}) +target_compile_definitions(web-cbe PRIVATE APP_LOG_NAME="web-cbe") + +# No RPATH at all: the link stub must not be shipped, and the TV resolves libcbe +# from /usr/lib like every other system library. +set_target_properties(web-cbe PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-cbe + APPID org.webosbrew.sample.web.cbe + TITLE "CBE WebView (webOS 4)" + COLOR "#4285F4" + WEBOS_VERSIONS ">=4, <4.10") diff --git a/web/cbe/README.md b/web/cbe/README.md new file mode 100644 index 0000000..7612d5e --- /dev/null +++ b/web/cbe/README.md @@ -0,0 +1,415 @@ +# libcbe - the TV's Chromium in a native app + +`/usr/lib/libcbe.so` is the "Chromium Browser Engine": a ~70 MB build of Chromium with +LG's webOS patches, and the thing every web app on the TV actually runs inside. It is a +plain shared library with a C++ ABI, and a native app can link against it and get a real +web view - no WAM, no web app package, no `type: "web"`. + +No headers ship on the device, none ship in the buildroot NDK, and the library is stripped, +so what this sample links against was reconstructed - and the reconstruction is the +interesting part, so it is written down below. + +There *is* one more place to look, though it does not remove the need: the **webOS OSE SDK** +(`/opt/webos-sdk-x86_64`) ships real `webos/webview_base.h`, `webview_delegate.h` and +`webapp_window_delegate.h`, plus the whole of WAM under `usr/include/webappmanager/`. See +"What the OSE SDK gives you" below for why that is a naming reference rather than an ABI. + +## What it does + +Hands the process to Chromium, waits to be called back on the browser UI thread, and puts +a page on screen as a normal foreground card: + +``` +main() + | + +- g_idle_add(CreateWebApp) queue work on the default GMainContext + | + +- WebOSMain(argc, argv) never returns + | + +- ... Chromium starts, begins pumping the default GMainContext ... + | + +- CreateWebApp() on the browser UI thread + WebAppWindowBase::InitWindow + SetWindowProperty("appId", ...) + WebViewBase::Initialize + LoadUrl + window->AttachWebContents(webview->GetWebContents()) +``` + +That inversion is the whole shape of the sample. `WebOSMain()` is Chromium's content main: +it takes the process over, re-execs this same binary for the renderer, owns the message +loop, and does not return. So there is no "initialise the web view, then carry on" - the +app has to give the process away and arrange to be called back. + +The seam is the one WAM uses. libcbe drives its browser UI thread from the **default** +`GMainContext`, which is how WAM's Luna service ends up running on that thread: WAM's +`main()` starts its LS2 service, then calls `WebOSMain()`, and the callbacks arrive on the +browser thread afterwards. Anything queued onto that context before `WebOSMain()` therefore +runs exactly once, on the browser UI thread, as soon as Chromium is up - which is the first +moment a window or a web view may legally be created. + +## webOS 4 only, and why + +| release | libcbe | entry point | verdict | +|---|---|---|---| +| 1.x, 2.x | absent | - | a different engine entirely - Qt5WebKit, see below | +| 3.4 - 3.9 | yes | `WebOSMain` | reachable, but a legacy-ABI variant: see below | +| **4.x** | **yes** | **`WebOSMain`** | **this sample** | +| 5.x | yes | `webos::WebOSMain::Run` | different entry point and `Initialize` | +| 6.x - 11.x | yes | `webos::WebOSMain::Run`, plus `neva_app_runtime` | a second, parallel API | + +Both bounds are ABI, not caution, and neither is a wall - each is a build variant, the same +way `media/smp/common` is built four times for four generations of `StarfishMediaAPIs`. + +**Below.** webOS 3's libcbe is the pre-C++11 `std::string` ABI throughout - there is not one +`__cxx11` symbol in the whole library - and it predates the split constructor: + +| | webOS 3.4 | webOS 4.4 | +|---|---|---| +| `std::string` | `std::basic_string` | `std::__cxx11::basic_string` | +| construction | `WebViewBase(int w, int h)` | `WebViewBase()` then `Initialize(...)` | +| window setup | `Resize()` + `Show()` | `InitWindow(w, h)` + `Activate()` | +| absent on 3.4 | | `SetAppPath`, `LoadExtension`, `UpdatePreferences`, `SetWindowProperty`* | + +\* `SetWindowProperty` and `SetCustomCursor` do exist on 3.4, but with old-ABI strings. + +Of the 30 libcbe symbols this sample needs, 11 are absent from a webOS 3.4 dump - all of +them either old-ABI spellings or the newer split-init calls. So webOS 3 wants a variant +built `-D_GLIBCXX_USE_CXX11_ABI=0` against an older header, not a newer runtime. + +**Above.** `webos::WebViewBase` exists all the way to 11.2, but it is not one API - how you +construct and initialise it moves five times: + +| release | constructor | `Initialize(...)` | entry point | +|---|---|---|---| +| 3.4 - 3.9 | `WebViewBase(int, int)` | *(none)* | `WebOSMain()`, pre-C++11 strings | +| **4.0 - 4.9** | **`WebViewBase()`** | **`(5 str, int, int, bool)`** | **`WebOSMain()`** - what this sample builds | +| 4.10 | `WebViewBase()` | `(5 str, int, int, bool, bool)` | `WebOSMain()` | +| 5.x | `WebViewBase(int, int)` | `(5 str, int, int, bool, WebViewMode, bool)` | `webos::WebOSMain::Run()` | +| 6.4 - 11.2 | `WebViewBase(bool, int, int)` | `(5 str, bool)` | `webos::WebOSMain::Run()` | + +Everything else is stable: of the 30 libcbe symbols this sample uses, the *same three* are +the only ones missing on every release from 5.3.1 to 11.2 - the free `WebOSMain`, the +no-argument constructor, and that `Initialize`. The other 27 are untouched across six +generations. + +So the interesting variant is not webOS 5, it is **6.4 through 11.2**: one build covers +every set from 2021 to 2025, because the constructor and `Initialize` do not move again +after 6.4. (Those releases also carry a second, parallel API under `neva_app_runtime`, +which is the one with public upstream headers.) + +The upper bound of *this* sample is therefore 4.9, not 5 - the break is `Initialize` +gaining one `bool` at 4.10, not the webOS 5 rewrite. + +**The hard floor is 3.4**, and it is the library, not the ABI: webOS 1 and 2 have no +`libcbe.so` at all. Chromium arrived with webOS 3. + +### webOS 1 and 2: Qt5WebKit, and why it is not a variant + +Those releases do have a web engine, just not this one: + +| | webOS 1.2 / 1.4 | webOS 2.2.3 | webOS 3.4+ | +|---|---|---|---| +| Qt | 5.0.0 | 5.2.1 | - | +| engine | `libQt5WebKit.so.5.0.0` | `libQt5WebKit.so.5.2.0` | `libcbe.so` | + +`libQt5WebKitWidgets` is there too, with the whole classic WebKit1 API - `QWebView`, +`QWebPage`, `QWebFrame`, `QWebSettings` - and a JavaScript bridge that is frankly nicer +than anything libcbe offers: `QWebFrame::evaluateJavaScript()` returns a value +synchronously, `addToJavaScriptWindowObject()` hands page JavaScript a real `QObject` with +slots and properties, and `QWebPage::javaScriptConsoleMessage()` is a console hook. No +injection to load, no command names to overload. + +Two things make it a separate project rather than a variant of this one: + +* **Nothing in the firmware uses the widgets API.** Scanning every shared object in the + webOS 2.2.3 dump, `libQt5WebKitWidgets.so.5` has zero consumers; WAM reaches WebKit + through QML instead (it needs `libQt5Qml`, `libQt5Quick` and `libQt5WebKit`). The dumps + cover libraries and not executables, so this is not quite proof - but a shipped, + unexercised library is exactly the situation this repo already has a scar from, so the + first job would be proving it loads and paints at all. +* **The NDK cannot build for it.** The buildroot SDK ships Qt **5.15.14** against TVs + running 5.0.0 and 5.2.1, and Qt's binary compatibility runs forwards only. It also has no + QtWebKit headers at all, QtWebKit having been dropped from Qt after 5.5. That means + period-correct headers from upstream plus link stubs - the same trick `web/libcbe` uses - + plus `moc` for any object exposed to JavaScript. + +A 55LF6310 (webOS 2.2.0) is the set that could settle the first point. + +## Which sets that actually is + +The firmware dumps carry one entry per major webOS release, so a release number in the +table above stands for the whole generation rather than for one build. Two of them are +webOS 4: + +| dump | LG's name for it | this sample | +|---|---|---| +| starfish 4.4.2 (`HE_DTV_W18R`) | webOS 4.0, the 2018 sets | **works** | +| starfish 4.10.0 (`HE_DTV_W19P`) | webOS 4.5, the 2019 sets | **does not load** | + +So the honest scope is *webOS 4.0 only* - one model generation. Hardware testing was a +49LK5900 (2018) at starfish 4.4.3. + +Reaching the 2019 sets is one `bool`: `Initialize` takes nine arguments there instead of +eight. That is a smaller delta than any other variant on this page, and it doubles the +hardware. + +### `-verify` does not catch this + +Worth knowing before trusting a green run. `webosbrew-elf-verify` reports **All OK** for +starfish 4.10.0 even though nothing in that entire dump exports the eight-argument +`Initialize` - grepping every `.json` in the firmware finds only the nine-argument one. It +flags the same symbol correctly on 5.3.1 ("missing symbol ... is bound lazily"), so the +check works in general and this firmware is a false negative. + +The range here was therefore set by reading the symbol tables directly, not by trusting the +tool. Which is the same lesson this repo already learned from the other direction: a clean +`-verify` says the symbols were found, not that the app runs. + +## Where the headers came from + +Three sources that agree with each other: + +* **Firmware symbol tables** (`dev-toolbox-cli/common/data/*/libcbe.so.json`) give every + exported name, and therefore every signature, across every release. +* **WAM's own vtables.** `libWebAppMgr.so` contains `BlinkWebView` and + `WebAppWaylandWindow`, the only in-firmware subclasses of these classes. Their vtables + pin down slot order, and their constructors pin down object size - `operator new(32)` + followed by a `BlinkWebView` that writes its first field at offset 8 says + `webos::WebViewBase` is exactly 8 bytes: a vptr and one pimpl pointer. +* **The OSE SDK headers**, for names and return types the binaries cannot give. + +### What the OSE SDK gives you + +`/opt/webos-sdk-x86_64/.../usr/include/chromium53/webos/` has the genuine article, and it +confirms all three load-bearing facts above independently: `WebViewBase : public +WebViewDelegate` with a single `WebView* m_webview` private member, a `WebViewDelegate` +with no virtual destructor, and a `WebAppWindowDelegate` that has one. It also names the +delegate slots this repo had numbered, and corrects `CheckKeyFilterTable` from `bool` to +`unsigned`. + +What it cannot do is replace the reconstruction, because it is a different Chromium. The +SDK is chromium53; retail webOS 4 is chromium68, and LG moved the delegate in between - +`AcceptsVideoCapture` and `AcceptsAudioCapture` are gone, `DidStartNavigation`, +`DidFinishNavigation` and `LoadAborted` are new. Its bundled `libcbe.so` is a third thing +again, exporting `webos::WebOSMain::Run` and a six-argument `Initialize`, which is the +webOS 6+ shape. Useful, but not the TV. + +Three facts in `web/libcbe/webos/webview_base.h` are load-bearing: + +* `WebViewDelegate` has **no virtual destructor**. Adding one shifts every slot by two. +* The delegate is **24 slots** long. Only the first 17 have recoverable names; libcbe still + indexes past them, and a short vtable reads whatever follows it in memory. Leaving them + out segfaults a few hundred milliseconds into the first page load - which is exactly how + they were found. +* Chromium is built **without RTTI** and exports no typeinfo for these classes, so anything + deriving from them must be compiled `-fno-rtti` too. + +`WebAppWindowDelegate`, by contrast, *does* have a virtual destructor, so its `event()` +lands at slot 2. The two classes are not symmetric. + +## Linking + +The NDK has no libcbe, so the build makes a stand-in: `web/libcbe/cbe_stub.cpp` is compiled +into a shared object with SONAME `libcbe.so` and nothing else in it. The loader picks up the TV's +real library at runtime because the SONAME matches. It is never installed - see +`BUNDLE_LIBS` being absent from the `webos_add_ipk` call. + +The stub is written as ordinary C++ against the same headers the sample uses, rather than +as a list of mangled names, so the two cannot drift apart. + +## Resources + +None need shipping. The system libcbe has `/usr/lib/cbe/webos_resources.pak` compiled in, +and finds `icudtl.dat`, the V8 snapshots and the locale paks next to it. (LG's own browser +app bundles a private copy of all of that, plus its own libcbe - that is a different, much +heavier arrangement, and not one homebrew wants.) + +## Running it + +``` +cmake --build build --target web-cbe-install +ares-launch -d org.webosbrew.sample.web.cbe +``` + +Launch it, do not run the binary from a shell: SAM sets `APPID` and `XDG_RUNTIME_DIR` and +gives the process the session it needs. Direct execution mostly works but is not the thing +being demonstrated. + +SAM points a launched app's stdout at `/dev/null`, so the sample redirects it - its own +delegate output is in `/tmp/web-cbe.log`. libcbe's logging goes through PmLog and reaches +`/var/log/messages` under the `web-cbe` tag either way (`journalctl` is not on the PATH +these shells get). + +To see what actually reached the screen - LSM will happily report a foreground surface that +drew nothing: + +```sh +ares-shell -d -r "luna-send -n 1 -w 10000 -f \ + luna://com.webos.service.tv.capture/executeOneShot \ + '{\"path\":\"/tmp/shot.png\",\"method\":\"DISPLAY\",\"format\":\"PNG\"}'" +ares-pull -d /tmp/shot.png . +``` + +## Hybrid apps: talking to the page, and being talked to + +All of this was checked on the 49LK5900, with a throwaway probe build rather than with the +sample as committed. + +**Native to JS.** `RunJavaScript()` and `RunJavaScriptInAllFrames()` work and take effect +immediately. Neither returns a value - there is no `...AndReturnResult` in this API. + +**JS to native.** libcbe carries Chromium V8 *injections*, and one of them is the +`PalmSystem` object every webOS web app already uses. Load it before navigating: + +```cpp +webview->Initialize(app_id, app_path, "trusted", "", "", 1920, 1080, false); +webview->LoadExtension("palmsystem"); // "palmsystem", NOT "v8/palmsystem" +``` + +The injection's native functions do not stay inside libcbe - they turn into +`BrowserControlMsg_Command` / `BrowserControlMsg_Function` IPC, which surfaces in the +browser process as delegate slots 19 and 20, `HandleBrowserControlCommand()` and +`HandleBrowserControlFunction()`. Those are *your* overrides. `Function` is synchronous and +hands you a `std::string*` to fill in, and the value lands back in JavaScript as the return +value of the call. + +So this round trip works today: + +```js +var reply = PalmSystem.getResource('probe-cmd', 'probe-arg'); // -> "native-said-hello" +``` +``` +[bridge] FUNCTION 'getResource' (1 args) +[bridge] arg[0] = 'probe-cmd' +``` + +Two things to know. The command names are the injection's, not yours - you are overloading +`getResource`, `serviceCall`, `activate` and the rest, so a real app JSON-encodes its own +protocol into one of them. And only the first argument came through on `getResource`, so +pack everything into that one string. The injection also calls `initialize` and +`identifier` on startup expecting WAM-shaped answers. + +Some of the injection's calls do not need overloading at all, because they map onto +delegate slots that exist for them: + +| JavaScript | native | +|---|---| +| `PalmSystem.close()` | `WebViewDelegate::Close()` | +| `PalmSystem.platformBack()` | `HandleBrowserControlCommand("platformBack")` | + +Prefer those where they fit - `web/hybrid` leaves its web view through `close()`. + +`PalmServiceBridge` is injected too (`new PalmServiceBridge()` yields an object with a +`call` function), which is the standard path for page JavaScript to reach `luna://` +services - including one your own native process registers. That is the right answer when +the bridge needs a real protocol rather than a signal, but it was not tested here. + +**What not to use.** `document.title` also reaches native code, as `TitleChanged()`, and it +is tempting because it needs no injection. It is not an IPC channel: the title is a UI +property with one global slot, so it collides with any page that manages its own, carries +no arguments, and forces edge-detection hacks to tell a fresh signal from a leftover one. +Use it for what it is - a page title. + +**Capturing console output.** Page `console.log` goes nowhere by default. Add +`--enable-logging=stderr` to the switch list and it appears on stderr, tagged with the app +id: + +``` +[org.webosbrew.sample.web.cbe] "PROBE console.log works", source: data:text/html,... +``` + +## Showing and hiding the web window + +The web window can be taken off the screen and brought back, and this was measured with +display captures rather than guessed at: + +```cpp +webview->SuspendPaintingAndSetVisibilityHidden(); +webview->SuspendWebPageDOM(); +webview->SetVisible(false); +window->SetWindowHostState(webos::NATIVE_WINDOW_MINIMIZED); +window->Hide(); +// ... later ... +window->Show(); +window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); +webview->SetVisible(true); +webview->ResumeWebPageDOM(); +webview->ResumePaintingAndSetVisibilityVisible(); +window->Activate(); +``` + +Hidden, the surface really is gone - the capture showed the TV falling through to the HDMI +input behind it. Restored, the page came back **without reloading**: same document, no +`LoadStarted`, no navigation. So "hand the screen to something else and come back" works +inside one process, and the page keeps its state across the round trip. + +That is the cheap half. The expensive half is what fills the screen while the web window is +hidden, and there the process layout decides everything. + +## Two processes, or one? + +**Two apps.** Tested with `media/lgnc` (an ordinary SDL2 sample) as the native app and this +one as the web app: + +* launching the web app over the running native app works, and the native app keeps + running in the background; +* `luna://com.webos.applicationManager/closeByAppId` closes the web app cleanly; +* **but the screen does not go back.** Closing the web app left the TV showing + `com.webos.app.externalinput.av1`, not the native app that was still alive behind it. LSM + does not restore a caller; +* and relaunching the native app to get back gave it **new pids** - a cold restart, not a + resume, because the sample implements none of the SAM native lifecycle + (`nativeLifeCycleInterfaceVersion`, `handlesRelaunch`, `registerApp`). + +So the two-app split is workable but not free: the web app has to explicitly launch the +native app on its way out, and the native app has to implement the SAM lifecycle or it will +lose its state every time you come back. That lifecycle work is the real cost of this +option, not the launching. + +**One process.** The constraint is that `WebOSMain()` owns `main()` and the default +`GMainContext` and never returns, so a second toolkit cannot run a `while (SDL_PollEvent)` +loop in the usual place. The way around it is the same seam the sample already uses for +startup: SDL is driven *from* the glib loop - create the window and pump events from a +`g_timeout_add` on the browser UI thread, so both toolkits live on one thread with one +loop. Two Wayland surfaces in one process is not itself a problem. + +This is reasoning, not a result: it has not been built. If you go that way, the thing to +check first is whether SDL2 tolerates having its video subsystem initialised somewhere +other than the real `main()` thread. + +## Combining with native rendering + +There is no offscreen path. Nothing in the exported API hands back a GL texture or an +exported surface - `AttachWebContents()` gives the contents to a libcbe-owned Wayland +window and that is where they are drawn. So a hybrid UI has to be composed at the window +level, and libcbe exports the pieces for it: + +* `WebViewBase::SetTransparentBackground(true)` punches the page through to whatever is + behind it. This is how web apps on the TV show hardware video: the decoder owns a plane, + the page draws the UI over a transparent hole. Pairing it with `media/smp/acb` is the + most likely shape of a native-plus-web app here. +* `WebAppWindowBase::CreateWindowGroup()` / `AttachToWindowGroup()` with + `WindowGroupConfiguration::AddLayer()` put several surfaces into one LSM group with named, + ordered layers - WAM's mechanism for overlays. +* `SetOpacity()` and `WebOSPlatform::SetInputRegion()` control blending and which rectangles + take input, so a native surface can stay clickable under a full-screen page. + +Neither of those two combinations was tested. What *is* established is the constraint that +shapes them: `WebOSMain()` owns the process and the default `GMainContext`, so a second +toolkit (SDL2, say) cannot run its own main loop in the usual way - it would need its own +thread and its own Wayland surface, and the two would then compete for LSM focus. + +## A worked hybrid app + +`web/hybrid` is this sample plus an SDL2 view, in one process, swapping which one is on +screen. It is where the show/hide and JavaScript-bridge machinery below is actually used. + +## What is not here + +* **Input.** The sample never calls `ForwardWebOSEvent()`, and whether remote-control keys + reach the page on their own has not been tested. +* **The bridge.** The sample loads no injection and overrides the browser-control slots + with empty bodies. The section above says what it takes to turn them on. +* **Lifecycle.** No SAM relaunch/close handling and no suspend on background. The calls + exist and are wired into the header - see the show/hide section - but nothing drives them, + and without the SAM side a relaunch is a cold restart. +* **webOS 5+.** See the table above. diff --git a/web/cbe/main.cpp b/web/cbe/main.cpp new file mode 100644 index 0000000..b37c837 --- /dev/null +++ b/web/cbe/main.cpp @@ -0,0 +1,162 @@ +// Embedding the TV's own Chromium in a native webOS app. +// +// Every other sample in this repo calls a media API and keeps control of its own +// main loop. This one is the opposite, and that inversion is the point: libcbe +// exports WebOSMain(), which is Chromium's content main. It takes the process +// over - dispatching the renderer and GPU subprocesses, owning the message loop - +// and does not return until the browser shuts down. There is no "initialise the +// web view, then carry on". +// +// So the app hands the process over and arranges to be called back. The seam is +// the one WAM itself uses: libcbe drives its browser UI thread from the *default* +// GMainContext, which is how WAM's Luna service ends up running on that thread. +// Anything queued onto that context before WebOSMain() therefore runs exactly +// once, on the browser UI thread, as soon as Chromium is up - which is the first +// moment a window or a web view may legally be created. + +#include + +#include +#include +#include +#include +#include +#include + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +extern "C" int WebOSMain(int argc, const char** argv); + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.cbe"; +const char kUrl[] = "https://example.com/"; + +// None of this is needed to get a page on screen - the sample implements the +// whole delegate so it doubles as documentation of what libcbe calls back. +class SampleWebView : public webos::WebViewBase { + public: + void LoadProgressChanged(double progress) override { + printf("[cbe] progress %3.0f%%\n", progress * 100); + } + void DidFirstFrameFocused() override { puts("[cbe] first frame focused"); } + void LoadVisuallyCommitted() override { puts("[cbe] visually committed"); } + void TitleChanged(const std::string& title) override { + printf("[cbe] title '%s'\n", title.c_str()); + } + void NavigationHistoryChanged() override {} + void Close() override { puts("[cbe] close requested"); } + bool DecidePolicyForResponse(bool, int status, const std::string& url, + const std::string&) override { + printf("[cbe] response %d %s\n", status, url.c_str()); + return false; // false: let Chromium handle it + } + void LoadStarted() override { puts("[cbe] load started"); } + void LoadStopped() override { puts("[cbe] load stopped"); } + void DidStartNavigation(const std::string& url, bool) override { + printf("[cbe] navigate %s\n", url.c_str()); + } + void DidFinishNavigation(const std::string&, bool) override {} + void LoadFinished(const std::string& url) override { + printf("[cbe] finished %s\n", url.c_str()); + } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[cbe] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void LoadAborted(const std::string& url) override { + printf("[cbe] aborted %s\n", url.c_str()); + } + void DocumentLoadFinished() override { puts("[cbe] document loaded"); } + void RenderProcessCreated(int pid) override { printf("[cbe] renderer pid %d\n", pid); } + void RenderProcessGone() override { puts("[cbe] renderer gone"); } +}; + +class SampleWindow : public webos::WebAppWindowBase { + public: + bool event(WebOSEvent*) override { return false; } +}; + +SampleWindow* g_window; +SampleWebView* g_webview; +std::string g_app_path; + +// Runs on Chromium's browser UI thread, once. +gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); + g_window->InitWindow(1920, 1080); + // Without this the surface still reaches the screen, but LSM reports it as a + // card with an empty appId: no lifecycle, no place in the recents list, and + // nothing for the Home key to come back to. + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowProperty("title", "CBE WebView"); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + + g_webview = new SampleWebView(); + g_webview->Initialize(kAppId, g_app_path, "default", "", "", 1920, 1080, false); + g_webview->SetAppId(kAppId); + g_webview->SetAppPath(g_app_path); + g_webview->SetAllowLocalResourceLoad(true); + g_webview->SetLocalStorageEnabled(true); + g_webview->SetVisible(true); + g_webview->UpdatePreferences(); + + // The window does not own the web contents, it only composites them. + g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->Show(); + g_window->Activate(); + + printf("[cbe] loading %s\n", kUrl); + g_webview->LoadUrl(kUrl); + return G_SOURCE_REMOVE; +} + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + // SAM points a launched app's stdout at /dev/null, so everything printed below + // is invisible unless it is redirected somewhere. (libcbe's own logging goes + // through PmLog and reaches /var/log/messages regardless.) + if (IsBrowserProcess(argc, argv)) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, nullptr, _IOLBF, 0); + + const char* slash = strrchr(argv[0], '/'); + g_app_path = slash ? std::string(argv[0], slash - argv[0]) : std::string("."); + const bool browser = IsBrowserProcess(argc, argv); + + // The launcher starts a native app with a JSON parameter, not with Chromium + // switches, so the app supplies its own. WAM gets the equivalent list from its + // systemd unit; these are the few that are not optional. + std::vector args; + args.push_back(argv[0]); + if (browser) { + args.push_back("--ozone-platform=wayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); // no forked helper; Chromium re-execs + args.push_back("--in-process-gpu"); // one less process to get right + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + // Wayland clients need this and the app launcher does not always set it. + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + + // Chromium re-execs this same binary for the renderer; only the browser + // process gets a window. + if (browser) g_idle_add(CreateWebApp, NULL); + + return WebOSMain(static_cast(cargv.size()), cargv.data()); +} diff --git a/web/hybrid/CMakeLists.txt b/web/hybrid/CMakeLists.txt new file mode 100644 index 0000000..975b81f --- /dev/null +++ b/web/hybrid/CMakeLists.txt @@ -0,0 +1,55 @@ +# SDL2 and a libcbe web view in one process, swapping which one is on screen. +# +# The interesting constraint is in main.cpp: WebOSMain() owns the process and the +# message loop, so SDL cannot have its own. It is pumped from the glib loop that +# libcbe already runs on its browser UI thread, which puts both toolkits on one +# thread and makes switching views a plain function call. +# +# webOS 4 only, for the same ABI reasons as web/cbe - and verified only from 4.4.2 +# up, since no earlier webOS 4 dump exists. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) +pkg_check_modules(SDL2 REQUIRED sdl2) +pkg_check_modules(GLES2 REQUIRED glesv2) + +# Nuklear draws the native view. It is two single headers - the library and its +# SDL/GLES2 backend - so it is fetched rather than vendored, and compiled into +# exactly one translation unit (native_ui.c). +# +# GLES2 and not SDL_Renderer, and that is forced: the TV ships SDL 2.0.4, which +# predates SDL_RenderGeometry, so Nuklear's sdl_renderer backend cannot link +# there. The buildroot NDK's SDL is 2.30.12, so this is exactly the kind of +# mistake that builds cleanly and fails on the device - `-verify` is what +# catches it. +include(FetchContent) +set(NUKLEAR_REPOSITORY "https://github.com/Immediate-Mode-UI/Nuklear.git" + CACHE STRING "Nuklear git repository") +set(NUKLEAR_TAG "master" CACHE STRING "Nuklear git tag or branch") +FetchContent_Declare(nuklear + GIT_REPOSITORY "${NUKLEAR_REPOSITORY}" + GIT_TAG "${NUKLEAR_TAG}" + GIT_SHALLOW TRUE + SOURCE_SUBDIR do-not-add-this-project) +FetchContent_MakeAvailable(nuklear) + +add_executable(web-hybrid main.cpp native_ui.c) +target_include_directories(web-hybrid PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_include_directories(web-hybrid SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS} ${SDL2_INCLUDE_DIRS} + "${nuklear_SOURCE_DIR}" "${nuklear_SOURCE_DIR}/demo/sdl_opengles2") +target_link_directories(web-hybrid PRIVATE ${GLIB_LIBRARY_DIRS} ${SDL2_LIBRARY_DIRS} ${GLES2_LIBRARY_DIRS}) +target_link_libraries(web-hybrid PRIVATE cbe-stub ${GLIB_LIBRARIES} ${SDL2_LIBRARIES} ${GLES2_LIBRARIES}) +target_compile_definitions(web-hybrid PRIVATE APP_LOG_NAME="web-hybrid") + +set_target_properties(web-hybrid PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-hybrid + APPID org.webosbrew.sample.web.hybrid + TITLE "SDL + WebView (webOS 4)" + COLOR "#7E57C2" + WEBOS_VERSIONS ">=4, <4.10" + MEDIA "${CMAKE_CURRENT_SOURCE_DIR}/page.html") diff --git a/web/hybrid/README.md b/web/hybrid/README.md new file mode 100644 index 0000000..5dc5df9 --- /dev/null +++ b/web/hybrid/README.md @@ -0,0 +1,361 @@ +# SDL2 and a web view in one process + +A native app that needs the user to sign in on somebody else's web page, and needs whatever +that page hands back. Native code draws one screen, Chromium draws the provider's, and the +result comes home in the redirect URL: + +``` + [native: "Sign in"] -> [web: provider's login form] -> [native: "signed in as demo"] + | + navigates to redirect_uri?user=... + which the app intercepts and the browser never loads +``` + +One process, two Wayland surfaces, no second app and no browser to ship. + +This is the shape [chiaki-ng](https://github.com/streetpea/chiaki-ng) needs for PSN login, +and shipping a whole browser for it is absurd. The provider here is a local `page.html` +rather than Sony, so the sample is self-contained, but the mechanism is identical. + +## The problem this solves + +The two toolkits disagree about who owns the process. `WebOSMain()` is Chromium's content +main: it never returns and it owns the message loop. SDL normally wants a +`while (SDL_PollEvent(...))` loop in `main()`. Only one of them can have it. + +Chromium wins, and SDL is driven from *its* loop instead. libcbe pumps the default +`GMainContext` on its browser UI thread, so a `g_timeout_add(16, Pump, ...)` there polls SDL +events and repaints at ~60 Hz. Both toolkits then live on one thread with one loop, and +switching views is a plain function call rather than a lifecycle event: + +```cpp +gboolean Pump(gpointer) { + SDL_Event e; + while (SDL_PollEvent(&e)) { ... } + if (g_native_visible) DrawNativeView(); + return G_SOURCE_CONTINUE; +} +``` + +SDL initialises fine from there - `SDL_InitSubSystem(SDL_INIT_VIDEO)` on the browser UI +thread reports the `wayland` driver and gives back a window, with libcbe already running in +the same process. So does a second GL context: `Mali-470 MP, OpenGL ES 2.0`, alongside the +EGL that Chromium is already using. + +## The native view + +Drawn with [Nuklear](https://github.com/Immediate-Mode-UI/Nuklear) on GLES2, behind the +four functions in `native_ui.h`. It is in its own file, and in C rather than C++, for two +reasons: Nuklear compiles its implementation into exactly one translation unit and is not +happy as C++, and `main.cpp` is about one process owning two window systems - burying that +under an immediate-mode toolkit would hide the part worth reading. + +The toolkit earns its place by drawing text at all. SDL2 alone cannot, so the alternative +was coloured rectangles and a shipped font; Nuklear bakes its own atlas, rebaked here at +30px because the built-in 13px is unreadable across a living room. + +### GLES2 is not a preference + +Nuklear's `sdl_renderer` backend - and Dear ImGui's `imgui_impl_sdlrenderer2` - are built on +`SDL_RenderGeometry`, which arrived in **SDL 2.0.18**. The TV ships **2.0.4**: + +``` +SDL_RenderGeometry: MISSING +SDL_RenderGeometryRaw: MISSING +SDL_GetTicks64: MISSING +``` + +The buildroot NDK ships 2.30.12, so a `sdl_renderer` build compiles perfectly and fails on +the device. `demo/sdl_opengles2/nuklear_sdl_gles2.h` avoids all of it by using its own GL +context. + +That backend still calls `SDL_GetTicks64` twice, which `-verify` caught and the compiler +could not: + +``` +* Symbol SDL_GetTicks64 is undefined (bound lazily) +``` + +`native_ui.c` substitutes a function-like macro over the two call sites before including +the header - no patching a fetched dependency, and no competing `SDL_GetTicks64` in this +binary that would shadow the real one on firmware that has it. + +## Switching + +Hiding is not just `SDL_HideWindow()` on one side and `Hide()` on the other; the web view +also gets suspended, which is what stops a backgrounded page burning CPU on timers and +animation: + +```cpp +webview->SuspendPaintingAndSetVisibilityHidden(); +webview->SuspendWebPageDOM(); +webview->SetVisible(false); +window->SetWindowHostState(webos::NATIVE_WINDOW_MINIMIZED); +window->Hide(); +``` + +Coming back resumes rather than reloads. That is the whole reason for suspending instead of +destroying: the second visit keeps whatever state the page had, and the log shows no +navigation at all. + +Two things about coming back are not obvious, and both showed up as a white screen on the +second visit with the page alive and running script behind it. + +**`Hide()` destroys the window, it does not unmap it.** The log is explicit - `Wayland +Window(id:1 widget:0xc1f28) will be destroyed` - and the next `Show()` builds a new one, +`id:2`. Web contents attached to the old window composite nowhere. So +`AttachWebContents()` has to run on *every* show, not just the first. + +**It has to run before `Show()`, not after.** Attaching afterwards leaves the page loading +normally, reporting `load finished`, and never appearing. + +There is no matching `DetachWebContents()` on the way out. Calling it there segfaults on a +null pointer: by then the contents it would detach are already gone. + +## Getting the answer out: intercept the navigation + +The important channel is not JavaScript at all. Every navigation the web view starts arrives +in the delegate first, with the full URL and its query string: + +```cpp +void DidStartNavigation(const std::string& url, bool is_main_frame) override { + if (!is_main_frame) return; + if (url.compare(0, sizeof(kRedirectPrefix) - 1, kRedirectPrefix) != 0) return; + OnRedirect(url); // pull "user" and "state" straight out of the URL +} +``` + +**It fires before the request is made**, which is the property the whole flow rests on: the +redirect target never has to exist. This sample points it at `webosbrew.invalid`, and the +log shows the interception landing first and the failure arriving after: + +``` +[web] navigate https://webosbrew.invalid/callback?state=s1-9431&user=demo +[auth] redirect: user='demo' state='s1-9431' +``` + +`StopLoading()` in the handler keeps the dead host from costing a DNS lookup and an error +page. A real provider's `redirect_uri` behaves the same way - chiaki sends PSN to +`https://remoteplay.dl.playstation.net/remoteplay/redirect`, which serves nothing useful, +and reads `?code=` off it exactly like this. + +Data goes the *other* way in the URL too, the way any OAuth client does it - no JavaScript +involved: + +```cpp +"file://" + app_path + "/page.html?state=" + nonce + "&redirect_uri=" + kRedirectPrefix +``` + +The page reads those with `URLSearchParams` and sends the nonce back; the app throws the +result away if it does not match. That check is not decoration - it is what stops an +unrelated navigation being mistaken for your redirect. + +The form needs somewhere to type from a remote, so the window sets +`SetUseVirtualKeyboard(true)`. + +What this sample skips and a real client must not: proper percent-decoding of the query +(the parser here is about ten lines), a nonce that is actually unguessable, and probably +`SetUserAgent()` - some providers refuse to serve a login page to an unrecognised browser. +Cookies persist in the `WebViewProfile`, so a second sign-in may not need the form at all. + +## The other channel: JavaScript to native + +The login flow does not need it, but it is the only channel that carries a value in *both* +directions, so it is worth knowing: + +```js +var reply = PalmSystem.getResource('note::hello', ''); // -> whatever native writes +``` +```cpp +void HandleBrowserControlFunction(const std::string& command, + const std::vector& args, + std::string* result) override { + // args[0] == "note::hello"; *result becomes the JS return value, synchronously +} +``` + +Two constraints. The command names belong to the injection rather than to you - the page +arrives by calling `getResource` - so a real protocol lives inside the argument. And only +the **first** argument survives the trip. + +Going the other way, `RunJavaScript()` returns nothing, so native can push but never ask. +A native-initiated request needs two hops and a request id. + +### This is what webOSTV.js is built on + +Not a homebrew-only seam. LG's own [webOSTV.js](https://webostv.developer.lge.com/develop/tools/webostvjs-introduction) +reaches the platform exactly this way - unminified, its service call is: + +```js +new PalmServiceBridge, this.bridge.onservicecallback = ..., this.bridge.call(uri, params) +``` + +and it uses `PalmSystem.platformBack`, `PalmSystem.deviceInfo`, `PalmSystem.identifier` and +`PalmSystem.stageReady` besides - `platformBack` being the same call this sample leaves the +web view with. So a page inside this app has the same foundation a normal webOS web app +does, and stock `webOSTV.js` should load in it. + +Whether `webOS.service.request()` then *succeeds* is a separate question and untested here: +`PalmServiceBridge.call()` ends up at ls-hubd, which already logs +`Can not find service "" permissions` for this executable. That is the LS2 role side, and +this sample makes no Luna calls. + +## How the page asks to leave + +Through libcbe's own callbacks. The app loads the `palmsystem` injection, which gives page +JavaScript real entry points, and two of them arrive in the delegate: + +| JavaScript | native | what it means | +|---|---|---| +| `PalmSystem.platformBack()` | `HandleBrowserControlCommand("platformBack")` | a notification - nothing is torn down | +| `PalmSystem.close()` | `WebViewDelegate::Close()` | a real close - the render view is going away | + +```js +function exitToNative() { PalmSystem.platformBack(); } +``` + +**Not `close()`.** It reads like the right call and it is not. The callback arrives from +`RenderViewHostImpl::OnClose()` with the render view already being destroyed, which is +correct for a page that is quitting and wrong for one stepping aside for a moment - the +next visit gets a dead view. `platformBack()` is a plain notification, so the page survives +to be shown again. `Close()` is still handled, so a page that really does close itself +hands the screen back rather than leaving a dead window up. + +Turning the injection on is two lines, and both matter: + +```cpp +webview->Initialize(app_id, app_path, "trusted", "", "", 1920, 1080, false); +webview->LoadExtension("palmsystem"); // "palmsystem", NOT "v8/palmsystem" +``` + +Get either wrong and there is no `PalmSystem` object at all, with no error - the page just +finds it undefined, which is why `page.html` checks for it and says so on screen. + +`web/cbe/README.md` covers the rest of the bridge, including +`HandleBrowserControlFunction()`, which is synchronous and returns a string to JavaScript, +and `PalmServiceBridge` for reaching `luna://` services. + +### Not the title + +An earlier version of this sample signalled the exit by setting `document.title` and +watching `TitleChanged()`. It worked, and it was wrong: the title is a UI property with one +global slot, so the channel collides with any page that manages its own title, cannot carry +arguments, and needs edge-detection hacks - a leftover title from the previous visit +bounced the user straight back out, and the page had to reset it on `visibilitychange` to +manufacture a fresh edge. None of that exists now. `TitleChanged()` is only logged. + +## Loading the app's own page + +Three settings are needed to load `page.html` over `file://`, and two are not enough: + +```cpp +webview->SetAllowLocalResourceLoad(true); +webview->SetFileAccessBlocked(false); +webview->SetAllowUniversalAccessFromFileUrls(true); +``` + +Without the third, the renderer is killed mid-load - `bad IPC message, reason 114` - rather +than being told no, which looks like a crash rather than a permissions problem. +`SetWebSecurityEnabled(false)`, the obvious sledgehammer, turns out not to be needed. + +## Logging + +SAM points a launched app's stdout at `/dev/null`, so both samples redirect it: + +```sh +ares-shell -d -r 'cat /tmp/web-hybrid.log' +``` + +libcbe's own logging goes through PmLog and reaches `/var/log/messages` either way, and +page `console.log` shows up in the redirected stream because the app passes +`--enable-logging=stderr`. + +## State + +Verified on a 49LK5900 (webOS 4.4.3): the whole sign-in flow runs end to end - native panel, +provider form, and back to the panel reading `signed in as demo`, with the nonce checked and +the redirect never loaded. Both views render full-screen, and the switch works in both +directions. The SDL view keeps +animating after coming back. + +Driven entirely by injected remote keys, one at a time, comparing frames pixel-by-pixel - +which is how the focus limitation above turned up. By pointer the same flow is two clicks. + +Anything that changes the window or the web view must also be **deferred out of a delegate +callback**. Those callbacks run inside libcbe's own call stack, and switching views from +one lands in the middle of a teardown it has not finished - a null-pointer segfault, with +`ShowNativeView` sitting directly under `RenderViewHostImpl::OnClose()` in the backtrace. +A one-shot `g_idle_add` is enough: the switch then happens once libcbe is back at idle. + +### Testing the remote without a remote + +`/dev/uinput` is a dead end - a virtual device registers, but LSM does not route its events +to the app. What does work is the service the phone-remote app drives: + +```sh +ares-shell -d -r \ + 'luna-send -n 1 -f luna://com.webos.service.networkinput/test/sendKeyCode "{\"keyCode\":28}"' +``` + +It takes **evdev** codes, not webOS or JavaScript ones. `28` is `KEY_ENTER`, and the app +logs `[sdl] key 13` - `SDLK_RETURN` - followed by `[switch] native -> web`. So the OK-key +path is verified. (`13` also injects, arriving as SDL keysym `61`, which is a reminder that +the mapping is not identity.) + +`ls-monitor -i com.webos.service.networkinput` lists the rest, including +`getPointerInputSocket` for the magic-remote pointer. + +Injecting and watching gives this mapping, which is worth having written down because two +different numbers are involved: + +| evdev code sent | SDL `keysym.sym` | SDL `keysym.scancode` | | +|---|---|---|---| +| 28 `KEY_ENTER` | 13 `SDLK_RETURN` | 40 | OK | +| 174 `KEY_EXIT` | **0** | **505** `SDL_WEBOS_SCANCODE_EXIT` | | +| 1 `KEY_ESC` | 27 | 41 | | +| 14 `KEY_BACKSPACE` | 8 | 42 | | + +**Back and Exit have no keysym.** They arrive only as scancodes above 480, defined in +`SDL_webOS.h`, so code that switches on `keysym.sym` drops them silently - which is what +this sample did until the table above was measured. `Pump()` now checks the scancode first. + +`KEY_BACK` (158) injects without error and arrives nowhere; the service's `sendSpecialKey` +table, in `strings /usr/sbin/network-input-service`, has no BACK or EXIT either. Exit is +reachable and Back is not, at least by injection. + +### Keys only reach whichever window is up + +Once the web view is showing, the SDL loop stops seeing keys entirely - they belong to +libcbe's window. So the OK key opening the web view is an SDL concern, and leaving again is +not: that has to come from the page (its `keydown` handler) or from +`WebAppWindowBase::event()`, and neither is verified. + +The web window does ask for the key, using the same property names WAM uses: + +```cpp +window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_BACK", "true"); +window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_EXIT", "true"); +``` + +Those names are not documented anywhere; they come out of `libWebAppMgr.so`. + +### The web view starts with nothing focused + +By remote the flow is **OK, Tab, Tab, Tab, Enter**, not OK then Enter. Until Tab is pressed +the page has no focused element, so Enter does nothing; Tab does not merely move focus, it +creates it. After that Enter activates a focused button, though it still never submits from +a text input. By pointer it is one click, because mouse events carry their own target. + +`autofocus`, a page-side `.focus()` on load, and `WebViewBase::SetFocus(true)` were all +tried and none helped. libcbe hints at why: + +``` +ERROR:webos_view.h(123)] Not implemented reached in + virtual void WebOSView::OnWidgetActivationChanged(views::Widget*, bool) +``` + +Widget activation is a stub in this build, and in Chromium's views that is what hands focus +to the `FocusManager` - which would explain a web view that takes pointer input and ignores +the keyboard until focus traversal forces the issue. Untested next step: +`SetCSSNavigationEnabled(true)`, webOS's own spatial navigation. There is a fuller write-up +on the pull request. diff --git a/web/hybrid/main.cpp b/web/hybrid/main.cpp new file mode 100644 index 0000000..11c3002 --- /dev/null +++ b/web/hybrid/main.cpp @@ -0,0 +1,423 @@ +// One process, two windows: an SDL2 view and a libcbe web view, swapping places. +// +// This is the shape a hybrid app wants - native code drawing one screen, a real +// browser drawing another - and the awkward part is that the two disagree about +// who owns the process. WebOSMain() is Chromium's content main: it never returns +// and it owns the message loop. SDL normally wants a `while (SDL_PollEvent)` loop +// in main(). Only one of them can have it. +// +// Chromium wins, and SDL is driven from its loop instead. libcbe pumps the +// default GMainContext on its browser UI thread, so a g_timeout_add() there polls +// SDL events and repaints at a fixed rate. Both toolkits then live on one thread +// with one loop, each with its own Wayland surface, and swapping views is a +// matter of hiding one and showing the other. +// +// [SDL view] --OK/Enter--> [web view] --exit button or Back--> [SDL view] + +#include +#include + +#include "native_ui.h" +#include + +#include +#include +#include +#include +#include +#include + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +extern "C" int WebOSMain(int argc, const char** argv); + +// Only the webOS fork of SDL has this, and only from webOS 4 on. +extern "C" SDL_bool SDL_webOSCursorVisibility(SDL_bool visible) __attribute__((weak)); + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.hybrid"; + +// Where the login flow is told to send the user when it is done. It does not +// have to resolve, and here it deliberately does not: the app intercepts the +// navigation before the load can fail. This is how an OAuth redirect_uri +// behaves in a native client - chiaki's PSN login uses +// https://remoteplay.dl.playstation.net/remoteplay/redirect the same way. +const char kRedirectPrefix[] = "https://webosbrew.invalid/callback"; + +std::string g_app_path; + +// ---------------------------------------------------------------- web view + +class HybridWebView; +void ShowNativeView(); +void RequestNativeView(); +void OnRedirect(const std::string& url); +std::string BuildAuthUrl(); + +// The page asks to leave through libcbe's own callbacks, not through a side +// effect of some UI property. Loading the "palmsystem" injection gives page +// JavaScript real entry points, and two of them land here: +// +// PalmSystem.close() -> WebViewDelegate::Close() +// PalmSystem.platformBack() -> HandleBrowserControlCommand("platformBack") +// +// Close() is the delegate's own dedicated slot, so nothing is overloaded and +// nothing has to be parsed out of a shared channel. + +bool g_web_visible; + +class HybridWebView : public webos::WebViewBase { + public: + void TitleChanged(const std::string& title) override { + printf("[web] title '%s'\n", title.c_str()); + } + void LoadFinished(const std::string&) override { puts("[web] load finished"); } + // The redirect target never resolves, so this fires with ERR_NAME_NOT_RESOLVED + // right after the interception above. Expected, and not worth reporting. + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + if (url.compare(0, sizeof(kRedirectPrefix) - 1, kRedirectPrefix) == 0) return; + printf("[web] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void DidFinishNavigation(const std::string&, bool) override {} + bool DecidePolicyForResponse(bool, int, const std::string&, const std::string&) override { + return false; + } + void RenderProcessCreated(int pid) override { printf("[web] renderer pid %d\n", pid); } + + void LoadProgressChanged(double) override {} + void DidFirstFrameFocused() override {} + void LoadVisuallyCommitted() override {} + void NavigationHistoryChanged() override {} + // PalmSystem.close(): a real close. Chromium means it - the callback arrives + // from RenderViewHostImpl::OnClose() and the render view is being destroyed - + // so this is the wrong way to say "hide me, I will be back". The page uses + // platformBack for that; this is kept so a page that does close itself still + // hands the screen back rather than leaving a dead window up. + void Close() override { + puts("[web] page closed itself"); + RequestNativeView(); + } + + // PalmSystem.platformBack(): a Back gesture, and only a notification - nothing + // is torn down, so the page survives to be shown again. + void HandleBrowserControlCommand(const std::string& command, + const std::vector&) override { + printf("[web] browser control '%s'\n", command.c_str()); + if (command == "platformBack") RequestNativeView(); + } + + // The other direction, unused by this flow but kept because it is the only + // channel that carries a value both ways. See README.md. + void HandleBrowserControlFunction(const std::string& command, + const std::vector& args, + std::string*) override { + printf("[web] function '%s' (%zu args)\n", command.c_str(), args.size()); + } + void LoadStarted() override { puts("[web] load started"); } + void LoadStopped() override { puts("[web] load stopped"); } + // The whole point of the sample. Every navigation the web view starts passes + // through here first, with the full URL and its query string, *before* the + // request is made - so a redirect target that cannot resolve is still caught. + void DidStartNavigation(const std::string& url, bool is_main_frame) override { + printf("[web] navigate %s\n", url.c_str()); + if (!is_main_frame) return; + if (url.compare(0, sizeof(kRedirectPrefix) - 1, kRedirectPrefix) != 0) return; + OnRedirect(url); + } + void LoadAborted(const std::string&) override {} + void DocumentLoadFinished() override {} + void RenderProcessGone() override { puts("[web] renderer gone"); } +}; + +class HybridWindow : public webos::WebAppWindowBase { + public: + // The remote's Back key, if the TV lets the app have it. + bool event(WebOSEvent*) override { return false; } +}; + +HybridWindow* g_window; +HybridWebView* g_webview; + +// ---------------------------------------------------------------- SDL view + +SDL_Window* g_sdl_window; +bool g_native_visible; + +// --------------------------------------------------------------- switching + +void ShowWebView() { + if (g_web_visible) return; + puts("[switch] native -> web"); + g_native_visible = false; + SDL_HideWindow(g_sdl_window); + + const bool first_time = (g_webview == NULL); + if (first_time) { + g_window = new HybridWindow(); + g_window->InitWindow(1920, 1080); + g_window->SetWindowProperty("appId", kAppId); + // Without these the TV keeps Back and Exit for itself and the web view never + // sees them - the same access policy the SDL side asks for through + // SDL_HINT_WEBOS_ACCESS_POLICY_KEYS_*. The property names come out of + // libWebAppMgr.so, which is the only place they are written down. + g_window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_BACK", "true"); + g_window->SetWindowProperty("_WEBOS_ACCESS_POLICY_KEYS_EXIT", "true"); + g_window->SetWindowHostState(webos::NATIVE_WINDOW_FULLSCREEN); + // The form needs somewhere to type from a remote. + g_window->SetUseVirtualKeyboard(true); + + g_webview = new HybridWebView(); + g_webview->Initialize(kAppId, g_app_path, "trusted", "", "", 1920, 1080, false); + g_webview->SetAppId(kAppId); + g_webview->SetAppPath(g_app_path); + // All three are needed to load the app's own page.html over file://. Two are + // not enough: without SetAllowUniversalAccessFromFileUrls the renderer is + // killed mid-load with "bad IPC message, reason 114" rather than being told + // no. SetWebSecurityEnabled(false), which is the obvious sledgehammer, turns + // out not to be needed at all. + g_webview->SetAllowLocalResourceLoad(true); + g_webview->SetFileAccessBlocked(false); + g_webview->SetAllowUniversalAccessFromFileUrls(true); + g_webview->SetLocalStorageEnabled(true); + // Without this the page has no PalmSystem object and therefore no way to + // reach native code at all. It needs the "trusted" trust level passed to + // Initialize() above, and the name is "palmsystem", not "v8/palmsystem". + g_webview->LoadExtension("palmsystem"); + g_webview->UpdatePreferences(); + } else { + // Second time round the page is still loaded - only woken up. + g_webview->ResumeWebPageDOM(); + g_webview->ResumePaintingAndSetVisibilityVisible(); + } + + g_webview->SetVisible(true); + + // Every time, not just the first, and before Show(). Hide() does not unmap the + // window - it destroys it, "Wayland Window(id:1) will be destroyed" - so the + // next Show() builds a fresh one and contents left attached to the old window + // composite nowhere. Attaching after Show() does not work either: the page + // loads and never appears. + g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->Show(); + g_window->Activate(); + g_web_visible = true; + + // A sign-in always starts fresh, with a new nonce - resuming a half-finished + // login page would be the wrong thing even though the machinery allows it. + const std::string url = BuildAuthUrl(); + printf("[web] loading %s\n", url.c_str()); + g_webview->LoadUrl(url); +} + +void ShowNativeView() { + if (!g_native_visible) { + puts("[switch] web -> native"); + if (g_web_visible) { + // Suspending the page as well as hiding the window is what stops a + // backgrounded web view from burning CPU on timers and animation. + g_webview->SuspendPaintingAndSetVisibilityHidden(); + g_webview->SuspendWebPageDOM(); + g_webview->SetVisible(false); + // No DetachWebContents() here. It segfaults: by the time this runs the + // contents libcbe would detach are already gone, and it dereferences null. + g_window->SetWindowHostState(webos::NATIVE_WINDOW_MINIMIZED); + g_window->Hide(); + g_web_visible = false; + } + SDL_ShowWindow(g_sdl_window); + SDL_RaiseWindow(g_sdl_window); + g_native_visible = true; + } +} + +// Delegate callbacks arrive *inside* libcbe's own call stack - Close() comes +// straight out of RenderViewHostImpl::OnClose() - so tearing the window down +// from one lands in the middle of a teardown libcbe has not finished. It +// segfaults on a null pointer. Bouncing through the loop first means the switch +// happens once libcbe is back at idle and its own state is consistent. +gboolean SwitchToNativeLater(gpointer) { + ShowNativeView(); + return G_SOURCE_REMOVE; +} + +void RequestNativeView() { g_idle_add(SwitchToNativeLater, NULL); } + +// Pulls one query parameter out of a URL. Deliberately tiny: a real client +// wants proper percent-decoding, this only needs to survive a demo. +std::string QueryParam(const std::string& url, const std::string& key) { + const std::string needle = key + "="; + size_t at = url.find('?'); + if (at == std::string::npos) return std::string(); + for (size_t p = at + 1; p < url.size();) { + size_t end = url.find('&', p); + if (end == std::string::npos) end = url.size(); + if (url.compare(p, needle.size(), needle) == 0) + return url.substr(p + needle.size(), end - p - needle.size()); + p = end + 1; + } + return std::string(); +} + +// The redirect happened. Everything the flow produced is in this URL. +void OnRedirect(const std::string& url) { + const std::string user = QueryParam(url, "user"); + const std::string state = QueryParam(url, "state"); + printf("[auth] redirect: user='%s' state='%s'\n", user.c_str(), state.c_str()); + + // Stop before the dead host wastes a DNS lookup and an error page. + g_webview->StopLoading(); + + char line[200]; + if (state != native_ui_state()) { + // Someone else's redirect, or a stale one. A real client must check this. + snprintf(line, sizeof(line), "rejected: state did not match"); + } else if (user.empty()) { + snprintf(line, sizeof(line), "sign-in cancelled"); + } else { + snprintf(line, sizeof(line), "signed in as %s", user.c_str()); + } + native_ui_set_result(line); + RequestNativeView(); +} + +// Native hands data *to* the flow the same way any OAuth client does: in the +// URL it opens. No JavaScript involved. +std::string BuildAuthUrl() { + return "file://" + g_app_path + "/page.html?state=" + native_ui_new_state() + + "&redirect_uri=" + kRedirectPrefix; +} + +// ------------------------------------------------------------- the one loop + +gboolean Pump(gpointer) { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT) return G_SOURCE_REMOVE; + + native_ui_handle_event(&e); + if (e.type != SDL_KEYDOWN) continue; + + // Two different numbers. The remote's OK arrives as an ordinary keysym, but + // Back and Exit have no keysym at all - the webOS fork of SDL reports them + // as scancodes above 480, listed in SDL_webOS.h. Switching on keysym.sym + // alone means Back can never match, which is a quiet way to lose it. + const int sym = static_cast(e.key.keysym.sym); + const int scancode = static_cast(e.key.keysym.scancode); + printf("[sdl] key sym=%d scancode=%d\n", sym, scancode); + + if (scancode == SDL_WEBOS_SCANCODE_BACK || scancode == SDL_WEBOS_SCANCODE_EXIT) { + if (!g_native_visible) ShowNativeView(); + continue; + } + switch (sym) { + case SDLK_RETURN: + case SDLK_KP_ENTER: + case SDLK_SPACE: + if (g_native_visible) ShowWebView(); + break; + case SDLK_AC_BACK: + case SDLK_ESCAPE: + case SDLK_BACKSPACE: + if (!g_native_visible) ShowNativeView(); + break; + default: + break; + } + } + // Nuklear is immediate mode: the UI is rebuilt every frame, and the button + // reports itself by return value rather than through a callback. + if (g_native_visible && g_sdl_window != NULL && native_ui_frame(g_sdl_window)) { + ShowWebView(); + } + return G_SOURCE_CONTINUE; +} + +// Runs on Chromium's browser UI thread, once, as soon as the browser is up. +gboolean StartApp(gpointer) { + // SDL reads its EGL platform from the environment at video-init time. + setenv("EGL_PLATFORM", "wayland", 0); + + if (SDL_Init(0) != 0) { + printf("[sdl] SDL_Init failed: %s\n", SDL_GetError()); + return G_SOURCE_REMOVE; + } + // Without these the TV keeps Back and Exit for itself and the app never sees + // them. They are plain strings, so setting them costs nothing where they are + // not understood. + SDL_SetHint(SDL_HINT_WEBOS_ACCESS_POLICY_KEYS_BACK, "true"); + SDL_SetHint(SDL_HINT_WEBOS_ACCESS_POLICY_KEYS_EXIT, "true"); + + if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { + printf("[sdl] SDL_INIT_VIDEO failed: %s\n", SDL_GetError()); + return G_SOURCE_REMOVE; + } + printf("[sdl] video driver: %s\n", SDL_GetCurrentVideoDriver()); + + g_sdl_window = SDL_CreateWindow("hybrid", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + 1920, 1080, SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL); + if (g_sdl_window == NULL) { + printf("[sdl] SDL_CreateWindow failed: %s\n", SDL_GetError()); + return G_SOURCE_REMOVE; + } + if (!native_ui_init(g_sdl_window)) return G_SOURCE_REMOVE; + // The remote's pointer is a mouse as far as SDL is concerned, but the cursor + // has to be asked for. Weakly linked: webOS 3 and older have the hints but not + // this call, and a hard reference would stop the app loading there. + if (SDL_webOSCursorVisibility != NULL) SDL_webOSCursorVisibility(SDL_TRUE); + + g_native_visible = true; + puts("[sdl] native view up - click the button, or press OK/Enter"); + + g_timeout_add(16, Pump, NULL); + return G_SOURCE_REMOVE; +} + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + // SAM points a launched app's stdout at /dev/null, so everything printed below + // is invisible unless it is redirected somewhere. (libcbe's own logging goes + // through PmLog and reaches /var/log/messages regardless.) + if (IsBrowserProcess(argc, argv)) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, NULL, _IOLBF, 0); + + const char* slash = strrchr(argv[0], '/'); + g_app_path = slash ? std::string(argv[0], slash - argv[0]) : std::string("."); + const bool browser = IsBrowserProcess(argc, argv); + + std::vector args; + args.push_back(argv[0]); + if (browser) { + args.push_back("--ozone-platform=wayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); + args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + // Page console.log is invisible without this. + args.push_back("--enable-logging=stderr"); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + + // Chromium re-execs this same binary for the renderer; only the browser + // process gets windows. + if (browser) g_idle_add(StartApp, NULL); + + return WebOSMain(static_cast(cargv.size()), cargv.data()); +} diff --git a/web/hybrid/native_ui.c b/web/hybrid/native_ui.c new file mode 100644 index 0000000..98298f0 --- /dev/null +++ b/web/hybrid/native_ui.c @@ -0,0 +1,160 @@ +#include "native_ui.h" + +#include + +/* Nuklear is a single header that compiles its own implementation into exactly + * one translation unit - this one. The SDL/GLES2 backend is a second such + * header from the same repository. Neither is happy being compiled as C++, + * which is the other reason this file is C. */ +#define NK_INCLUDE_FIXED_TYPES +#define NK_INCLUDE_STANDARD_IO +#define NK_INCLUDE_STANDARD_VARARGS +#define NK_INCLUDE_DEFAULT_ALLOCATOR +#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT +#define NK_INCLUDE_FONT_BAKING +#define NK_INCLUDE_DEFAULT_FONT +#define NK_IMPLEMENTATION +#define NK_SDL_GLES2_IMPLEMENTATION +#include "nuklear.h" + +/* The backend calls SDL_GetTicks64, which arrived in SDL 2.0.18. The TV ships + * 2.0.4, and the buildroot NDK ships 2.30.12 - so this links cleanly on the host + * and dies on the device the moment Nuklear times a double click. It was caught + * by `-verify`, not by the compiler. + * + * A function-like macro substitutes at both call sites without patching the + * fetched header, and without defining a competing SDL_GetTicks64 in this binary + * that would then shadow the real one on firmware that has it. The 32-bit + * counter wraps after ~49 days, which matters to nothing here. */ +#define SDL_GetTicks64() ((Uint64)SDL_GetTicks()) +#include "nuklear_sdl_gles2.h" +#undef SDL_GetTicks64 + +/* Nuklear's vertex and element scratch buffers. The UI here is a handful of + * rectangles and some text, so these are generous. */ +#define MAX_VERTEX_MEMORY (256 * 1024) +#define MAX_ELEMENT_MEMORY (128 * 1024) + +/* The panel, centred on a 1920x1080 panel. */ +#define PANEL_W 900 +#define PANEL_H 470 + +static SDL_GLContext g_gl; +static struct nk_context *g_nk; +static bool g_open_requested; + +/* The sign-in nonce, and whatever the flow produced. */ +static char g_state[32] = ""; +static char g_result[200] = "(not signed in)"; + +const char *native_ui_new_state(void) { + /* Good enough to prove the redirect came from the attempt we started. A real + * client wants something unguessable. */ + static unsigned n; + SDL_snprintf(g_state, sizeof(g_state), "s%u-%u", ++n, (unsigned)SDL_GetTicks()); + return g_state; +} + +const char *native_ui_state(void) { return g_state; } + +void native_ui_set_result(const char *text) { + SDL_strlcpy(g_result, text != NULL ? text : "", sizeof(g_result)); +} + +bool native_ui_init(SDL_Window *window) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); + + g_gl = SDL_GL_CreateContext(window); + if (g_gl == NULL) { + SDL_Log("native_ui: SDL_GL_CreateContext failed: %s", SDL_GetError()); + return false; + } + SDL_Log("native_ui: GL %s on %s", glGetString(GL_VERSION), glGetString(GL_RENDERER)); + + g_nk = nk_sdl_init(window); + if (g_nk == NULL) { + SDL_Log("native_ui: nk_sdl_init failed"); + return false; + } + + /* Nuklear's built-in font is baked at 13px, which is unreadable across a + * living room. Rebaking it larger is the whole of "font handling" here - + * there is no font file to ship. */ + struct nk_font_atlas *atlas; + nk_sdl_font_stash_begin(&atlas); + struct nk_font *font = nk_font_atlas_add_default(atlas, 30.0f, NULL); + nk_sdl_font_stash_end(); + if (font != NULL) { + nk_style_set_font(g_nk, &font->handle); + } + return true; +} + +void native_ui_handle_event(const SDL_Event *event) { + if (g_nk == NULL) { + return; + } + /* nk_sdl_handle_event wants a mutable pointer but does not modify it. */ + SDL_Event copy = *event; + nk_input_begin(g_nk); + nk_sdl_handle_event(©); + nk_input_end(g_nk); +} + +bool native_ui_frame(SDL_Window *window) { + if (g_nk == NULL) { + return false; + } + int w = 0, h = 0; + SDL_GetWindowSize(window, &w, &h); + + const struct nk_rect panel = + nk_rect((float)(w - PANEL_W) / 2, (float)(h - PANEL_H) / 2, PANEL_W, PANEL_H); + + if (nk_begin(g_nk, "native", panel, NK_WINDOW_NO_SCROLLBAR | NK_WINDOW_BORDER)) { + nk_layout_row_dynamic(g_nk, 56, 1); + nk_label(g_nk, "sign in", NK_TEXT_CENTERED); + + nk_layout_row_dynamic(g_nk, 34, 1); + nk_label(g_nk, "The provider's login page runs in a web view;", NK_TEXT_CENTERED); + nk_label(g_nk, "the code comes back in the redirect URL.", NK_TEXT_CENTERED); + + nk_layout_row_dynamic(g_nk, 16, 1); + nk_spacing(g_nk, 1); + + nk_layout_row_dynamic(g_nk, 40, 1); + nk_label(g_nk, g_result, NK_TEXT_LEFT); + + nk_layout_row_dynamic(g_nk, 72, 1); + if (nk_button_label(g_nk, "Sign in")) { + g_open_requested = true; + } + + nk_layout_row_dynamic(g_nk, 32, 1); + nk_label(g_nk, "or press OK on the remote", NK_TEXT_CENTERED); + } + nk_end(g_nk); + + glViewport(0, 0, w, h); + glClearColor(0.05f, 0.07f, 0.12f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + nk_sdl_render(NK_ANTI_ALIASING_ON, MAX_VERTEX_MEMORY, MAX_ELEMENT_MEMORY); + SDL_GL_SwapWindow(window); + + const bool requested = g_open_requested; + g_open_requested = false; + return requested; +} + +void native_ui_shutdown(void) { + if (g_nk != NULL) { + nk_sdl_shutdown(); + g_nk = NULL; + } + if (g_gl != NULL) { + SDL_GL_DeleteContext(g_gl); + g_gl = NULL; + } +} diff --git a/web/hybrid/native_ui.h b/web/hybrid/native_ui.h new file mode 100644 index 0000000..7d8bdd6 --- /dev/null +++ b/web/hybrid/native_ui.h @@ -0,0 +1,40 @@ +// The native half of the hybrid app's UI, drawn with Nuklear on GLES2. +// +// Kept behind this four-function C interface on purpose. main.cpp is about one +// process owning two window systems; the immediate-mode toolkit behind here is +// an implementation detail of "the native view", and inlining it would bury the +// part worth reading. It is also plain C, which is what Nuklear is. +#ifndef WEB_HYBRID_NATIVE_UI_H_ +#define WEB_HYBRID_NATIVE_UI_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Creates the GLES2 context on the window and bakes the font atlas. +bool native_ui_init(SDL_Window *window); + +// Feed every SDL event here, whether or not the native view is on screen. +void native_ui_handle_event(const SDL_Event *event); + +// Draws one frame. Returns true if the user asked to open the web view. +bool native_ui_frame(SDL_Window *window); + +// The nonce this sign-in attempt was started with. A fresh one each time, and +// the redirect has to carry it back or the result is thrown away. +const char *native_ui_new_state(void); +const char *native_ui_state(void); + +// What came back from the login flow: the captured code, or an error. +void native_ui_set_result(const char *text); + +void native_ui_shutdown(void); + +#ifdef __cplusplus +} +#endif + +#endif // WEB_HYBRID_NATIVE_UI_H_ diff --git a/web/hybrid/page.html b/web/hybrid/page.html new file mode 100644 index 0000000..6f3dc78 --- /dev/null +++ b/web/hybrid/page.html @@ -0,0 +1,70 @@ + + +sign in + +
+

Example Provider

+
+
+ + +
+
+ + +
+
+ + +
+
+

+
+ diff --git a/web/libcbe/CMakeLists.txt b/web/libcbe/CMakeLists.txt new file mode 100644 index 0000000..a937124 --- /dev/null +++ b/web/libcbe/CMakeLists.txt @@ -0,0 +1,38 @@ +# The libcbe ABI: reconstructed headers, and a link stub to resolve them against. +# +# Shared by every sample under web/, the way media/common is shared by the media +# ones. Nothing here talks to libcbe - it only describes it. +# +# The NDK has no libcbe, so this builds something with the right SONAME to link +# against. libcbe.so carries no version suffix on the TV, and the SONAME is what +# makes the loader pick the real 70 MB library up at runtime, so there is +# deliberately no SOVERSION here. It is never installed. +# +# The stub is written as ordinary C++ against the same headers the samples use, +# rather than as a hand-written list of mangled names, so the two cannot drift. + +add_library(cbe-stub SHARED cbe_stub.cpp) +set_target_properties(cbe-stub PROPERTIES OUTPUT_NAME cbe) +target_include_directories(cbe-stub PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") + +# Chromium is built without RTTI and exports no typeinfo for these classes, so +# anything deriving from them has to agree - hence PUBLIC, not PRIVATE. +target_compile_options(cbe-stub PUBLIC -fno-rtti) + +# The same again for webOS 3, where libcbe is a different API and not merely a +# different string ABI: no Initialize(), the constructor takes the size, and the +# delegate has its own slot order. See webos3/webos/webview_base.h. +# +# _GLIBCXX_USE_CXX11_ABI=0 is PUBLIC because it has to reach anything that links +# this - it is what makes std::string mangle the way that generation's libcbe +# expects, and getting it wrong produces a binary that links and then finds +# nothing on the TV. +add_library(cbe-stub-webos3 SHARED webos3/cbe_stub.cpp) +set_target_properties(cbe-stub-webos3 PROPERTIES OUTPUT_NAME cbe) +target_include_directories(cbe-stub-webos3 PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/webos3") +target_compile_options(cbe-stub-webos3 PUBLIC -fno-rtti) +target_compile_definitions(cbe-stub-webos3 PUBLIC _GLIBCXX_USE_CXX11_ABI=0) + +# Both stubs are named libcbe.so, so they need separate build directories. +set_target_properties(cbe-stub-webos3 PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/webos3") diff --git a/web/libcbe/cbe_stub.cpp b/web/libcbe/cbe_stub.cpp new file mode 100644 index 0000000..4f91e09 --- /dev/null +++ b/web/libcbe/cbe_stub.cpp @@ -0,0 +1,92 @@ +// Link-time stand-in for the TV's /usr/lib/libcbe.so. +// +// The NDK has no libcbe and no stub for it, so the build makes one: a shared +// object with the right SONAME and the right symbol names, and nothing else. +// The loader picks up the TV's real 70 MB library at runtime because the SONAME +// matches; this file is never installed. +// +// It is written as ordinary C++ against the same headers the sample uses, rather +// than as a hand-written list of mangled names, so the two cannot drift apart. +// If a declaration changes, the stub changes with it. + +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +#define STUB(...) \ + { return __VA_ARGS__; } + +extern "C" int WebOSMain(int, const char**) STUB(0) + +namespace webos { + +WebViewBase::WebViewBase() : webview_(0) STUB() +WebViewBase::~WebViewBase() STUB() + +void WebViewBase::Initialize(const std::string&, const std::string&, const std::string&, + const std::string&, const std::string&, int, int, bool) STUB() +void* WebViewBase::GetWebContents() STUB(0) +void WebViewBase::LoadUrl(const std::string&) STUB() +void WebViewBase::StopLoading() STUB() +void WebViewBase::Reload() STUB() +bool WebViewBase::CanGoBack() const STUB(false) +std::string WebViewBase::GetUrl() STUB(std::string()) +std::string WebViewBase::DocumentTitle() const STUB(std::string()) +std::string WebViewBase::DefaultUserAgent() const STUB(std::string()) +void WebViewBase::RunJavaScript(const std::string&) STUB() +void WebViewBase::RunJavaScriptInAllFrames(const std::string&) STUB() +void WebViewBase::LoadExtension(const std::string&) STUB() +void WebViewBase::ClearExtensions() STUB() +void WebViewBase::AddUserStyleSheet(const std::string&) STUB() +void WebViewBase::ForwardWebOSEvent(WebOSEvent*) STUB() +void WebViewBase::EnableInspectablePage() STUB() +void WebViewBase::UpdatePreferences() STUB() +void WebViewBase::SuspendPaintingAndSetVisibilityHidden() STUB() +void WebViewBase::ResumePaintingAndSetVisibilityVisible() STUB() +void WebViewBase::SuspendWebPageDOM() STUB() +void WebViewBase::ResumeWebPageDOM() STUB() +void WebViewBase::SuspendWebPageMedia() STUB() +void WebViewBase::ResumeWebPageMedia() STUB() +void WebViewBase::SetAppId(const std::string&) STUB() +void WebViewBase::SetAppPath(const std::string&) STUB() +void WebViewBase::SetTrustLevel(const std::string&) STUB() +void WebViewBase::SetUserAgent(const std::string&) STUB() +void WebViewBase::SetVisible(bool) STUB() +void WebViewBase::SetFocus(bool) STUB() +void WebViewBase::SetViewportSize(int, int) STUB() +void WebViewBase::SetHardwareResolution(int, int) STUB() +void WebViewBase::SetTransparentBackground(bool) STUB() +void WebViewBase::SetBackgroundColor(int, int, int, int) STUB() +void WebViewBase::SetAllowLocalResourceLoad(bool) STUB() +void WebViewBase::SetAllowUniversalAccessFromFileUrls(bool) STUB() +void WebViewBase::SetFileAccessBlocked(bool) STUB() +void WebViewBase::SetWebSecurityEnabled(bool) STUB() +void WebViewBase::SetLocalStorageEnabled(bool) STUB() +void WebViewBase::SetJavascriptCanOpenWindows(bool) STUB() +void WebViewBase::SetSupportsMultipleWindows(bool) STUB() +void WebViewBase::SetShouldSuppressDialogs(bool) STUB() +void WebViewBase::SetDisallowScrollingInMainFrame(bool) STUB() +void WebViewBase::SetVisibilityState(WebPageVisibilityState) STUB() +void WebViewBase::SetFontHinting(FontRenderParams) STUB() + +WebAppWindowBase::WebAppWindowBase() : webapp_window_(0) STUB() +WebAppWindowBase::~WebAppWindowBase() STUB() +void WebAppWindowBase::InitWindow(int, int) STUB() +void WebAppWindowBase::Show() STUB() +void WebAppWindowBase::Hide() STUB() +void WebAppWindowBase::SetCustomCursor(CustomCursorType, const std::string&, int, int) STUB() +void WebAppWindowBase::AttachWebContents(void*) STUB() +void WebAppWindowBase::DetachWebContents() STUB() +void WebAppWindowBase::RecreatedWebContents() STUB() +void WebAppWindowBase::Activate() STUB() +void WebAppWindowBase::Deactivate() STUB() +void WebAppWindowBase::Resize(int, int) STUB() +void WebAppWindowBase::SetOpacity(float) STUB() +void WebAppWindowBase::SetWindowHostState(NativeWindowState) STUB() +NativeWindowState WebAppWindowBase::GetWindowHostState() const STUB(NATIVE_WINDOW_DEFAULT) +void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() +void WebAppWindowBase::SetUseVirtualKeyboard(bool) STUB() +void WebAppWindowBase::SetKeyMask(WebOSKeyMask, bool) STUB() +int WebAppWindowBase::DisplayWidth() STUB(0) +int WebAppWindowBase::DisplayHeight() STUB(0) + +} // namespace webos diff --git a/web/libcbe/neva/cbe_stub.cpp b/web/libcbe/neva/cbe_stub.cpp new file mode 100644 index 0000000..dc288a5 --- /dev/null +++ b/web/libcbe/neva/cbe_stub.cpp @@ -0,0 +1,30 @@ +// Link-time stand-in for the neva_app_runtime half of libcbe. +#include "neva_app_runtime/webapp_window_base.h" +#include "neva_app_runtime/webview_base.h" + +#define STUB(...) \ + { return __VA_ARGS__; } + +int AppRuntimeMain(int, const char**) STUB(0) + +namespace neva_app_runtime { + +WebViewBase::WebViewBase(int, int, WebViewProfile*) : webview_(0) STUB() +WebViewBase::~WebViewBase() STUB() +void* WebViewBase::GetWebContents() STUB(0) +void WebViewBase::LoadUrl(const std::string&) STUB() +void WebViewBase::SetAppId(const std::string&) STUB() +void WebViewBase::SetVisible(bool) STUB() + +WebAppWindowBase::WebAppWindowBase() : webapp_window_(0) STUB() +WebAppWindowBase::~WebAppWindowBase() STUB() +void WebAppWindowBase::SetBounds(int, int, int, int) STUB() +void WebAppWindowBase::Show() STUB() +void WebAppWindowBase::Hide() STUB() +void WebAppWindowBase::Activate() STUB() +void WebAppWindowBase::Resize(int, int) STUB() +void WebAppWindowBase::AttachWebContents(void*) STUB() +void WebAppWindowBase::SetWindowHostState(WidgetState) STUB() +void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() + +} // namespace neva_app_runtime diff --git a/web/libcbe/neva/neva_app_runtime/webapp_window_base.h b/web/libcbe/neva/neva_app_runtime/webapp_window_base.h new file mode 100644 index 0000000..5248aa8 --- /dev/null +++ b/web/libcbe/neva/neva_app_runtime/webapp_window_base.h @@ -0,0 +1,50 @@ +// neva_app_runtime::WebAppWindowBase. Five virtual slots, none of them pure - +// libcbe implements every one, so a subclass that overrides nothing is valid. +#pragma once + +#include + +namespace neva_app_runtime { + +enum WidgetState { + UNINITIALIZED = 0, + CREATED, + SHOW, + HIDE, + FULLSCREEN, + MAXIMIZED, + MINIMIZED, + RESTORE, + ACTIVE, + INACTIVE, + RESIZE, + DESTROYED, +}; + +class AppRuntimeEvent; + +class WebAppWindowBase { + public: + WebAppWindowBase(); + virtual ~WebAppWindowBase(); + + virtual void OnWindowClosing() {} + virtual void CursorVisibilityChanged(bool visible) { (void)visible; } + virtual bool event(AppRuntimeEvent* e) { (void)e; return false; } + + // No InitWindow() on this API - Resize gives the window its size. + void Resize(int width, int height); + void SetBounds(int x, int y, int width, int height); + void Show(); + void Hide(); + void Activate(); + void AttachWebContents(void* web_contents); + void SetWindowHostState(WidgetState state); + void SetWindowProperty(const std::string& name, const std::string& value); + + private: + // libcbe's constructor writes as far as offset 4. + void* webapp_window_; +}; + +} // namespace neva_app_runtime diff --git a/web/libcbe/neva/neva_app_runtime/webview_base.h b/web/libcbe/neva/neva_app_runtime/webview_base.h new file mode 100644 index 0000000..749f5b3 --- /dev/null +++ b/web/libcbe/neva/neva_app_runtime/webview_base.h @@ -0,0 +1,150 @@ +// neva_app_runtime - the API LG introduced alongside webos:: on webOS 6. +// +// Unlike every other header under web/libcbe, this one starts from *published* +// source: webosose/chromium87, src/neva/app_runtime/public/webview_delegate.h. +// It is still not a drop-in, because the TV is Chromium 79 - between the public +// chromium68 and chromium87 trees - and its vtable has 40 slots where +// chromium87 declares 32 virtuals and chromium68 declares 26. +// +// What makes the upstream order trustworthy anyway is a cross-check that does +// not depend on names at all. libcbe's own vtable says which slots are pure +// virtual (they relocate to __cxa_pure_virtual) and which carry a default +// implementation, and that pattern is: +// +// firmware pure at: 0-15, 18, 19, 20, 21, 29 +// upstream pure at: 0-15, 18, 19, 20, 21 +// +// Identical through slot 28. So slots 0-28 are upstream's, in upstream's order, +// and 29-39 are LG additions this header leaves as placeholders. +// +// Measured against a 65UP7560 running starfish 6.5.2. +#pragma once + +#include +#include + +namespace neva_app_runtime { + +class WebViewProfile; + +enum DropPeerConnectionReason { + DROP_PEER_CONNECTION_REASON_UNKNOWN, + DROP_PEER_CONNECTION_REASON_PAGE_HIDDEN, + DROP_PEER_CONNECTION_REASON_MULTIMEDIA_PLAYING, +}; + +class WebViewDelegate { + public: + // slot 0 + virtual void OnLoadProgressChanged(double progress) {} + // slot 1 + virtual void DidFirstFrameFocused() {} + // slot 2 + virtual void TitleChanged(const std::string& title) {} + // slot 3 + virtual void NavigationHistoryChanged() {} + // slot 4 + virtual void Close() {} + // slot 5 + virtual bool DecidePolicyForResponse(bool is_main_frame, int status_code, const std::string& url, const std::string& status_text) { return false; } + // slot 6 + virtual bool AcceptsVideoCapture() { return false; } + // slot 7 + virtual bool AcceptsAudioCapture() { return false; } + // slot 8 + virtual void LoadStarted() {} + // slot 9 + virtual void LoadFinished(const std::string& url) {} + // slot 10 + virtual void LoadFailed(const std::string& url, int error_code, const std::string& error_description) {} + // slot 11 + virtual void LoadAborted(const std::string& url) {} + // slot 12 + virtual void LoadStopped() {} + // slot 13 + virtual void RenderProcessCreated(int pid) {} + // slot 14 + virtual void RenderProcessGone() {} + // slot 15 + virtual void DocumentLoadFinished() {} + // slot 16 + virtual void DidStartNavigation(const std::string& url, bool is_main_frame) {} + // slot 17 + virtual void DidFinishNavigation(const std::string& url, bool is_main_frame) {} + // slot 18 + virtual void DidHistoryBackOnTopPage() {} + // slot 19 + virtual void DidClearWindowObject() {} + // slot 20 + virtual void DidSwapCompositorFrame() {} + // slot 21 + virtual void DidErrorPageLoadedFromNetErrorHelper() {} + // slot 22 + virtual void DidLoadingEnd() {} + // slot 23 + virtual void DidFirstPaint() {} + // slot 24 + virtual void DidFirstContentfulPaint() {} + // slot 25 + virtual void DidFirstImagePaint() {} + // slot 26 + virtual void DidFirstMeaningfulPaint() {} + // slot 27 + virtual void DidNonFirstMeaningfulPaint() {} + // slot 28 + virtual void DidLargestContentfulPaint() {} + // slot 29 + virtual void DidDropAllPeerConnections(neva_app_runtime::DropPeerConnectionReason reason) {} + // slot 30 + virtual void DidResumeDOM() {} + // slot 31 + virtual void SendCookiesForHostname(const std::string& cookies) {} + // slot 32 - beyond what upstream declares; LG additions. + virtual void Unknown32() {} + // slot 33 - beyond what upstream declares; LG additions. + virtual void Unknown33() {} + // slot 34 - beyond what upstream declares; LG additions. + virtual void Unknown34() {} + // slot 35 - beyond what upstream declares; LG additions. + virtual void Unknown35() {} + // slot 36 - beyond what upstream declares; LG additions. + virtual void Unknown36() {} + // slot 37 - beyond what upstream declares; LG additions. + virtual void Unknown37() {} + // slot 38 - beyond what upstream declares; LG additions. + virtual void Unknown38() {} + // slot 39 - beyond what upstream declares; LG additions. + virtual void Unknown39() {} +}; + +class WebViewBase : public WebViewDelegate { + public: + // No Initialize() on this API - the constructor takes the size, and a null + // profile means the default one. + WebViewBase(int width, int height, WebViewProfile* profile); + ~WebViewBase(); + + // Virtual, and libcbe implements it - declared with no body here so the slot + // resolves to libcbe's own symbol rather than being shadowed. Stubbing a slot + // that returns a pointer is what breaks these bindings; see + // ../../../cbe-webos6/README.md. + void* GetWebContents(); + void LoadUrl(const std::string& url); + void SetAppId(const std::string& app_id); + void SetVisible(bool visible); + + private: + // libcbe's constructor writes as far as offset 12, so 16 bytes. + void* webview_; + char reserved_[8]; +}; + +} // namespace neva_app_runtime + +// The entry point. A free function on this API, where webos:: has a WebOSMain +// class - and Chromium's content main either way: it does not return. +// +// Deliberately NOT extern "C": libcbe exports it C++-mangled, as +// _Z14AppRuntimeMainiPPKc, so an extern "C" declaration looks for a plain +// "AppRuntimeMain" that does not exist. +int AppRuntimeMain(int argc, const char** argv); diff --git a/web/libcbe/webos/webapp_window_base.h b/web/libcbe/webos/webapp_window_base.h new file mode 100644 index 0000000..f63fe3c --- /dev/null +++ b/web/libcbe/webos/webapp_window_base.h @@ -0,0 +1,67 @@ +// webos::WebAppWindowBase - the LSM/Wayland window libcbe paints a WebViewBase +// into. Same reconstruction caveats as webview_base.h. +// +// Layout that must hold: WebAppWindowDelegate DOES have a virtual destructor +// (slots 0 and 1), event() is slot 2, and the object is 8 bytes total. +#pragma once + +#include + +class WebOSEvent; + +namespace webos { + +enum NativeWindowState { + NATIVE_WINDOW_DEFAULT = 0, + NATIVE_WINDOW_MINIMIZED, + NATIVE_WINDOW_MAXIMIZED, + NATIVE_WINDOW_FULLSCREEN, +}; + +enum CustomCursorType { CUSTOM_CURSOR_NOT_USE, CUSTOM_CURSOR_BLANK, CUSTOM_CURSOR_PATH }; + +typedef unsigned WebOSKeyMask; + +// libcbe does not export this class's own members (webOS 4 and older do not +// export it at all), so the defaults live here. They only ever fill vtable slots +// in *our* subclass; libcbe's WebAppWindowBase keeps its internal ones. +class WebAppWindowDelegate { + public: + virtual ~WebAppWindowDelegate() {} + virtual bool event(WebOSEvent*) { return false; } + // unsigned, not bool - the SDK's webos/webapp_window_delegate.h says so. + virtual unsigned CheckKeyFilterTable(unsigned, unsigned*) { return 0; } +}; + +class WebAppWindowBase : public WebAppWindowDelegate { + public: + WebAppWindowBase(); + ~WebAppWindowBase() override; + + void InitWindow(int width, int height); + + virtual void Show(); + virtual void Hide(); + virtual void SetCustomCursor(CustomCursorType type, const std::string& path, + int hotspot_x, int hotspot_y); + virtual void AttachWebContents(void* web_contents); + virtual void DetachWebContents(); + virtual void RecreatedWebContents(); + + void Activate(); + void Deactivate(); + void Resize(int width, int height); + void SetOpacity(float opacity); + void SetWindowHostState(NativeWindowState state); + NativeWindowState GetWindowHostState() const; + void SetWindowProperty(const std::string& name, const std::string& value); + void SetUseVirtualKeyboard(bool enable); + void SetKeyMask(WebOSKeyMask key_mask, bool set); + int DisplayWidth(); + int DisplayHeight(); + + private: + void* webapp_window_; // WebAppWindow*, owned by libcbe +}; + +} // namespace webos diff --git a/web/libcbe/webos/webview_base.h b/web/libcbe/webos/webview_base.h new file mode 100644 index 0000000..995ca4b --- /dev/null +++ b/web/libcbe/webos/webview_base.h @@ -0,0 +1,172 @@ +// webos::WebViewBase - the Chromium embedding surface exported by the TV's +// /usr/lib/libcbe.so ("Chromium Browser Engine"). +// +// This is not an SDK header. There is none: libcbe ships as a stripped 70 MB +// blob with a C++ ABI and no headers anywhere on the device or in the NDK. What +// is here was reconstructed from two sources that agree with each other - the +// firmware symbol tables, which give every name and signature, and the vtables +// of WAM's own BlinkWebView, which is the only in-firmware subclass of this +// class and therefore pins down the slot order. +// +// Three things are load-bearing and must not be tidied up: +// +// * WebViewDelegate has NO virtual destructor. Adding one shifts every slot +// by two and libcbe will call the wrong function. +// * The delegate is 24 slots long. Only the first 17 have recoverable names; +// the rest are no-ops in WAM too, but they must exist, because libcbe +// indexes past slot 17 and a short vtable reads whatever follows it in +// memory. Leaving them out is a segfault a few hundred milliseconds into +// the first page load, which is exactly how they were found. +// * WebViewBase adds no virtuals and exactly one pointer member, so the whole +// object is 8 bytes - the size libcbe's own constructor assumes. +#pragma once + +#include +#include + +class WebOSEvent; + +namespace webos { + +class WebViewProfile; + +class WebViewDelegate { + public: + virtual void LoadProgressChanged(double progress) = 0; + virtual void DidFirstFrameFocused() = 0; + virtual void LoadVisuallyCommitted() = 0; + virtual void TitleChanged(const std::string& title) = 0; + virtual void NavigationHistoryChanged() = 0; + virtual void Close() = 0; + virtual bool DecidePolicyForResponse(bool is_main_frame, + int status_code, + const std::string& url, + const std::string& status_text) = 0; + virtual void LoadStarted() = 0; + virtual void LoadStopped() = 0; + virtual void DidStartNavigation(const std::string& url, bool is_main_frame) = 0; + virtual void DidFinishNavigation(const std::string& url, bool is_main_frame) = 0; + virtual void LoadFinished(const std::string& url) = 0; + virtual void LoadFailed(const std::string& url, + int err_code, + const std::string& err_desc) = 0; + virtual void LoadAborted(const std::string& url) = 0; + virtual void DocumentLoadFinished() = 0; + virtual void RenderProcessCreated(int pid) = 0; + virtual void RenderProcessGone() = 0; + + // Slots 17 to 23. Two are named in WAM's binary - the browser-control bridge + // behind window.PalmSystem - and the names of the rest come from the webOS OSE + // SDK's own webos/webview_delegate.h, which is chromium53 where the TV is + // chromium68. That header is a naming reference, not the ABI: its version of + // this class has AcceptsVideoCapture/AcceptsAudioCapture where webOS 4 has + // DidStartNavigation, DidFinishNavigation and LoadAborted, so the slot numbers + // below still come from the vtable rather than from it. + // + // DidClearWindowObject is not a guess: slot 18 fires twice during a page load, + // which is exactly when the window object is recreated. + virtual void DidHistoryBackOnTopPage() {} + virtual void DidClearWindowObject() {} + virtual void HandleBrowserControlCommand( + const std::string& command, const std::vector& arguments) { + (void)command; + (void)arguments; + } + virtual void HandleBrowserControlFunction( + const std::string& command, + const std::vector& arguments, + std::string* result) { + (void)command; + (void)arguments; + (void)result; + } + // Beyond 20 the mapping is unconfirmed. The SDK header offers + // DidDropAllPeerConnections(reason), AllowMouseOnOffEvent() and + // SendCookiesForHostname(), and also declares ~WebViewBase() virtual - so one + // of these pairs may be the destructor slots instead. They are kept as + // no-argument stubs because nothing here calls them; AllowMouseOnOffEvent + // returns bool rather than void so a caller that reads the result gets a + // definite answer instead of whatever was in r0. + virtual void DidDropAllPeerConnections() {} + virtual bool AllowMouseOnOffEvent() const { return false; } + virtual void SendCookiesForHostname() {} +}; + +class WebViewBase : public WebViewDelegate { + public: + enum FontRenderParams { HINTING_NONE, HINTING_SLIGHT, HINTING_MEDIUM, HINTING_FULL }; + enum MemoryPressureLevel { + MEMORY_PRESSURE_NONE, + MEMORY_PRESSURE_LOW, + MEMORY_PRESSURE_CRITICAL + }; + enum WebPageVisibilityState { VISIBILITY_VISIBLE, VISIBILITY_HIDDEN, VISIBILITY_LAUNCHING }; + + WebViewBase(); + ~WebViewBase(); + + // The last three arguments are the appinfo.json "width", "height" and + // "useNativeScroll" keys - that is literally where WAM reads them from. + void Initialize(const std::string& app_id, + const std::string& app_path, + const std::string& trust_level, + const std::string& v8_snapshot_path, + const std::string& v8_extra_flags, + int width, + int height, + bool use_native_scroll); + + void* GetWebContents(); + void LoadUrl(const std::string& url); + void StopLoading(); + void Reload(); + bool CanGoBack() const; + std::string GetUrl(); + std::string DocumentTitle() const; + std::string DefaultUserAgent() const; + void RunJavaScript(const std::string& js); + void RunJavaScriptInAllFrames(const std::string& js); + // Loads one of libcbe's built-in V8 injections by name ("v8/palmsystem", + // "v8/netcast", ...). An injection is what gives page JavaScript something to + // call that reaches native code: its native functions land in the browser + // process as HandleBrowserControlCommand / HandleBrowserControlFunction. + void LoadExtension(const std::string& name); + void ClearExtensions(); + void AddUserStyleSheet(const std::string& css); + void ForwardWebOSEvent(WebOSEvent* event); + void EnableInspectablePage(); + void UpdatePreferences(); + void SuspendPaintingAndSetVisibilityHidden(); + void ResumePaintingAndSetVisibilityVisible(); + void SuspendWebPageDOM(); + void ResumeWebPageDOM(); + void SuspendWebPageMedia(); + void ResumeWebPageMedia(); + + void SetAppId(const std::string& app_id); + void SetAppPath(const std::string& app_path); + void SetTrustLevel(const std::string& trust_level); + void SetUserAgent(const std::string& user_agent); + void SetVisible(bool visible); + void SetFocus(bool focus); + void SetViewportSize(int width, int height); + void SetHardwareResolution(int width, int height); + void SetTransparentBackground(bool transparent); + void SetBackgroundColor(int r, int g, int b, int a); + void SetAllowLocalResourceLoad(bool allow); + void SetAllowUniversalAccessFromFileUrls(bool allow); + void SetFileAccessBlocked(bool blocked); + void SetWebSecurityEnabled(bool enabled); + void SetLocalStorageEnabled(bool enabled); + void SetJavascriptCanOpenWindows(bool allow); + void SetSupportsMultipleWindows(bool support); + void SetShouldSuppressDialogs(bool suppress); + void SetDisallowScrollingInMainFrame(bool disallow); + void SetVisibilityState(WebPageVisibilityState state); + void SetFontHinting(FontRenderParams hinting); + + private: + void* webview_; // WebView*, owned by libcbe +}; + +} // namespace webos diff --git a/web/libcbe/webos3/cbe_stub.cpp b/web/libcbe/webos3/cbe_stub.cpp new file mode 100644 index 0000000..b6a660b --- /dev/null +++ b/web/libcbe/webos3/cbe_stub.cpp @@ -0,0 +1,71 @@ +// Link-time stand-in for webOS 3's /usr/lib/libcbe.so. Same idea as the webOS 4 +// stub next door, compiled against the webOS 3 headers and with the pre-C++11 +// std::string ABI, so the mangled names come out matching that generation. +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +#define STUB(...) \ + { return __VA_ARGS__; } + +extern "C" int WebOSMain(int, const char**) STUB(0) + +namespace base { +FilePath::FilePath(const std::string& p) : path_(p) STUB() +FilePath::~FilePath() STUB() +} // namespace base + +namespace webos { + +Runtime* Runtime::Get() STUB(0) +void Runtime::InitializePlatform(const base::FilePath&) STUB() +void Runtime::Initialize(PlatformDelegate*) STUB() +void Runtime::SetWindowSize(int, int) STUB() + +WebViewBase::WebViewBase(int, int) : webview_(0) STUB() +WebViewBase::~WebViewBase() STUB() + +void* WebViewBase::GetWebContents() STUB(0) +void WebViewBase::LoadUrl(const std::string&) STUB() +void WebViewBase::StopLoading() STUB() +void WebViewBase::Reload() STUB() +std::string WebViewBase::GetUrl() STUB(std::string()) +std::string WebViewBase::DocumentTitle() const STUB(std::string()) +void WebViewBase::RunJavaScript(const std::string&) STUB() +void WebViewBase::LoadExtension(const std::string&) STUB() +void WebViewBase::ClearExtensions() STUB() +void WebViewBase::SetAppId(const std::string&) STUB() +void WebViewBase::SetUserAgent(const std::string&) STUB() +void WebViewBase::SetVisible(bool) STUB() +void WebViewBase::SetViewportSize(int, int) STUB() +void WebViewBase::SetTransparentBackground(bool) STUB() +void WebViewBase::SetAllowLocalResourceLoad(bool) STUB() +void WebViewBase::SetAllowUniversalAccessFromFileUrls(bool) STUB() +void WebViewBase::SetFileAccessBlocked(bool) STUB() +void WebViewBase::SetLocalStorageEnabled(bool) STUB() +void WebViewBase::SetShouldSuppressDialogs(bool) STUB() +void WebViewBase::SetVisibilityState(WebPageVisibilityState) STUB() + +WebAppWindowBase::WebAppWindowBase() : webapp_window_(0) STUB() +WebAppWindowBase::~WebAppWindowBase() STUB() +void WebAppWindowBase::Show() STUB() +void WebAppWindowBase::Hide() STUB() +void WebAppWindowBase::SetCustomCursor(CustomCursorType, const std::string&, int, int) STUB() +void WebAppWindowBase::SetHiddenState(bool) STUB() +void WebAppWindowBase::FirstFrameVisuallyCommitted() STUB() +void* WebAppWindowBase::GetNativeWindow() STUB(0) +void WebAppWindowBase::AttachWebContents(void*) STUB() +void WebAppWindowBase::DetachWebContents() STUB() +void WebAppWindowBase::RecreatedWebContents() STUB() +void WebAppWindowBase::Resize(int, int) STUB() +void WebAppWindowBase::SetOpacity(float) STUB() +void WebAppWindowBase::SetScaleFactor(float) STUB() +void WebAppWindowBase::SetWindowHostState(NativeWindowState) STUB() +NativeWindowState WebAppWindowBase::GetWindowHostState() const STUB(NATIVE_WINDOW_DEFAULT) +void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() +void WebAppWindowBase::SetUseVirtualKeyboard(bool) STUB() +void WebAppWindowBase::SetKeyMask(WebOSKeyMask, bool) STUB() +unsigned WebAppWindowBase::GetWindowHandle() STUB(0) +int WebAppWindowBase::DisplayWidth() STUB(0) +int WebAppWindowBase::DisplayHeight() STUB(0) + +} // namespace webos diff --git a/web/libcbe/webos3/webos/webapp_window_base.h b/web/libcbe/webos3/webos/webapp_window_base.h new file mode 100644 index 0000000..4d77caa --- /dev/null +++ b/web/libcbe/webos3/webos/webapp_window_base.h @@ -0,0 +1,67 @@ +// webos::WebAppWindowBase on webOS 3. Thirteen virtual slots against webOS 4's +// ten, and no InitWindow() - the window exists once the object does, and Resize +// gives it a size. +#pragma once + +#include + +class WebOSEvent; + +namespace webos { + +enum NativeWindowState { + NATIVE_WINDOW_DEFAULT = 0, + NATIVE_WINDOW_MINIMIZED, + NATIVE_WINDOW_MAXIMIZED, + NATIVE_WINDOW_FULLSCREEN, +}; + +enum CustomCursorType { CUSTOM_CURSOR_NOT_USE, CUSTOM_CURSOR_BLANK, CUSTOM_CURSOR_PATH }; + +typedef unsigned WebOSKeyMask; + +class WebAppWindowDelegate { + public: + virtual ~WebAppWindowDelegate() {} + virtual bool event(WebOSEvent*) { return false; } + virtual unsigned CheckKeyFilterTable(unsigned, unsigned*) { return 0; } +}; + +class WebAppWindowBase : public WebAppWindowDelegate { + public: + WebAppWindowBase(); + ~WebAppWindowBase() override; + + // Slots 4 to 12, in this order. SetHiddenState, FirstFrameVisuallyCommitted + // and GetNativeWindow sit in the middle of them on this generation, where + // webOS 4 has nothing. + virtual void Show(); + virtual void Hide(); + virtual void SetCustomCursor(CustomCursorType type, const std::string& path, + int hotspot_x, int hotspot_y); + virtual void SetHiddenState(bool hidden); + virtual void FirstFrameVisuallyCommitted(); + virtual void* GetNativeWindow(); + virtual void AttachWebContents(void* web_contents); + virtual void DetachWebContents(); + virtual void RecreatedWebContents(); + + void Resize(int width, int height); + void SetOpacity(float opacity); + // WAM calls this; a window whose scale is left at zero has nothing to render. + void SetScaleFactor(float scale); + void SetWindowHostState(NativeWindowState state); + NativeWindowState GetWindowHostState() const; + void SetWindowProperty(const std::string& name, const std::string& value); + void SetUseVirtualKeyboard(bool enable); + void SetKeyMask(WebOSKeyMask key_mask, bool set); + // The handle the GPU side uses to make an accelerated surface for this window. + unsigned GetWindowHandle(); + int DisplayWidth(); + int DisplayHeight(); + + private: + void* webapp_window_; // WebAppWindow*, owned by libcbe +}; + +} // namespace webos diff --git a/web/libcbe/webos3/webos/webview_base.h b/web/libcbe/webos3/webos/webview_base.h new file mode 100644 index 0000000..f1e0757 --- /dev/null +++ b/web/libcbe/webos3/webos/webview_base.h @@ -0,0 +1,163 @@ +// webos::WebViewBase as it exists on webOS 3 - a different API from the one in +// ../../webos/webview_base.h, not just a different string ABI. +// +// Recovered the same way: the firmware symbol tables for the names, and the +// vtable of BlinkWebView inside webOS 3's own libWebAppMgr.so for the slot +// order. Measured against a 43UH6100 running starfish 3.4.0. +// +// What moved between webOS 3 and 4: +// +// * the whole library is the pre-C++11 std::string ABI, so everything here +// must be compiled -D_GLIBCXX_USE_CXX11_ABI=0; +// * there is no Initialize() at all - the constructor takes the dimensions; +// * DidFirstNonBlankPaint exists and DidStartNavigation does not, so a +// redirect has to be caught in LoadStarted, which carries the URL here; +// * LoadStarted and LoadStopped take a URL, and LoadProgressChanged takes one +// besides the progress; +// * the browser-control pair sits at 18/19 rather than 19/20. +#pragma once + +#include +#include + +class WebOSEvent; + +namespace webos { + +class WebViewProfile; + +} // namespace webos + +// Chromium's own path type, exported by libcbe. Layout is a single std::string, +// which is what makes it safe to declare here. +namespace base { +class FilePath { + public: + explicit FilePath(const std::string& path); + ~FilePath(); + + private: + std::string path_; +}; +} // namespace base + +namespace webos { + +// Recovered from the exported vtable of webos::PlatformDelegate: two destructor +// slots followed by nine pure virtuals, all of them __cxa_pure_virtual in the +// base. The names and signatures are unknown - nothing in the firmware +// implements this class - so these are placeholders with the right *shape*. +// Enough to hand Runtime::Initialize something it will accept. +class PlatformDelegate { + public: + virtual ~PlatformDelegate() {} + virtual void Unknown2() {} + virtual void Unknown3() {} + virtual void Unknown4() {} + virtual void Unknown5() {} + virtual void Unknown6() {} + virtual void Unknown7() {} + virtual void Unknown8() {} + virtual void Unknown9() {} + virtual void Unknown10() {} +}; + +// A singleton libcbe keeps for platform-wide state. Unlike webos::Platform, +// which belongs to the browser application, this one may exist in a plain +// embedder - and the window size lives here too. +class Runtime { + public: + static Runtime* Get(); + void SetWindowSize(int width, int height); + // Suspected to be what builds webos::Platform - the layer that owns the Luna + // side, and which is null in a plain embedder. + void InitializePlatform(const base::FilePath& path); + void Initialize(PlatformDelegate* delegate); +}; + + +// 24 slots. Names for 0-14 and 18-19 come from BlinkWebView's vtable; the rest +// are stubs it fills with empty bodies, and must be present or libcbe indexes +// past the end of ours. +class WebViewDelegate { + public: + virtual void LoadProgressChanged(double progress, const std::string& url) = 0; + virtual void DidFirstFrameFocused() = 0; + virtual void DidFirstNonBlankPaint() = 0; + virtual void LoadVisuallyCommitted() = 0; + virtual void TitleChanged(const std::string& title) = 0; + virtual void NavigationHistoryChanged() = 0; + virtual void Close() = 0; + virtual bool DecidePolicyForResponse(bool is_main_frame, + int status_code, + const std::string& url, + const std::string& status_text) = 0; + // Slot 8. The nearest thing webOS 3 has to DidStartNavigation, and the hook a + // redirect-catching login flow has to use here. + virtual void LoadStarted(const std::string& url) = 0; + virtual void LoadFinished(const std::string& url) = 0; + virtual void LoadFailed(const std::string& url, + int err_code, + const std::string& err_desc) = 0; + virtual void LoadStopped(const std::string& url) = 0; + virtual void RenderProcessCreated(int pid) = 0; + virtual void RenderProcessGone() = 0; + virtual void DocumentLoadFinished() = 0; + virtual void Unknown15() {} + virtual void Unknown16() {} + virtual void Unknown17() {} + virtual void HandleBrowserControlCommand( + const std::string& command, const std::vector& arguments) { + (void)command; + (void)arguments; + } + virtual void HandleBrowserControlFunction( + const std::string& command, + const std::vector& arguments, + std::string* result) { + (void)command; + (void)arguments; + (void)result; + } + virtual void Unknown20() {} + virtual void Unknown21() {} + virtual void Unknown22() {} + virtual void Unknown23() {} +}; + +class WebViewBase : public WebViewDelegate { + public: + enum FontRenderParams { HINTING_NONE, HINTING_SLIGHT, HINTING_MEDIUM, HINTING_FULL }; + enum WebPageVisibilityState { VISIBILITY_VISIBLE, VISIBILITY_HIDDEN, VISIBILITY_LAUNCHING }; + + // No Initialize() on this generation: the size goes in here. + WebViewBase(int width, int height); + ~WebViewBase(); + + void* GetWebContents(); + void LoadUrl(const std::string& url); + void StopLoading(); + void Reload(); + std::string GetUrl(); + std::string DocumentTitle() const; + void RunJavaScript(const std::string& js); + void LoadExtension(const std::string& name); + void ClearExtensions(); + + void SetAppId(const std::string& app_id); + void SetUserAgent(const std::string& user_agent); + void SetVisible(bool visible); + void SetViewportSize(int width, int height); + void SetTransparentBackground(bool transparent); + void SetAllowLocalResourceLoad(bool allow); + void SetAllowUniversalAccessFromFileUrls(bool allow); + void SetFileAccessBlocked(bool blocked); + void SetLocalStorageEnabled(bool enabled); + void SetShouldSuppressDialogs(bool suppress); + void SetVisibilityState(WebPageVisibilityState state); + + private: + void* webview_; // WebView*, owned by libcbe +}; + +} // namespace webos diff --git a/web/libcbe/webos6/cbe_stub.cpp b/web/libcbe/webos6/cbe_stub.cpp new file mode 100644 index 0000000..61e1e23 --- /dev/null +++ b/web/libcbe/webos6/cbe_stub.cpp @@ -0,0 +1,45 @@ +// Link-time stand-in for webOS 6+'s /usr/lib/libcbe.so. Same idea as the webOS 4 +// and webOS 3 stubs: right SONAME, right mangled names, never installed. +#include "webos/webapp_window_base.h" +#include "webos/webview_base.h" + +#define STUB(...) \ + { return __VA_ARGS__; } + +namespace webos { + +WebOSMain::WebOSMain(WebOSMainDelegate*) : main_(0) STUB() +int WebOSMain::Run(int, const char**) STUB(0) + +WebViewBase::WebViewBase(bool, int, int) : webview_(0) STUB() +WebViewBase::~WebViewBase() STUB() +void WebViewBase::Initialize(const std::string&, const std::string&, const std::string&, + const std::string&, const std::string&, int, int, bool, + WebViewMode, bool) STUB() +void* WebViewBase::GetWebContents() STUB(0) +void WebViewBase::LoadUrl(const std::string&) STUB() +void WebViewBase::StopLoading() STUB() +void WebViewBase::Reload() STUB() +std::string WebViewBase::GetUrl() STUB(std::string()) +void WebViewBase::RunJavaScript(const std::string&) STUB() +void WebViewBase::UpdatePreferences() STUB() +void WebViewBase::SetAppId(const std::string&) STUB() +void WebViewBase::SetTrustLevel(const std::string&) STUB() +void WebViewBase::SetUserAgent(const std::string&) STUB() +void WebViewBase::SetVisible(bool) STUB() +void WebViewBase::SetAllowLocalResourceLoad(bool) STUB() +void WebViewBase::SetAllowUniversalAccessFromFileUrls(bool) STUB() +void WebViewBase::SetFileAccessBlocked(bool) STUB() +void WebViewBase::SetLocalStorageEnabled(bool) STUB() +void WebViewBase::SetVisibilityState(WebPageVisibilityState) STUB() + +WebAppWindowBase::WebAppWindowBase() : webapp_window_(0) STUB() +WebAppWindowBase::~WebAppWindowBase() STUB() +void WebAppWindowBase::InitWindow(int, int) STUB() +void WebAppWindowBase::Show() STUB() +void WebAppWindowBase::Activate() STUB() +void WebAppWindowBase::AttachWebContents(void*) STUB() +void WebAppWindowBase::SetWindowHostState(NativeWindowState) STUB() +void WebAppWindowBase::SetWindowProperty(const std::string&, const std::string&) STUB() + +} // namespace webos diff --git a/web/libcbe/webos6/webos/webapp_window_base.h b/web/libcbe/webos6/webos/webapp_window_base.h new file mode 100644 index 0000000..da3f963 --- /dev/null +++ b/web/libcbe/webos6/webos/webapp_window_base.h @@ -0,0 +1,83 @@ +// webos::WebAppWindowBase on webOS 6 and newer. +// +// Thirteen virtual slots, and a hierarchy that gives away where LG was heading: +// the vtable mixes webos:: methods with neva_app_runtime::WebAppWindowDelegate +// ones, so on this generation the old API sits on top of the new runtime rather +// than beside it. +// +// Slot order from WebAppWaylandWindow's vtable in a 65UP7560's libWebAppMgr.so: +// +// 0,1 destructor +// 2 OnWindowClosing (webos::WebAppWindowBase) +// 3 CursorVisibilityChanged (neva_app_runtime::WebAppWindowDelegate) +// 4 event(AppRuntimeEvent*) (neva_app_runtime::WebAppWindowDelegate) +// 5 WebAppWindowDestroyed (webos::WebAppWindowBase) +// 6 event(WebOSEvent*) +// 7 CheckKeyFilterTable +// 8,9 not recoverable +// 10 OnCreatedMediaLayer +// 11 WillDestroyAllMediaLayers +// 12 ResizedSwapBuffer +#pragma once + +#include + +class WebOSEvent; + +namespace neva_app_runtime { +class AppRuntimeEvent; +} + +namespace webos { + +enum NativeWindowState { + NATIVE_WINDOW_DEFAULT = 0, + NATIVE_WINDOW_MINIMIZED, + NATIVE_WINDOW_MAXIMIZED, + NATIVE_WINDOW_FULLSCREEN, +}; + +enum CustomCursorType { CUSTOM_CURSOR_NOT_USE, CUSTOM_CURSOR_BLANK, CUSTOM_CURSOR_PATH }; + +typedef unsigned WebOSKeyMask; + +class WebAppWindowBase { + public: + WebAppWindowBase(); + virtual ~WebAppWindowBase(); + + virtual void OnWindowClosing() {} + virtual void CursorVisibilityChanged(bool visible) { (void)visible; } + virtual void event(neva_app_runtime::AppRuntimeEvent* e) { (void)e; } + virtual void WebAppWindowDestroyed() {} + virtual bool event(WebOSEvent* e) { (void)e; return false; } + virtual unsigned CheckKeyFilterTable(unsigned keycode, unsigned* modifier) { + (void)keycode; + (void)modifier; + return 0; + } + virtual void Unknown8() {} + virtual void Unknown9() {} + virtual void OnCreatedMediaLayer(const std::string& a, const std::string& b, unsigned c) { + (void)a; + (void)b; + (void)c; + } + virtual void WillDestroyAllMediaLayers() {} + virtual void ResizedSwapBuffer() {} + + // Non-virtual, and InitWindow is back after webOS 3 did without it. + void InitWindow(int width, int height); + void Show(); + void Activate(); + void AttachWebContents(void* web_contents); + void SetWindowHostState(NativeWindowState state); + void SetWindowProperty(const std::string& name, const std::string& value); + + private: + // 8 bytes, unlike WebViewBase next door: libcbe's constructor touches only + // offsets 0 and 4. + void* webapp_window_; // WebAppWindow*, owned by libcbe +}; + +} // namespace webos diff --git a/web/libcbe/webos6/webos/webview_base.h b/web/libcbe/webos6/webos/webview_base.h new file mode 100644 index 0000000..700cf5b --- /dev/null +++ b/web/libcbe/webos6/webos/webview_base.h @@ -0,0 +1,240 @@ +// webos::WebViewBase as it exists on webOS 6 and newer. +// +// A third shape of the same API - see ../../webos/webview_base.h for webOS 4 and +// ../../webos3/webos/webview_base.h for webOS 3. What changed here: +// +// * the entry point is a class, webos::WebOSMain(delegate)->Run(argc, argv), +// not the free WebOSMain() of webOS 3 and 4; +// * WebViewBase takes (bool, int, int) and Initialize takes five strings and a +// bool, where webOS 4 took five strings, two ints and a bool; +// * the delegate is 61 slots rather than 24, and its order is different again. +// +// Measured on a 65UP7560 running starfish 6.5.2 (Chromium 79). The slot order +// below is BlinkWebView's vtable inside that set's libWebAppMgr.so - WAM still +// subclasses the webos:: API on this generation, so it remains the reference +// implementation. Slots it fills with local functions are named UnknownNN: they +// must exist and keep their position, but their signatures are not recoverable. +// They are declared void and argument-less deliberately - the callee ignores +// arguments, and on AAPCS that is safe. +// +// Names come from the vtable itself; upstream's neva_app_runtime headers +// (webosose/chromium87, src/neva/app_runtime/public) describe a related but +// *different* interface - 32 virtuals against these 61 - so they are a naming +// reference, not the layout. +#pragma once + +#include +#include + +class WebOSEvent; + +namespace webos { + +class WebViewProfile; + +class WebViewDelegate { + public: + // slot 0 + virtual void OnLoadProgressChanged(double progress) { (void)progress; } + // slot 1 + virtual void DidFirstFrameFocused() {} + // slot 2 + virtual void TitleChanged(const std::string& title) { (void)title; } + // slot 3 + virtual void NavigationHistoryChanged() {} + // slot 4 + virtual void Close() {} + // slot 5 + virtual bool DecidePolicyForResponse(bool is_main_frame, int status_code, + const std::string& url, + const std::string& status_text) { (void)is_main_frame; (void)status_code; (void)url; (void)status_text; return false; } + // slot 6 + virtual bool AcceptsVideoCapture() { return false; } + // slot 7 + virtual bool AcceptsAudioCapture() { return false; } + // slot 8 + virtual void LoadStarted() {} + // slot 9 + virtual void LoadFinished(const std::string& url) { (void)url; } + // slot 10 + virtual void LoadFailed(const std::string& url, int err_code, + const std::string& err_desc) { (void)url; (void)err_code; (void)err_desc; } + // slot 11 + virtual void LoadAborted(const std::string& url) { (void)url; } + // slot 12 - name not recoverable; WAM implements it with a local function. + virtual void Unknown12() {} + // slot 13 + virtual void RenderProcessCreated(int pid) { (void)pid; } + // slot 14 + virtual void RenderProcessGone(bool crashed) { (void)crashed; } + // slot 15 + virtual void DocumentLoadFinished() {} + // slot 16 + virtual void DidStartNavigation(const std::string& url, bool is_main_frame) { (void)url; (void)is_main_frame; } + // slot 17 + virtual void DidFinishNavigation(const std::string& url, bool is_main_frame) { (void)url; (void)is_main_frame; } + // slot 18 - name not recoverable; WAM implements it with a local function. + virtual void Unknown18() {} + // slot 19 + virtual void DidClearWindowObject() {} + // slot 20 + virtual void DidSwapCompositorFrame() {} + // slot 21 + virtual bool AllowMouseOnOffEvent() const { return false; } + // slot 22 - name not recoverable; WAM implements it with a local function. + virtual void Unknown22() {} + // slot 23 + virtual void DidLoadingEnd() {} + // slot 24 - name not recoverable; WAM implements it with a local function. + virtual void Unknown24() {} + // slot 25 - name not recoverable; WAM implements it with a local function. + virtual void Unknown25() {} + // slot 26 - name not recoverable; WAM implements it with a local function. + virtual void Unknown26() {} + // slot 27 + virtual void DidFirstMeaningfulPaint() {} + // slot 28 + virtual void DidNonFirstMeaningfulPaint() {} + // slot 29 + virtual void ErrorPageStateChanged(bool enable) { (void)enable; } + // slot 30 - name not recoverable; WAM implements it with a local function. + virtual void Unknown30() {} + // slot 31 + virtual void SkipBeginMainFrameAck() {} + // slot 32 + virtual void CreatePlugin() {} + // slot 33 - name not recoverable; WAM implements it with a local function. + virtual void Unknown33() {} + // slot 34 + virtual void MediaAboutToPlayNotify(const std::string& id, bool audio) { (void)id; (void)audio; } + // slot 35 - name not recoverable; WAM implements it with a local function. + virtual void Unknown35() {} + // slot 36 - name not recoverable; WAM implements it with a local function. + virtual void Unknown36() {} + // slot 37 + virtual void RequestLaunchFullBrowser(const std::string& url) { (void)url; } + // slot 38 + virtual bool CanDownload(std::string& url) { (void)url; return false; } + // slot 39 - name not recoverable; WAM implements it with a local function. + virtual void Unknown39() {} + // slot 40 + virtual void LoadStopped(const std::string& url) { (void)url; } + // slot 41 - name not recoverable; WAM implements it with a local function. + virtual void Unknown41() {} + // slot 42 - name not recoverable; WAM implements it with a local function. + virtual void Unknown42() {} + // slot 43 - name not recoverable; WAM implements it with a local function. + virtual void Unknown43() {} + // slot 44 + virtual void NotifyFault(const std::string& a, int b, const std::string& c) { (void)a; (void)b; (void)c; } + // slot 45 + virtual void HandleKeyboardEvent(int key) { (void)key; } + // slot 46 + virtual void RequestMediaLayer(const std::string& id, unsigned type) { (void)id; (void)type; } + // slot 47 - name not recoverable; WAM implements it with a local function. + virtual void Unknown47() {} + // slot 48 - name not recoverable; WAM implements it with a local function. + virtual void Unknown48() {} + // slot 49 + virtual void SetMediaProperty(const std::string& a, const std::string& b, + const std::string& c) { (void)a; (void)b; (void)c; } + // slot 50 + virtual void DestroyMediaLayer(const std::string& id) { (void)id; } + // slot 51 - name not recoverable; WAM implements it with a local function. + virtual void Unknown51() {} + // slot 52 - name not recoverable; WAM implements it with a local function. + virtual void Unknown52() {} + // slot 53 is GetWebContents(), and it is virtual - libcbe calls it through the + // vtable during Initialize(). Declaring it void, as a placeholder, hands + // libcbe whatever was in r0 as a WebContents pointer and the process dies + // inside Initialize. It is pure here and overridden in WebViewBase below with + // no body, so the slot resolves to libcbe's own implementation at link time. + virtual void* GetWebContents() = 0; + // slot 54 + virtual void HandleBrowserControlCommand(const std::string& command, + const std::vector& args) { (void)command; (void)args; } + // slot 55 + virtual void HandleBrowserControlFunction(const std::string& command, + const std::vector& args, + std::string* result) { (void)command; (void)args; (void)result; } + // slot 56 + virtual void LoadVisuallyCommitted() {} + // slot 57 - name not recoverable; WAM implements it with a local function. + virtual void Unknown57() {} + // slot 58 - name not recoverable; WAM implements it with a local function. + virtual void Unknown58() {} + // slot 59 - name not recoverable; WAM implements it with a local function. + virtual void Unknown59() {} + // slot 60 + virtual void DidDropAllPeerConnections(int reason) { (void)reason; } +}; + +class WebViewBase : public WebViewDelegate { + public: + enum WebViewMode { WEBVIEW_MODE_NORMAL }; + enum WebPageVisibilityState { VISIBILITY_VISIBLE, VISIBILITY_HIDDEN, VISIBILITY_LAUNCHING }; + + WebViewBase(bool alt_storage, int width, int height); + ~WebViewBase(); + + // Two overloads exist on this generation. WAM calls this one - the long form, + // carrying width/height, a WebViewMode and two bools - at every one of its + // three call sites, and the short (five strings + bool) form segfaults inside + // libcbe when called from here. Use this one. + void Initialize(const std::string& app_id, + const std::string& app_path, + const std::string& trust_level, + const std::string& v8_snapshot_path, + const std::string& v8_extra_flags, + int width, + int height, + bool use_native_scroll, + WebViewMode mode, + bool inspectable); + + void* GetWebContents() override; // implemented by libcbe, not by us + void LoadUrl(const std::string& url); + void StopLoading(); + void Reload(); + std::string GetUrl(); + void RunJavaScript(const std::string& js); + void UpdatePreferences(); + + void SetAppId(const std::string& app_id); + void SetTrustLevel(const std::string& trust_level); + void SetUserAgent(const std::string& user_agent); + void SetVisible(bool visible); + void SetAllowLocalResourceLoad(bool allow); + void SetAllowUniversalAccessFromFileUrls(bool allow); + void SetFileAccessBlocked(bool blocked); + void SetLocalStorageEnabled(bool enabled); + void SetVisibilityState(WebPageVisibilityState state); + + private: + // 92 bytes total, and the size is load-bearing: libcbe's constructor writes as + // far as offset 90, so a subclass declared any smaller corrupts the heap and + // the process dies later inside malloc, nowhere near the cause. The layout is + // opaque; only the size matters here. (webOS 4's WebViewBase was 8 bytes, + // which is why this needed measuring rather than assuming.) + void* webview_; // WebView*, owned by libcbe + char reserved_[84]; // to sizeof == 92 +}; + +// The entry point on this generation. Run() is Chromium's content main: it takes +// the process over and does not return. +class WebOSMainDelegate { + public: + // One slot, per WAM's WebOSMainDelegateWAM vtable. + virtual void AboutToCreateContentBrowserClient() = 0; +}; + +class WebOSMain { + public: + explicit WebOSMain(WebOSMainDelegate* delegate); + int Run(int argc, const char** argv); + + private: + void* main_; +}; + +} // namespace webos diff --git a/web/neva/CMakeLists.txt b/web/neva/CMakeLists.txt new file mode 100644 index 0000000..6e9cb70 --- /dev/null +++ b/web/neva/CMakeLists.txt @@ -0,0 +1,34 @@ +# The same web view as web/cbe-webos6, through neva_app_runtime instead. +# +# Both APIs live in the same libcbe on the same TV. This is the one whose +# headers LG published (webosose/chromium87, src/neva/app_runtime/public), which +# makes it the better starting point of the two - see +# ../libcbe/neva/neva_app_runtime/webview_base.h for how far the published order +# can be trusted and how that was checked. + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +add_library(cbe-stub-neva SHARED ../libcbe/neva/cbe_stub.cpp) +set_target_properties(cbe-stub-neva PROPERTIES OUTPUT_NAME cbe + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/stub") +target_include_directories(cbe-stub-neva PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../libcbe/neva") +target_compile_options(cbe-stub-neva PUBLIC -fno-rtti) + +add_executable(web-neva main.cpp) +target_include_directories(web-neva SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS}) +target_link_directories(web-neva PRIVATE ${GLIB_LIBRARY_DIRS}) +target_link_libraries(web-neva PRIVATE cbe-stub-neva ${GLIB_LIBRARIES}) +target_compile_definitions(web-neva PRIVATE APP_LOG_NAME="web-neva") + +set_target_properties(web-neva PROPERTIES + SKIP_BUILD_RPATH TRUE + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "") + +webos_add_ipk( + TARGET web-neva + APPID org.webosbrew.sample.web.neva + TITLE "Neva WebView (webOS 6+)" + COLOR "#00897B" + WEBOS_VERSIONS ">=6.4") diff --git a/web/neva/README.md b/web/neva/README.md new file mode 100644 index 0000000..cfa5a7b --- /dev/null +++ b/web/neva/README.md @@ -0,0 +1,54 @@ +# neva_app_runtime + +The same web view as `../cbe-webos6`, through the other API in the same library. + +**Verified on a 65UP7560 (starfish 6.5.2):** the page loads and renders full-screen and the +app takes the foreground. `-verify` is clean from webOS 6.4 to 11.2. + +## Why this one is the better starting point + +webOS 6 ships two embedding APIs in one `libcbe.so`: the `webos::` one WAM still uses, and +`neva_app_runtime`, whose headers LG **published** - `webosose/chromium87`, +`src/neva/app_runtime/public`. That makes this the only sample under `web/` that starts +from source rather than from a vtable. + +It is smaller and simpler, too: + +| | `webos::` (webOS 6) | `neva_app_runtime` | +|---|---|---| +| entry point | `webos::WebOSMain(delegate).Run(...)` | `AppRuntimeMain(argc, argv)` | +| view setup | constructor **and** a 10-argument `Initialize` | constructor only | +| delegate slots | 61 | 40 | +| `sizeof(WebViewBase)` | 92 | 16 | +| window sizing | `InitWindow(w, h)` | `Resize(w, h)` | + +## The published headers are a starting point, not the answer + +The TV is Chromium **79** - between the public `chromium68` and `chromium87` trees - and its +vtable has **40** slots where chromium87 declares 32 virtuals and chromium68 declares 26. So +the upstream order still has to be checked. + +What checks it, without relying on names at all, is which slots are **pure virtual**. libcbe's +own vtable says: a pure slot relocates to `__cxa_pure_virtual`, an implemented one to real +code. Lining that up against the header's `= 0` versus `{}`: + +``` +firmware pure at: 0-15, 18, 19, 20, 21, 29 +upstream pure at: 0-15, 18, 19, 20, 21 +``` + +Identical through slot 28. That is a strong enough signal to take slots 0-28 as upstream's, +in upstream's order; 29-39 are LG additions and stay placeholders. + +## Two traps, both worth knowing + +**`AppRuntimeMain` is not `extern "C"`.** libcbe exports it C++-mangled, as +`_Z14AppRuntimeMainiPPKc`. Declaring it `extern "C"` looks for a plain `AppRuntimeMain` that +does not exist, and `-verify` catches it. + +**Object sizes are load-bearing.** libcbe's constructors write to offset 12 +(`WebViewBase`) and 4 (`WebAppWindowBase`), so those classes are 16 and 8 bytes. A subclass +declared smaller lets libcbe write past the allocation, and the process dies later inside +`malloc` with a backtrace nowhere near the cause. `../cbe-webos6/README.md` has the longer +version of that story, along with the slot-tracer trick for finding a stubbed slot that was +supposed to return a pointer. diff --git a/web/neva/main.cpp b/web/neva/main.cpp new file mode 100644 index 0000000..6752643 --- /dev/null +++ b/web/neva/main.cpp @@ -0,0 +1,113 @@ +// The same web view again, through neva_app_runtime instead of webos::. +// +// Worth having beside web/cbe-webos6 because the two APIs live in the same +// library on the same TV, and this is the one whose headers LG published. The +// differences are all simplifications: +// +// * AppRuntimeMain(argc, argv) is a free function, where webos:: has a +// WebOSMain class taking a delegate; +// * there is no Initialize() - WebViewBase(width, height, profile) does it; +// * the delegate is 40 slots against webos::'s 61, and 0-28 of them are +// upstream's own, in upstream's order. +// +// Verified on a 65UP7560 running starfish 6.5.2. + +#include + +#include +#include +#include +#include +#include +#include + +#include "neva_app_runtime/webapp_window_base.h" +#include "neva_app_runtime/webview_base.h" + +namespace { + +const char kAppId[] = "org.webosbrew.sample.web.neva"; +const char kUrl[] = "https://example.com/"; + +class SampleWebView : public neva_app_runtime::WebViewBase { + public: + SampleWebView(int w, int h) : neva_app_runtime::WebViewBase(w, h, NULL) {} + + void OnLoadProgressChanged(double progress) override { + printf("[neva] progress %3.0f%%\n", progress * 100); + } + void TitleChanged(const std::string& title) override { + printf("[neva] title '%s'\n", title.c_str()); + } + void LoadStarted() override { puts("[neva] load started"); } + void LoadFinished(const std::string& url) override { + printf("[neva] finished %s\n", url.c_str()); + } + void LoadFailed(const std::string& url, int code, const std::string& desc) override { + printf("[neva] FAILED %s (%d %s)\n", url.c_str(), code, desc.c_str()); + } + void DocumentLoadFinished() override { puts("[neva] document loaded"); } + void DidFirstMeaningfulPaint() override { puts("[neva] first meaningful paint"); } + void RenderProcessCreated(int pid) override { printf("[neva] renderer pid %d\n", pid); } +}; + +class SampleWindow : public neva_app_runtime::WebAppWindowBase {}; + +SampleWindow* g_window; +SampleWebView* g_webview; + +gboolean CreateWebApp(gpointer) { + g_window = new SampleWindow(); + g_window->Resize(1920, 1080); + g_window->SetWindowProperty("appId", kAppId); + g_window->SetWindowHostState(neva_app_runtime::FULLSCREEN); + + g_webview = new SampleWebView(1920, 1080); + g_webview->SetAppId(kAppId); + g_webview->SetVisible(true); + + g_window->AttachWebContents(g_webview->GetWebContents()); + g_window->Show(); + g_window->Activate(); + + printf("[neva] loading %s\n", kUrl); + g_webview->LoadUrl(kUrl); + return G_SOURCE_REMOVE; +} + +bool IsBrowserProcess(int argc, char** argv) { + for (int i = 1; i < argc; ++i) + if (strncmp(argv[i], "--type=", 7) == 0) return false; + return true; +} + +} // namespace + +int main(int argc, char** argv) { + const bool browser = IsBrowserProcess(argc, argv); + if (browser) { + freopen("/tmp/" APP_LOG_NAME ".log", "w", stdout); + dup2(1, 2); + } + setvbuf(stdout, NULL, _IOLBF, 0); + + std::vector args; + args.push_back(argv[0]); + if (browser) { + args.push_back("--ozone-platform=wayland"); + args.push_back("--no-sandbox"); + args.push_back("--no-zygote"); + args.push_back("--in-process-gpu"); + args.push_back(std::string("--browser-subprocess-path=") + argv[0]); + args.push_back(std::string("--user-data-dir=/tmp/") + kAppId); + } + for (int i = 1; i < argc; ++i) args.push_back(argv[i]); + + std::vector cargv; + for (size_t i = 0; i < args.size(); ++i) cargv.push_back(args[i].c_str()); + + if (!getenv("XDG_RUNTIME_DIR")) setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); + if (browser) g_idle_add(CreateWebApp, NULL); + + return AppRuntimeMain(static_cast(cargv.size()), cargv.data()); +}