summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src/server.cpp
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 22:44:16 +0900
committerAdam Malczewski <[email protected]>2026-06-12 22:44:16 +0900
commitc102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7 (patch)
treef6dea2875b939c0f661292d8bfa0d79a96fe67d7 /packages/kernel/src/server.cpp
parent6949c3582ed1e480e70aabfcfa3a11b78007cc12 (diff)
downloadunbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.tar.gz
unbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.zip
Slice 4: extension host + typed bus; xdg-shell/layer-shell extracted to core extensions
The kernel now names NO concrete feature. It owns: the extension host (install/topological activate, missing-dep/cycle = startup error), the typed Event/Filter bus (error-isolated: a throwing extension is disabled, never the session; RAII Subscriptions), the Host API (per-extension facade: borrows, scene layers, event catalogue, typed services), the public RAII Listener, and a typed surface→scene-tree registry (Host::host_surface/scene_tree_for) that replaced the untyped wlr_surface.data convention both extensions flagged. - ext-xdg-shell (core): toplevel/popup lifecycle, focus-on-map, click/tap-to-focus, pointer/touch routing incl. button+axis (the kernel only moves the cursor and emits — a contract-doc lie caught by user hands-on), interactive move/resize via pure GrabMachine (fixes the request-arrives-after-release race: grab requires request ∧ button-down, release always ends it), Alt+F1 cycle, Ctrl+Alt+Backspace terminate (labwc's default A-Escape=Exit killed the dev session once — never again; see nested-run skill). - ext-layer-shell (core): wlr-layer-shell v1 (proto v5) for external clients; pure doctest-hard arrangement core; fuzzel verified visually nested (fix: seed outputs from output_layout at activate — events-only tracking missed pre-activation outputs; plus a scene-node double-free). - First protocol codegen: vendored wlr-layer-shell XML + wayland-scanner server-header propagated through kernel_dep; wlr.hpp grew a namespace→_namespace keyword fix for the generated header. - Glossary: 'scene layer' (user-approved). New rules earned: parallel-wave-builds, contract-docs. - User hands-on verified: typing, click-to-focus, drag-select, scroll, titlebar drag-move (slow + flick), Alt+F1, fuzzel + arrows, touch tap, Ctrl+Alt+Backspace. 68 doctest cases green, ASan/UBSan clean (our code), idle RSS ≈73 MiB.
Diffstat (limited to 'packages/kernel/src/server.cpp')
-rw-r--r--packages/kernel/src/server.cpp191
1 files changed, 168 insertions, 23 deletions
diff --git a/packages/kernel/src/server.cpp b/packages/kernel/src/server.cpp
index df24f1f..e3308de 100644
--- a/packages/kernel/src/server.cpp
+++ b/packages/kernel/src/server.cpp
@@ -3,6 +3,8 @@
#include <ctime>
#include <stdexcept>
#include <unistd.h>
+#include <unordered_map>
+#include <utility>
namespace unbox::kernel {
@@ -42,7 +44,16 @@ auto Server::socket_name() const -> std::string {
return impl_->socket;
}
+void Server::install(std::unique_ptr<Extension> extension) {
+ impl_->install(std::move(extension));
+}
+
+void Server::activate_extensions() {
+ impl_->activate_extensions();
+}
+
void Server::run() {
+ impl_->activate_extensions();
wlr_log(WLR_INFO, "unbox running on WAYLAND_DISPLAY=%s", impl_->socket.c_str());
wl_display_run(impl_->display);
}
@@ -68,6 +79,27 @@ auto Server::ui_spike_orientation() const -> int {
// ---- Impl lifecycle --------------------------------------------------------
+void Server::Impl::register_hook(detail::HookBase& hook) {
+ hook.set_sink(this);
+ all_hooks.push_back(&hook);
+}
+
+void Server::Impl::disable(ExtensionId who) noexcept {
+ // Error isolation: a callback owned by `who` threw. Mark the extension dead
+ // and purge its subscriptions from every hook. Safe mid-dispatch — each
+ // hook tombstones now and compacts when its dispatch unwinds.
+ for (ExtensionSlot& slot : extensions) {
+ if (slot.id == who && !slot.disabled) {
+ slot.disabled = true;
+ wlr_log(WLR_ERROR, "extension '%s' disabled: a hook callback threw",
+ slot.extension->manifest().id.c_str());
+ }
+ }
+ for (detail::HookBase* hook : all_hooks) {
+ hook->purge(who);
+ }
+}
+
void Server::Impl::init() {
wlr_log_init(WLR_INFO, nullptr);
@@ -91,13 +123,12 @@ void Server::Impl::init() {
scene_layout = require(wlr_scene_attach_output_layout(scene, output_layout),
"wlr_scene_output_layout");
- xdg_shell = require(wlr_xdg_shell_create(display, 3), "wlr_xdg_shell");
- new_xdg_toplevel.connect(xdg_shell->events.new_toplevel, [this](void* data) {
- handle_new_toplevel(static_cast<wlr_xdg_toplevel*>(data));
- });
- new_xdg_popup.connect(xdg_shell->events.new_popup, [this](void* data) {
- handle_new_popup(static_cast<wlr_xdg_popup*>(data));
- });
+ // Ordered z-bands. wlr_scene_tree_create appends as the top child of its
+ // parent, so creating background -> overlay yields exactly that stacking
+ // order (background lowest, overlay highest).
+ for (auto& layer : scene_layers) {
+ layer = require(wlr_scene_tree_create(&scene->tree), "wlr_scene_tree (layer)");
+ }
cursor = require(wlr_cursor_create(), "wlr_cursor");
wlr_cursor_attach_output_layout(cursor, output_layout);
@@ -110,6 +141,24 @@ void Server::Impl::init() {
seat = require(wlr_seat_create(display, "seat0"), "wlr_seat");
attach_seat_handlers();
+ // Register every kernel-emitted hook with the isolation registry.
+ for (detail::HookBase* hook : {
+ static_cast<detail::HookBase*>(&ev_output_added),
+ static_cast<detail::HookBase*>(&ev_output_removed),
+ static_cast<detail::HookBase*>(&ev_pointer_motion),
+ static_cast<detail::HookBase*>(&ev_pointer_button),
+ static_cast<detail::HookBase*>(&ev_pointer_axis),
+ static_cast<detail::HookBase*>(&ev_pointer_frame),
+ static_cast<detail::HookBase*>(&ev_touch_down),
+ static_cast<detail::HookBase*>(&ev_touch_motion),
+ static_cast<detail::HookBase*>(&ev_touch_up),
+ static_cast<detail::HookBase*>(&ev_touch_cancel),
+ static_cast<detail::HookBase*>(&ev_touch_frame),
+ static_cast<detail::HookBase*>(&key_filter),
+ }) {
+ all_hooks.push_back(hook); // sink already set via {this} constructor
+ }
+
const char* socket_cstr = wl_display_add_socket_auto(display);
if (socket_cstr == nullptr) {
throw std::runtime_error("failed to add a Wayland socket");
@@ -135,11 +184,96 @@ void Server::Impl::init() {
}
}
+// ---- Extension host --------------------------------------------------------
+
+void Server::Impl::install(std::unique_ptr<Extension> extension) {
+ if (extensions_activated) {
+ throw std::runtime_error("Server::install called after activate_extensions");
+ }
+ const std::string& id = extension->manifest().id;
+ for (const ExtensionSlot& slot : extensions) {
+ if (slot.extension->manifest().id == id) {
+ throw std::runtime_error("duplicate extension id: " + id);
+ }
+ }
+ ExtensionSlot slot;
+ // id 0 is the kernel; extensions start at 1.
+ slot.id = static_cast<ExtensionId>(extensions.size() + 1);
+ slot.host = std::make_unique<HostImpl>(this, slot.id);
+ slot.extension = std::move(extension);
+ extensions.push_back(std::move(slot));
+}
+
+void Server::Impl::activate_extensions() {
+ if (extensions_activated) {
+ return;
+ }
+ extensions_activated = true;
+
+ // Topological sort by Manifest depends_on. Index extensions by id; ties
+ // (no dependency relation) are broken by tier (core before standard) then
+ // install order — deterministic activation.
+ const std::size_t n = extensions.size();
+ std::unordered_map<std::string, std::size_t> by_id;
+ for (std::size_t i = 0; i < n; ++i) {
+ by_id.emplace(extensions[i].extension->manifest().id, i);
+ }
+
+ // Build adjacency (dep -> dependents) and indegree; validate deps exist.
+ std::vector<std::vector<std::size_t>> dependents(n);
+ std::vector<int> indegree(n, 0);
+ for (std::size_t i = 0; i < n; ++i) {
+ const Manifest& m = extensions[i].extension->manifest();
+ for (const std::string& dep : m.depends_on) {
+ auto it = by_id.find(dep);
+ if (it == by_id.end()) {
+ throw std::runtime_error("extension '" + m.id +
+ "' depends on missing extension '" + dep + "'");
+ }
+ dependents[it->second].push_back(i);
+ ++indegree[i];
+ }
+ }
+
+ // Kahn's algorithm with a deterministic tie-break: among ready nodes pick
+ // the lowest (tier, install-index). A linear scan is fine for the handful
+ // of extensions a session installs.
+ auto rank = [&](std::size_t i) {
+ return std::pair<int, std::size_t>(
+ static_cast<int>(extensions[i].extension->manifest().tier), i);
+ };
+ std::vector<bool> done(n, false);
+ std::vector<std::size_t> order;
+ order.reserve(n);
+ for (std::size_t step = 0; step < n; ++step) {
+ std::size_t pick = n;
+ for (std::size_t i = 0; i < n; ++i) {
+ if (!done[i] && indegree[i] == 0) {
+ if (pick == n || rank(i) < rank(pick)) {
+ pick = i;
+ }
+ }
+ }
+ if (pick == n) {
+ throw std::runtime_error("extension dependency cycle detected");
+ }
+ done[pick] = true;
+ order.push_back(pick);
+ for (std::size_t d : dependents[pick]) {
+ --indegree[d];
+ }
+ }
+
+ // Activate in topological order. An activate() throw is FATAL (not
+ // isolated): a core extension that cannot start is a broken session.
+ for (std::size_t i : order) {
+ ExtensionSlot& slot = extensions[i];
+ slot.extension->activate(*slot.host);
+ slot.activated = true;
+ }
+}
+
void Server::Impl::start_ui_spike() {
- // The bridge needs the wlr renderer's EGLDisplay to build its sibling
- // GLES 3.2 context. Only the gles2 renderer exposes one; under the
- // pixman renderer (e.g. headless CI) there is no GL path, so the spike
- // stays disabled — slice-2 behaviour is preserved.
if (!wlr_renderer_is_gles2(renderer)) {
wlr_log(WLR_INFO, "ui-spike: renderer is not gles2; spike disabled");
return;
@@ -150,24 +284,34 @@ void Server::Impl::start_ui_spike() {
return;
}
EGLDisplay display_egl = wlr_egl_get_display(egl);
- ui_spike = UiSpike::create(&scene->tree, display_egl, allocator, renderer);
+ // The spike sits in the overlay band so it composites above everything.
+ ui_spike = UiSpike::create(scene_layers[static_cast<std::size_t>(SceneLayer::overlay)],
+ display_egl, allocator, renderer);
}
void Server::Impl::shutdown() {
- // Slice-3 spike: tear down before scene/renderer/allocator die (it owns
- // a scene node, GL objects on a sibling context, and borrows the others).
+ // Destroy extensions FIRST, in reverse activation order: their RAII members
+ // (Subscriptions, Listeners, scene nodes) release while the wlr objects
+ // they borrow are still alive. Reverse of `extensions` install order is a
+ // safe superset of reverse-topological (a dependent installed later than
+ // its dependency dies first; if installed earlier, it still only borrows).
+ for (auto it = extensions.rbegin(); it != extensions.rend(); ++it) {
+ it->extension.reset();
+ it->host.reset();
+ }
+ extensions.clear();
+
+ // Slice-3 spike: tear down before scene/renderer/allocator die.
ui_spike.reset();
if (display != nullptr) {
- wl_display_destroy_clients(display); // fires toplevel/popup destroy events
+ wl_display_destroy_clients(display);
}
- // Server-level listeners must detach BEFORE the wlr objects owning their
- // signals die; a wl_listener outliving its signal is a use-after-free.
+ // Server-level listeners detach BEFORE the wlr objects owning their signals
+ // die; a wl_listener outliving its signal is a use-after-free.
new_output.disconnect();
new_input.disconnect();
- new_xdg_toplevel.disconnect();
- new_xdg_popup.disconnect();
cursor_motion.disconnect();
cursor_motion_absolute.disconnect();
cursor_button.disconnect();
@@ -203,7 +347,7 @@ void Server::Impl::shutdown() {
renderer = nullptr;
}
if (backend != nullptr) {
- wlr_backend_destroy(backend); // fires output + input-device destroy events
+ wlr_backend_destroy(backend);
backend = nullptr;
}
if (display != nullptr) {
@@ -233,12 +377,9 @@ void Server::Impl::handle_new_output(wlr_output* wlr_output) {
outputs.push_back(std::move(owned));
output->frame.connect(wlr_output->events.frame, [this, output](void*) {
- // Slice-3 spike: render the RMLUi document if dirty, before commit so
- // its damage is picked up this frame. Cheap no-op when disabled.
if (ui_spike != nullptr) {
ui_spike->tick();
}
-
wlr_scene_output* scene_output = wlr_scene_get_scene_output(scene, output->output);
wlr_scene_output_commit(scene_output, nullptr);
@@ -251,6 +392,8 @@ void Server::Impl::handle_new_output(wlr_output* wlr_output) {
wlr_output_commit_state(output->output, event->state);
});
output->destroy.connect(wlr_output->events.destroy, [this, output](void*) {
+ const OutputEvent ev{output->output};
+ ev_output_removed.emit(ev);
// Last action: destroys `output` (and these listeners with it).
outputs.remove_if([output](const auto& owned) { return owned.get() == output; });
});
@@ -260,6 +403,8 @@ void Server::Impl::handle_new_output(wlr_output* wlr_output) {
wlr_scene_output_layout_add_output(scene_layout, layout_output, scene_output);
wlr_log(WLR_INFO, "new output %s", wlr_output->name);
+ const OutputEvent ev{wlr_output};
+ ev_output_added.emit(ev);
}
} // namespace unbox::kernel