summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src/file_watcher.hpp
blob: 00806db898ff6321d6559e443c1ee448a35bcf95 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#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;

    // Watch the DIRECTORY `dir` for a change to ANY file within it (not a single
    // basename). Used by the ui-substrate's asset hot-reload: a document and its
    // `<link>`ed RCSS/assets live in the same dir, and editing ANY of them must
    // reload — without parsing the document's link set. Same coalescing / error
    // isolation / RAII as add(). `dir` should be an absolute directory path.
    [[nodiscard]] auto add_dir(const std::string& dir, 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; EMPTY = any file
        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