diff options
Diffstat (limited to 'packages/kernel/src')
| -rw-r--r-- | packages/kernel/src/file_watcher.cpp | 196 | ||||
| -rw-r--r-- | packages/kernel/src/file_watcher.hpp | 87 | ||||
| -rw-r--r-- | packages/kernel/src/server.cpp | 29 | ||||
| -rw-r--r-- | packages/kernel/src/server_impl.hpp | 19 | ||||
| -rw-r--r-- | packages/kernel/src/ui_substrate.cpp | 192 | ||||
| -rw-r--r-- | packages/kernel/src/ui_substrate.hpp | 13 |
6 files changed, 379 insertions, 157 deletions
diff --git a/packages/kernel/src/file_watcher.cpp b/packages/kernel/src/file_watcher.cpp new file mode 100644 index 0000000..ae292c2 --- /dev/null +++ b/packages/kernel/src/file_watcher.cpp @@ -0,0 +1,196 @@ +#include "file_watcher.hpp" + +// inotify is libc (NOT wlroots), so it does not go through wlr.hpp. Integrated +// into the kernel's wl_event_loop via wl_event_loop_add_fd — never blocks. +#include <sys/inotify.h> +#include <unistd.h> + +#include <algorithm> +#include <filesystem> + +namespace unbox::kernel { + +FileWatcher::FileWatcher(wl_event_loop* loop, std::function<void(ExtensionId)> disable) + : loop_(loop), disable_(std::move(disable)) {} + +FileWatcher::~FileWatcher() { + // Tear down before the loop/display dies: remove the event source, then + // close the fd (closing drops every inotify watch). + if (source_ != nullptr) { + wl_event_source_remove(source_); + source_ = nullptr; + } + if (fd_ >= 0) { + close(fd_); + fd_ = -1; + } +} + +auto FileWatcher::dispatch(int /*fd*/, std::uint32_t /*mask*/, void* data) -> int { + static_cast<FileWatcher*>(data)->on_readable(); + return 0; +} + +bool FileWatcher::ensure_started() { + if (fd_ >= 0) { + return true; + } + if (loop_ == nullptr) { + return false; + } + fd_ = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (fd_ < 0) { + wlr_log(WLR_ERROR, "file-watcher: inotify_init1 failed; watching disabled"); + return false; + } + source_ = wl_event_loop_add_fd(loop_, fd_, WL_EVENT_READABLE, dispatch, this); + if (source_ == nullptr) { + close(fd_); + fd_ = -1; + wlr_log(WLR_ERROR, "file-watcher: wl_event_loop_add_fd failed; watching disabled"); + return false; + } + return true; +} + +void FileWatcher::stop_if_idle() noexcept { + if (!entries_.empty()) { + return; + } + if (source_ != nullptr) { + wl_event_source_remove(source_); + source_ = nullptr; + } + if (fd_ >= 0) { + close(fd_); // drops all inotify watches + fd_ = -1; + } + wd_dirs_.clear(); +} + +void FileWatcher::arm_dir(const std::string& dir) { + if (fd_ < 0) { + return; + } + // inotify_add_watch on an already-watched path returns the SAME wd, so this + // is idempotent and auto re-arms after a watched file is replaced (we watch + // the directory, not the inode). + const int wd = inotify_add_watch(fd_, dir.c_str(), + IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE); + if (wd >= 0) { + wd_dirs_[wd] = dir; + } +} + +void FileWatcher::rearm_all_dirs() { + for (const auto& [token, e] : entries_) { + arm_dir(e.dir); + } +} + +auto FileWatcher::add(const std::string& path, std::function<void()> on_change, ExtensionId who) + -> FileWatch { + if (!ensure_started()) { + return FileWatch{}; // no loop / inotify unavailable: inert handle + } + std::filesystem::path p(path); + std::string dir = p.parent_path().string(); + if (dir.empty()) { + dir = "."; // a bare filename watches the cwd + } + std::string base = p.filename().string(); + if (base.empty()) { + return FileWatch{}; + } + + const Token token = ++next_token_; + entries_.emplace(token, Entry{std::move(dir), std::move(base), std::move(on_change), who}); + arm_dir(entries_.at(token).dir); + return FileWatch(this, token); +} + +void FileWatcher::remove_watch(Token token) noexcept { + auto it = entries_.find(token); + if (it == entries_.end()) { + return; + } + entries_.erase(it); + // Re-derive the inotify dir watches from the surviving entries: drop watches + // for directories no longer referenced. Cheapest correct approach — clear + // all wd watches and re-arm the dirs still in use. (Watch counts are tiny: + // unbox.toml + a handful of asset dirs.) + if (fd_ >= 0) { + for (const auto& [wd, dir] : wd_dirs_) { + inotify_rm_watch(fd_, wd); + } + wd_dirs_.clear(); + rearm_all_dirs(); + } + stop_if_idle(); +} + +void FileWatcher::on_readable() { + if (fd_ < 0) { + return; + } + // Drain ALL queued events (one readable notification may carry many; the fd + // is non-blocking). Collect the set of (dir, basename) pairs that changed, + // then fire each matching watch's callback AT MOST ONCE this drain + // (coalesced: one save = one callback even though it emits several events). + alignas(struct inotify_event) char buf[4096]; + std::vector<std::pair<std::string, std::string>> changed; // (dir, name) + for (;;) { + const ssize_t n = read(fd_, buf, sizeof(buf)); + if (n <= 0) { + break; // EAGAIN (drained) or closed + } + std::size_t off = 0; + while (off + sizeof(struct inotify_event) <= static_cast<std::size_t>(n)) { + auto* ev = reinterpret_cast<struct inotify_event*>(buf + off); + auto wd_it = wd_dirs_.find(ev->wd); + if (wd_it != wd_dirs_.end() && ev->len > 0) { + changed.emplace_back(wd_it->second, std::string(ev->name)); + } + off += sizeof(struct inotify_event) + ev->len; + } + } + if (changed.empty()) { + return; + } + + // Find the tokens whose (dir, basename) matched at least one event. Snapshot + // them so a callback that destroys its own (or another) watch mid-fire can't + // invalidate the iteration. + std::vector<Token> to_fire; + for (const auto& [token, e] : entries_) { + for (const auto& [dir, name] : changed) { + if (e.dir == dir && e.basename == name) { + to_fire.push_back(token); + break; + } + } + } + for (const Token token : to_fire) { + auto it = entries_.find(token); + if (it == entries_.end()) { + continue; // removed by an earlier callback this drain + } + // Copy what we need before invoking: the callback may remove this watch. + std::function<void()> cb = it->second.on_change; + const ExtensionId who = it->second.who; + if (!cb) { + continue; + } + try { + cb(); + } catch (...) { + // Same isolation boundary as a throwing hook/getter: disable the + // owning extension, never take down the loop/session. + if (disable_) { + disable_(who); + } + } + } +} + +} // namespace unbox::kernel diff --git a/packages/kernel/src/file_watcher.hpp b/packages/kernel/src/file_watcher.hpp new file mode 100644 index 0000000..fbd6d86 --- /dev/null +++ b/packages/kernel/src/file_watcher.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include <unbox/kernel/hooks.hpp> // ExtensionId +#include <unbox/kernel/watch.hpp> +#include <unbox/kernel/wlr.hpp> // wl_event_loop / wl_event_source + +#include <functional> +#include <string> +#include <unordered_map> +#include <vector> + +// The kernel's ONE inotify-on-the-wl_event_loop file watcher, shared by every +// consumer: Host::watch_file (config + any extension) AND the ui substrate's +// (UNBOX_DEV-gated) asset hot-reload. There is exactly one inotify instance per +// session; multiple watched paths multiplex over it. +// +// Editor-save safe: editors save by writing a temp file + renaming over the +// target, so the inode changes and IN_MODIFY on it is unreliable. We watch the +// containing DIRECTORY for IN_CLOSE_WRITE / IN_MOVED_TO / IN_CREATE and match +// the basename — this also fires when a not-yet-existing file is first created. +// +// Coalesced: a single readable notification is drained fully and each affected +// watch's callback fires AT MOST ONCE per drain (one save = one callback). +// +// Error-isolated: a throwing callback is caught at the boundary and the owning +// extension is disabled via the injected sink (same contract as hooks/getters), +// never the session. +// +// Lazy: the inotify fd + wl_event_loop source are created on the FIRST add() +// (whichever consumer is first) and torn down when the watcher is destroyed +// (before the loop/display dies) — kept open for the session while ≥1 watch +// lives; closed when the last watch is removed (re-created on the next add). +// +// Single wl_event_loop thread throughout; no internal locking. + +namespace unbox::kernel { + +class FileWatcher final : public detail::WatchRegistry { +public: + using Token = detail::WatchRegistry::Token; + + // `loop` is the kernel's wl_event_loop (may be null on a backend without + // one — then add() degrades to a no-op handle). `disable` disables the + // owning extension when its callback throws (injected by the kernel, same + // as the bus/substrate isolation sink). + FileWatcher(wl_event_loop* loop, std::function<void(ExtensionId)> disable); + ~FileWatcher() override; + FileWatcher(const FileWatcher&) = delete; + auto operator=(const FileWatcher&) -> FileWatcher& = delete; + + // Watch `path` (resolved by the caller to an absolute/usable path) for + // content changes; fire `on_change` (coalesced, event-loop thread, + // error-isolated to `who`). Returns a FileWatch RAII handle (inactive if + // the watcher could not be set up). The handle removes the watch on destroy. + [[nodiscard]] auto add(const std::string& path, std::function<void()> on_change, + ExtensionId who) -> FileWatch; + + // detail::WatchRegistry — stop the watch with this token (FileWatch dtor). + void remove_watch(Token token) noexcept override; + +private: + struct Entry { + std::string dir; // watched directory (absolute) + std::string basename; // file within `dir` to match + std::function<void()> on_change; + ExtensionId who{}; + }; + + bool ensure_started(); // lazy inotify_init + wl_event_loop_add_fd + void stop_if_idle() noexcept; // close fd + source when no entries remain + void arm_dir(const std::string& dir); // (re)add the inotify dir watch + void rearm_all_dirs(); // re-add every distinct dir's watch + void on_readable(); // drain inotify, coalesce, fire callbacks + + static auto dispatch(int fd, std::uint32_t mask, void* data) -> int; + + wl_event_loop* loop_ = nullptr; + std::function<void(ExtensionId)> disable_; + int fd_ = -1; + wl_event_source* source_ = nullptr; + + Token next_token_ = 0; + std::unordered_map<Token, Entry> entries_; // token -> watch + std::unordered_map<int, std::string> wd_dirs_; // inotify wd -> directory +}; + +} // namespace unbox::kernel diff --git a/packages/kernel/src/server.cpp b/packages/kernel/src/server.cpp index 5d78b70..cbd23c3 100644 --- a/packages/kernel/src/server.cpp +++ b/packages/kernel/src/server.cpp @@ -376,13 +376,25 @@ void Server::Impl::start_substrate() { } // A data-event/getter throw disables the owning extension via the same // isolation path the bus uses (Server::Impl is the DisableSink). The - // wl_event_loop lets the substrate poll the dev hot-reload inotify fd - // (UNBOX_DEV-gated) without ever blocking the loop. - substrate = Substrate::create(display_egl, allocator, renderer, - wl_display_get_event_loop(display), + // substrate uses the kernel's ONE shared FileWatcher for (UNBOX_DEV-gated) + // asset hot-reload — the same watcher Host::watch_file uses for config. + substrate = Substrate::create(display_egl, allocator, renderer, file_watcher(), [this](ExtensionId who) { disable(who); }); } +auto Server::Impl::file_watcher() -> FileWatcher* { + // Lazily create the ONE shared inotify watcher on first use (config watch or + // asset hot-reload), carrying the kernel's disable sink for error isolation. + if (watcher == nullptr) { + if (display == nullptr) { + return nullptr; + } + watcher = std::make_unique<FileWatcher>(wl_display_get_event_loop(display), + [this](ExtensionId who) { disable(who); }); + } + return watcher.get(); +} + void Server::Impl::shutdown() { // Destroy extensions FIRST, in reverse activation order: their RAII members // (Subscriptions, Listeners, scene nodes) release while the wlr objects @@ -396,9 +408,16 @@ void Server::Impl::shutdown() { extensions.clear(); // The ui substrate owns scene nodes + GL objects on a sibling context and - // borrows scene/renderer/allocator: tear it down before they die. + // borrows scene/renderer/allocator: tear it down before they die. (Its asset + // FileWatch handles release here, removing those watches from the watcher.) substrate.reset(); + // The shared file watcher removes its wl_event_loop source + closes the + // inotify fd here — AFTER every FileWatch holder (extensions, substrate) is + // gone, and while the display/loop is still alive (the source must be + // removed before wl_display_destroy). + watcher.reset(); + if (display != nullptr) { wl_display_destroy_clients(display); } diff --git a/packages/kernel/src/server_impl.hpp b/packages/kernel/src/server_impl.hpp index bcdb693..61c073f 100644 --- a/packages/kernel/src/server_impl.hpp +++ b/packages/kernel/src/server_impl.hpp @@ -5,6 +5,7 @@ #include <unbox/kernel/ui.hpp> #include <unbox/kernel/wlr.hpp> +#include "file_watcher.hpp" #include "listener.hpp" #include "ui_substrate.hpp" @@ -93,6 +94,16 @@ struct Server::Impl : detail::DisableSink { // per-extension facades (PerExtensionUi, one per HostImpl) borrow it. std::unique_ptr<Substrate> substrate; + // The ONE inotify-on-the-wl_event_loop file watcher for the session, shared + // by Host::watch_file (config + extensions) AND the substrate's asset + // hot-reload. Created lazily on the first watch (asset or watch_file) via + // file_watcher(); torn down in shutdown() BEFORE the display/loop dies (so + // its event source is removed while the loop is still alive). + std::unique_ptr<FileWatcher> watcher; + // Get-or-create the shared watcher (lazy). Returns nullptr only if there is + // no wl_event_loop (never in practice — the display always has one). + auto file_watcher() -> FileWatcher*; + std::list<std::unique_ptr<Output>> outputs; std::list<std::unique_ptr<Keyboard>> keyboards; std::list<std::unique_ptr<TouchDevice>> touch_devices; @@ -256,6 +267,14 @@ protected: } void adopt_hook(detail::HookBase& hook) override { server_->register_hook(hook); } auto surface_store() -> detail::PointerAssoc& override { return server_->surface_assoc; } + auto register_file_watch(std::string path, std::function<void()> on_change) + -> FileWatch override { + FileWatcher* w = server_->file_watcher(); + if (w == nullptr) { + return FileWatch{}; + } + return w->add(path, std::move(on_change), id_); + } private: Server::Impl* server_; diff --git a/packages/kernel/src/ui_substrate.cpp b/packages/kernel/src/ui_substrate.cpp index 0794536..8b7440e 100644 --- a/packages/kernel/src/ui_substrate.cpp +++ b/packages/kernel/src/ui_substrate.cpp @@ -1,5 +1,6 @@ #include "ui_substrate.hpp" +#include "file_watcher.hpp" #include "rmlui_renderer_gl3.h" #include <RmlUi/Core/Context.h> @@ -20,12 +21,6 @@ #include <GLES2/gl2ext.h> // glEGLImageTargetTexture2DOES #include <GLES3/gl32.h> -// inotify is libc (not wlroots), so it does NOT go through wlr.hpp. Integrated -// into the kernel's wl_event_loop via wl_event_loop_add_fd so the fd is polled, -// never blocking the loop. -#include <sys/inotify.h> -#include <unistd.h> - #include <algorithm> #include <cstdint> #include <cstdlib> @@ -384,6 +379,10 @@ struct Surface { std::string rml_path; // the spec's path (as the extension passed it) std::string resolved_path; // absolute path actually loaded (file-backed only) bool doc_loaded = false; + // Dev asset hot-reload (UNBOX_DEV): a watch on resolved_path's dir whose + // callback flags this surface for reload. Released when the Surface dies + // (stops the underlying inotify watch). Inactive for inline/non-dev surfaces. + FileWatch asset_watch; // Data bindings. Each bound scalar pairs a getter with a stable slot the // getter writes into; RmlUi binds to the slot's address. Bound BEFORE the @@ -605,36 +604,21 @@ struct Substrate::Impl { std::list<Surface> surfaces; // stable addresses (handles borrow Surface*) - // ---- Dev hot-reload watcher (UNBOX_DEV-gated; see top-of-file helpers) ---- - // ONE inotify fd integrated into the wl_event_loop. We watch DIRECTORIES (not - // inodes) because editors save via temp-file + rename — IN_CLOSE_WRITE / - // IN_MOVED_TO on the dir, matched by filename, catches that reliably. Each - // watched dir maps to the file basenames in it that back a surface. Reloads - // are coalesced and applied at the next tick_all (debounce within a frame). - wl_event_loop* loop = nullptr; - int inotify_fd = -1; - wl_event_source* inotify_source = nullptr; - // inotify watch descriptor (one per watched directory) -> directory path. - std::unordered_map<int, std::string> watch_dirs; - // directory path -> the surfaces whose document (or same-dir RCSS/assets) - // live there. A change to ANY file in the dir reloads them (covers the .rml - // AND a sibling .rcss the document <link>s, with no need to parse links). - std::unordered_map<std::string, std::vector<Surface*>> dir_surfaces; + // ---- Dev asset hot-reload (UNBOX_DEV-gated) ---- + // The substrate does NOT own an inotify fd: it borrows the kernel's ONE + // shared FileWatcher (injected at create). Each file-backed surface holds a + // FileWatch whose callback flags the surface in `pending_reloads`; the + // reload itself is applied (coalesced) at the next tick_all so it happens + // inside the frame, on the GL context, like every other render step. Only + // the DECISION to watch UI assets is UNBOX_DEV-gated — the watcher infra is + // always available (config watching via Host::watch_file is ungated). + FileWatcher* watcher = nullptr; // kernel-owned borrow (may be null: no loop) // surfaces flagged dirty by a file event, drained (coalesced) at tick_all. std::vector<Surface*> pending_reloads; - // Bring up the inotify fd + its wl_event_loop source (dev only). Idempotent; - // a failure leaves inotify_fd < 0 (watching simply disabled, no error). - void init_watcher(wl_event_loop* event_loop); - // Remove the event source + close the inotify fd (teardown / no surfaces). - void teardown_watcher(); - // Watch the directory of `abs_file` and remember that `s` depends on it. - // No-op if hot-reload is disabled or the fd is unusable. - void watch_file_for(Surface* s, const std::string& abs_file); - // Stop tracking `s` across all watched dirs (on destroy / before re-watch). + // Stop flagging `s` for reload (on destroy). The FileWatch on the Surface + // itself stops the inotify watch when the Surface is erased. void unwatch_surface(Surface* s); - // Drain the inotify fd; flag dependent surfaces for reload (coalesced). - void on_inotify_readable(); // Reload `s`'s document from its file, preserving context/model/bindings/ // geometry/visibility/previews; error-isolated (keeps the old doc on a bad // parse). Returns true if a NEW document was installed. Caller holds nothing; @@ -812,50 +796,7 @@ bool Substrate::Impl::init_surface_gl(Surface& s) { return true; } -// ---- Document load (first) + dev hot-reload -------------------------------- - -namespace { -// wl_event_loop fd callback: just drains inotify (never blocks the loop). -auto inotify_dispatch(int /*fd*/, std::uint32_t /*mask*/, void* data) -> int { - static_cast<Substrate::Impl*>(data)->on_inotify_readable(); - return 0; -} -} // namespace - -void Substrate::Impl::init_watcher(wl_event_loop* event_loop) { - loop = event_loop; - if (!hot_reload_enabled() || loop == nullptr || inotify_fd >= 0) { - return; - } - inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); - if (inotify_fd < 0) { - wlr_log(WLR_ERROR, "ui-substrate: inotify_init1 failed; hot-reload disabled"); - return; - } - inotify_source = wl_event_loop_add_fd(loop, inotify_fd, WL_EVENT_READABLE, - inotify_dispatch, this); - if (inotify_source == nullptr) { - close(inotify_fd); - inotify_fd = -1; - wlr_log(WLR_ERROR, "ui-substrate: wl_event_loop_add_fd failed; hot-reload disabled"); - return; - } - wlr_log(WLR_INFO, "ui-substrate: dev hot-reload ON (inotify watching asset dirs)"); -} - -void Substrate::Impl::teardown_watcher() { - if (inotify_source != nullptr) { - wl_event_source_remove(inotify_source); - inotify_source = nullptr; - } - if (inotify_fd >= 0) { - close(inotify_fd); // also drops all inotify watches - inotify_fd = -1; - } - watch_dirs.clear(); - dir_surfaces.clear(); - pending_reloads.clear(); -} +// ---- Document load (first) + dev asset hot-reload -------------------------- Rml::ElementDocument* Substrate::Impl::load_document_first(Surface& s) { Rml::ElementDocument* doc = nullptr; @@ -867,9 +808,22 @@ Rml::ElementDocument* Substrate::Impl::load_document_first(Surface& s) { s.rml_path.c_str(), s.resolved_path.c_str()); return nullptr; } - // Dev-only: watch the document's directory for editor saves. - if (hot_reload_enabled()) { - watch_file_for(&s, s.resolved_path); + // Dev-only: register an asset hot-reload watch on the kernel's SHARED + // file watcher (the same machinery Host::watch_file uses). The callback + // flags this surface for reload, applied (coalesced) at the next + // tick_all on the GL context. Only this DECISION is UNBOX_DEV-gated; the + // watcher infra itself is always available. + if (hot_reload_enabled() && watcher != nullptr) { + Surface* sp = &s; + s.asset_watch = watcher->add( + s.resolved_path, + [this, sp] { + if (std::find(pending_reloads.begin(), pending_reloads.end(), sp) == + pending_reloads.end()) { + pending_reloads.push_back(sp); + } + }, + s.who); } } else { doc = s.context->LoadDocumentFromMemory(s.rml_inline); @@ -882,74 +836,14 @@ Rml::ElementDocument* Substrate::Impl::load_document_first(Surface& s) { return doc; } -void Substrate::Impl::watch_file_for(Surface* s, const std::string& abs_file) { - if (inotify_fd < 0) { - return; - } - std::error_code ec; - const std::string dir = std::filesystem::path(abs_file).parent_path().string(); - if (dir.empty()) { - return; - } - auto& deps = dir_surfaces[dir]; - if (std::find(deps.begin(), deps.end(), s) == deps.end()) { - deps.push_back(s); - } - // Add the dir watch once (inotify_add_watch on the same path returns the - // SAME wd, so this is idempotent — re-arming after a watched file is - // replaced is automatic since we watch the directory, not the inode). - const int wd = inotify_add_watch(inotify_fd, dir.c_str(), - IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE); - if (wd >= 0) { - watch_dirs[wd] = dir; - } -} - void Substrate::Impl::unwatch_surface(Surface* s) { - for (auto it = dir_surfaces.begin(); it != dir_surfaces.end();) { - auto& deps = it->second; - deps.erase(std::remove(deps.begin(), deps.end(), s), deps.end()); - if (deps.empty()) { - it = dir_surfaces.erase(it); - } else { - ++it; - } - } + // The Surface's own FileWatch (asset_watch) stops the inotify watch when the + // Surface is erased; here we just drop any queued reload so a destroyed + // surface is never reloaded. pending_reloads.erase(std::remove(pending_reloads.begin(), pending_reloads.end(), s), pending_reloads.end()); } -void Substrate::Impl::on_inotify_readable() { - // Drain ALL queued inotify events (the fd is non-blocking; one readable - // notification may carry many). Flag every surface in a changed directory - // for reload — coalesced into pending_reloads (dedup) and applied once at - // the next tick_all, so a temp+rename burst causes a single reload. - alignas(struct inotify_event) char buf[4096]; - for (;;) { - const ssize_t n = read(inotify_fd, buf, sizeof(buf)); - if (n <= 0) { - break; // EAGAIN (drained) or closed - } - std::size_t off = 0; - while (off + sizeof(struct inotify_event) <= static_cast<std::size_t>(n)) { - auto* ev = reinterpret_cast<struct inotify_event*>(buf + off); - auto wd_it = watch_dirs.find(ev->wd); - if (wd_it != watch_dirs.end()) { - auto deps_it = dir_surfaces.find(wd_it->second); - if (deps_it != dir_surfaces.end()) { - for (Surface* s : deps_it->second) { - if (std::find(pending_reloads.begin(), pending_reloads.end(), s) == - pending_reloads.end()) { - pending_reloads.push_back(s); - } - } - } - } - off += sizeof(struct inotify_event) + ev->len; - } - } -} - bool Substrate::Impl::reload_surface(Surface& s) { // Only file-backed, already-loaded surfaces reload. The data model + the // extension's registered bind_*/bind_list*/bind_event getters are CONTEXT- @@ -1192,10 +1086,11 @@ bool Substrate::Impl::resize_surface_gl(Surface& s, int w, int h) { } void Substrate::Impl::destroy_surface(Surface* s) { - // Drop the hot-reload watch tracking for this surface FIRST (so a queued - // file event can never reload a dying surface). The inotify dir watch itself - // is left armed (cheap) and is reaped at teardown_watcher. + // Drop any queued reload for this surface FIRST (so a coalesced file event + // can never reload a dying surface). The surface's FileWatch (asset_watch) + // stops the underlying inotify watch when the Surface is erased below. unwatch_surface(s); + s->asset_watch.reset(); const bool cur = gl.make_current(); if (s->scene_buffer != nullptr) { wlr_scene_node_destroy(&s->scene_buffer->node); @@ -1457,14 +1352,14 @@ void Substrate::Impl::destroy_preview(PreviewState* p) { // ---- Substrate (private surface) -------------------------------------------- auto Substrate::create(EGLDisplay egl_display, wlr_allocator* allocator, wlr_renderer* renderer, - wl_event_loop* loop, SubstrateDisableFn disable) + FileWatcher* watcher, SubstrateDisableFn disable) -> std::unique_ptr<Substrate> { auto impl = std::make_unique<Impl>(); impl->allocator = allocator; impl->renderer = renderer; impl->disable = std::move(disable); + impl->watcher = watcher; // shared kernel-owned file watcher (asset hot-reload) impl->gl.init(egl_display); // sets gl.ok; failure => unavailable substrate - impl->init_watcher(loop); // dev-only (UNBOX_DEV); no-op otherwise return std::unique_ptr<Substrate>(new Substrate(std::move(impl))); } @@ -1482,7 +1377,8 @@ Substrate::~Substrate() { while (!impl_->surfaces.empty()) { impl_->destroy_surface(&impl_->surfaces.front()); } - impl_->teardown_watcher(); // remove the wl_event_loop source + close the fd + // The shared FileWatcher is kernel-owned (NOT the substrate's): each + // surface's FileWatch released above already removed its asset watch. impl_->gl.teardown(); } diff --git a/packages/kernel/src/ui_substrate.hpp b/packages/kernel/src/ui_substrate.hpp index 4ba477f..32072b4 100644 --- a/packages/kernel/src/ui_substrate.hpp +++ b/packages/kernel/src/ui_substrate.hpp @@ -36,6 +36,11 @@ class RenderInterface_GL3; namespace unbox::kernel { +// The kernel's shared file watcher (src/file_watcher.hpp); the substrate borrows +// it for (UNBOX_DEV-gated) asset hot-reload. Forward-declared to keep the header +// free of inotify internals. +class FileWatcher; + // Callback the substrate invokes to disable an extension whose data-event // callback threw — injected by the kernel (Server::Impl). Mirrors the bus's // detail::DisableSink but scoped to the substrate so ui.hpp carries no kernel @@ -124,11 +129,11 @@ class Substrate { public: // Build the substrate on the wlr renderer's EGLDisplay. `egl_display` may // be EGL_NO_DISPLAY (no gles2 renderer) — then available() is false and - // create_surface yields nullptr. `loop` is the kernel's wl_event_loop, used - // (dev only, UNBOX_DEV-gated) to poll the hot-reload inotify fd without ever - // blocking; pass nullptr to disable watching. Never throws. + // create_surface yields nullptr. `watcher` is the kernel's ONE shared + // FileWatcher, used (dev only, UNBOX_DEV-gated) for asset hot-reload; pass + // nullptr to disable watching. Never throws. static auto create(EGLDisplay egl_display, wlr_allocator* allocator, - wlr_renderer* renderer, wl_event_loop* loop, + wlr_renderer* renderer, FileWatcher* watcher, SubstrateDisableFn disable) -> std::unique_ptr<Substrate>; ~Substrate(); |
