* feat(ui-host): give a view its chance to finish before teardown
ui-host deleted the plugin the instant app.exec() returned. Core modules
have had a grace period since the teardown hook shipped; views did not.
Calls the shared logos::runPluginAboutToUnload() (logos-plugin-qt#23)
between exec() returning and `delete pluginObject`. Safe to run a nested
event loop there: the application loop has already returned, so this is the
same shape as logos_host's call site rather than a re-entrant exec(). It
must come BEFORE the delete — it is the plugin it asks, and the plugin has
to still be alive to answer.
THE GRACE PERIOD IS 2000ms, NOT logos_host's 3000. That difference is the
reason the helper takes it as a parameter instead of owning a constant.
ViewModuleHost::stop() gives this process terminate() plus
waitForFinished(3000) before it resorts to kill(), and everything after the
signal has to fit inside that 3s: unwinding exec() through the self-pipe
notifier, this grace period, `delete pluginObject`, the QRemoteObjectHost
destructor unlinking the QtRO socket, and process exit. Porting 3000 would
have consumed the entire budget and left every async view hard-killed
mid-teardown — the precise failure the hook exists to prevent.
Measured, through the real ViewModuleHost driving the real ui-host binary:
no hook 0.001s dtor
async finishes 1.003s about-to-unload, tick x10, work-done, signalled, dtor
async hangs 1.903s about-to-unload, tick x19, dtor
All exit 0 — none hard-killed. The hang case costs 1.9s rather than 2.0
because the deadline is a default coarse QTimer, which Qt may fire up to 5%
early; that errs toward giving up sooner, the safe direction for a budget
carved out of someone else's kill timer.
The assertions are on ENTRY ORDER and relative tick counts, never elapsed
time: `work-done` before `dtor` proves the wait happened, the ticks prove a
nested loop actually ran, and hangTicks > finishTicks proves the deadline
outlasts a completion — i.e. that deadline.isActive() really does
distinguish "finished" from "gave up". Wall clock is asserted only where a
bound IS the contract (stopMs < 3000).
Negative control: removing the call fails 3 of the 4 tests, and correctly
leaves the no-hook one passing.
Two pins move forward, both required rather than incidental:
* logos-plugin-qt -> #23, which adds the helper. TODO: re-point at master
once it merges.
* logos-protocol f4407ff -> 7989472. plugin-qt follows THIS repo's
protocol, and its logos_qt_host_shared refuses to configure against one
that does not export logos_protocol_shared (protocol#65) — a deliberate
FATAL_ERROR, because the alternative is falling back to the archive and
reintroducing the duplicate singleton at runtime.
10/10 tests pass, UiHostUnloadTests included.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): track logos-plugin-qt master now that the helper has landed
logos-plugin-qt#23 merged as ef11c21, so the temporary rev pin goes. The
relocked narHash is identical to the branch tip this was pinned to, so
nothing about the build changes — only the pin.
The logos-protocol bump stays: plugin-qt follows THIS repo's protocol, and
its logos_qt_host_shared refuses to configure against one that does not
export logos_protocol_shared (protocol#65).
10/10 tests green, UiHostUnloadTests included.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>