GCC Code Coverage Report


Directory: ./
File: FileMonitor.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 150 165 90.9%
Functions: 13 15 86.7%
Branches: 136 227 59.9%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the GPL v2 as described in the
10 * included LICENSE.GPL file.
11 */
12
13 #if __has_include(<sys/inotify.h>)
14 #define HAS_INOTIFY 1
15 #include <sys/inotify.h>
16 #endif
17
18 #include <cstring>
19 #include <unistd.h>
20 #include <vector>
21
22 #include "FileMonitor.h"
23 #include "Timers.h"
24 #include "common_mini.h"
25
26 class FileMonitorInfo {
27 public:
28 20940 FileMonitorInfo() = default;
29 20718 ~FileMonitorInfo() = default;
30
31 19051 bool checkForChanges(const std::string& fn) {
32 19051 uint64_t newTimestamp = FileTimestamp(fn);
33
2/2
✓ Branch 0 taken 4946 times.
✓ Branch 1 taken 14105 times.
19051 if (newTimestamp != timestamp) {
34 4946 timestamp = newTimestamp;
35 4946 return true; // File has changed
36 }
37 14105 return false; // No change
38 }
39
40 uint64_t timestamp = 0; // Last modified timestamp
41 int inotify_watch_fd = -1; // Inotify watch descriptor
42 bool isDirectory = false; // Is this a directory, just looking for create/delete events
43
44 std::map<std::string, std::function<void()>> callbacks; // Callbacks for this file
45 };
46
47 FileMonitor FileMonitor::INSTANCE;
48 2346 FileMonitor::FileMonitor() :
49 2346 inotify_fd_(-1) {
50 #ifdef HAS_INOTIFY
51 2346 inotify_fd_ = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
52 #endif
53 2346 }
54 458 FileMonitor::~FileMonitor() {
55 #ifdef HAS_INOTIFY
56
1/2
✓ Branch 0 taken 458 times.
✗ Branch 1 not taken.
458 if (inotify_fd_ >= 0) {
57 458 close(inotify_fd_);
58 458 inotify_fd_ = -1;
59 }
60 #endif
61 458 }
62
63 1885 void FileMonitor::Initialize(std::map<int, std::function<bool(int)>>& callbacks) {
64
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 if (inotify_fd_ >= 0) {
65
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
3770 callbacks[inotify_fd_] = [this](int fd) {
66 722 this->fileChangedEvent();
67 722 return false;
68 1885 };
69 } else {
70 ✗ Timers::INSTANCE.addPeriodicTimer("FileMonitor", 2000, [this]() {
71 ✗ this->fileChangedEvent();
72 ✗ });
73 }
74 1885 }
75 1867 void FileMonitor::Cleanup() {
76 #ifdef HAS_INOTIFY
77
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 std::lock_guard<std::mutex> lock(mutex_);
78
2/2
✓ Branch 0 taken 6779 times.
✓ Branch 1 taken 1867 times.
8646 for (const auto& entry : fileMapping_) {
79 6779 inotify_rm_watch(inotify_fd_, entry.first);
80 }
81 #endif
82
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1867 times.
1867 if (inotify_fd_ < 0) {
83 ✗ Timers::INSTANCE.stopPeriodicTimer("FileMonitor");
84 }
85 1867 fileMapping_.clear();
86 1867 files_.clear();
87 1867 }
88 20938 FileMonitor& FileMonitor::AddFile(const std::string& id, const std::string& file, const std::function<void()>& callback, bool modificationsOnly) {
89
1/2
✓ Branch 0 taken 20938 times.
✗ Branch 1 not taken.
20938 std::lock_guard<std::mutex> lock(mutex_);
90
91
1/2
✓ Branch 0 taken 20938 times.
✗ Branch 1 not taken.
20938 auto it = files_.find(file);
92
2/2
✓ Branch 0 taken 19051 times.
✓ Branch 1 taken 1887 times.
20938 if (it == files_.end()) {
93
2/4
✓ Branch 0 taken 19051 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19051 times.
✗ Branch 3 not taken.
19051 files_[file].checkForChanges(file);
94 }
95
1/2
✓ Branch 0 taken 20938 times.
✗ Branch 1 not taken.
20938 auto& fileInfo = files_[file];
96
2/4
✓ Branch 0 taken 20938 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20938 times.
✗ Branch 3 not taken.
20938 fileInfo.callbacks[id] = callback;
97 #ifdef HAS_INOTIFY
98
2/2
✓ Branch 0 taken 19050 times.
✓ Branch 1 taken 1888 times.
20938 if (!modificationsOnly) {
99
1/2
✓ Branch 0 taken 19050 times.
✗ Branch 1 not taken.
19050 std::string dir = file.substr(0, file.find_last_of("/") + 1);
100
1/2
✓ Branch 0 taken 19050 times.
✗ Branch 1 not taken.
19050 auto dit = files_.find(dir);
101
2/2
✓ Branch 0 taken 1889 times.
✓ Branch 1 taken 17161 times.
19050 if (dit == files_.end()) {
102 // IN_MOVED_TO catches files that appear via an atomic rename
103 // (write-temp-then-rename), which is how config files are written so
104 // a reader never sees a partially-written file. Without it, watchers
105 // on rename-replaced files would silently stop firing.
106 1889 int inotify_watch_fd = inotify_add_watch(inotify_fd_, dir.c_str(), IN_CREATE | IN_DELETE | IN_MOVED_TO);
107
2/4
✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1889 times.
✗ Branch 3 not taken.
1889 fileMapping_[inotify_watch_fd] = dir;
108
1/2
✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
1889 files_[dir].inotify_watch_fd = inotify_watch_fd;
109
1/2
✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
1889 files_[dir].isDirectory = true; // Mark as directory for create/delete events
110 }
111 19050 }
112
7/8
✓ Branch 0 taken 20938 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5675 times.
✓ Branch 3 taken 15263 times.
✓ Branch 4 taken 4946 times.
✓ Branch 5 taken 729 times.
✓ Branch 6 taken 4946 times.
✓ Branch 7 taken 15992 times.
20938 if (FileExists(file) && fileInfo.inotify_watch_fd < 0) {
113 // IN_CLOSE_WRITE, not IN_MODIFY: one save is an open with O_TRUNC and
114 // one or more writes, each its own IN_MODIFY, so every save ran the
115 // callbacks two or more times -- the first time on a truncated file.
116 // A channel output reload per event re-created every output twice.
117 4946 int inotify_watch_fd = inotify_add_watch(inotify_fd_, file.c_str(), IN_CLOSE_WRITE);
118
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4946 times.
4946 if (inotify_watch_fd < 0) {
119 ✗ return *this;
120 }
121
2/4
✓ Branch 0 taken 4946 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4946 times.
✗ Branch 3 not taken.
4946 fileMapping_[inotify_watch_fd] = file;
122 4946 fileInfo.inotify_watch_fd = inotify_watch_fd;
123 }
124 #endif
125 20938 return *this;
126 20938 }
127 7449 FileMonitor& FileMonitor::RemoveFile(const std::string& id, const std::string& file) {
128
1/2
✓ Branch 0 taken 7449 times.
✗ Branch 1 not taken.
7449 std::lock_guard<std::mutex> lock(mutex_);
129
1/2
✓ Branch 0 taken 7449 times.
✗ Branch 1 not taken.
7449 auto it = files_.find(file);
130
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7448 times.
7449 if (it != files_.end()) {
131
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 it->second.callbacks.erase(id);
132
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 if (it->second.callbacks.empty() && !it->second.isDirectory) {
133 // If no callbacks left, remove the file from monitoring
134 #ifdef HAS_INOTIFY
135
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (it->second.inotify_watch_fd >= 0) {
136 1 inotify_rm_watch(inotify_fd_, it->second.inotify_watch_fd);
137
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 fileMapping_.erase(it->second.inotify_watch_fd);
138 }
139 #endif
140
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 files_.erase(it);
141 }
142 }
143 7449 return *this;
144 7449 }
145
146 // Copy the matching callbacks out and invoke them with mutex_ released.
147 // Callbacks do arbitrary heavyweight work (reloading channel outputs, plugin
148 // teardown); if one crashes, the process exit path runs ~Plugin -> RemoveFile
149 // on this same thread, and holding mutex_ here turns that crash into a
150 // self-deadlocked zombie instead of a clean crash-exit.
151 // `id` empty means "every callback on this file".
152 16982 void FileMonitor::fireCallbacks(const std::string& id, const std::string& file) {
153 16982 std::vector<std::function<void()>> callbacks;
154 {
155
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 std::lock_guard<std::mutex> lock(mutex_);
156
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 auto it = files_.find(file);
157
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 if (it != files_.end()) {
158
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16982 times.
16982 if (id.empty()) {
159 ✗ for (const auto& callback : it->second.callbacks) {
160 ✗ callbacks.push_back(callback.second);
161 }
162 } else {
163
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 auto cit = it->second.callbacks.find(id);
164
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 if (cit != it->second.callbacks.end()) {
165
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 callbacks.push_back(cit->second);
166 }
167 }
168 }
169 16982 }
170
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 if (!callbacks.empty()) {
171
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 std::lock_guard<std::mutex> cbLock(callbackMutex_);
172
2/2
✓ Branch 0 taken 16982 times.
✓ Branch 1 taken 16982 times.
33964 for (auto& callback : callbacks) {
173
1/2
✓ Branch 0 taken 16982 times.
✗ Branch 1 not taken.
16982 callback();
174 }
175 16982 }
176 16982 }
177
178 ✗ FileMonitor& FileMonitor::TriggerFileChanged(const std::string& file) {
179 ✗ fireCallbacks("", file);
180 ✗ return *this;
181 }
182
183 16982 FileMonitor& FileMonitor::TriggerFileChanged(const std::string& id, const std::string& file) {
184 // An empty id would mean "all" to fireCallbacks(); AddFile() allows one, so
185 // keep the two overloads from silently converging on different behaviour.
186
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16982 times.
16982 if (id.empty()) {
187 ✗ return *this;
188 }
189 16982 fireCallbacks(id, file);
190 16982 return *this;
191 }
192
193 722 void FileMonitor::fileChangedEvent() {
194 #ifdef HAS_INOTIFY
195 char buffer[16384];
196 size_t buffer_i;
197 struct inotify_event* pevent;
198 ssize_t r;
199 size_t event_size;
200
201 722 memset(buffer, 0, sizeof(buffer));
202
1/2
✓ Branch 0 taken 722 times.
✗ Branch 1 not taken.
722 r = read(inotify_fd_, buffer, sizeof(buffer));
203
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 722 times.
722 if (r <= 0) {
204 ✗ return;
205 }
206 722 buffer_i = 0;
207
2/2
✓ Branch 0 taken 904 times.
✓ Branch 1 taken 115 times.
1019 while (buffer_i < r) {
208 /* Parse events and queue them. */
209 904 pevent = (struct inotify_event*)&buffer[buffer_i];
210 904 event_size = offsetof(struct inotify_event, name) + pevent->len;
211 904 buffer_i += event_size;
212
213 // Callbacks are collected under the lock but invoked after it is
214 // released -- see TriggerFileChanged for why holding mutex_ across a
215 // callback is not survivable.
216 904 std::vector<std::function<void()>> pendingCallbacks;
217
1/2
✓ Branch 0 taken 904 times.
✗ Branch 1 not taken.
904 std::unique_lock<std::mutex> lock(mutex_);
218
2/4
✓ Branch 0 taken 904 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 904 times.
✗ Branch 3 not taken.
904 std::string path = fileMapping_[pevent->wd];
219
220 // printf("%d) %s: (%s) id: %d mask: %X\n", buffer_i, path.c_str(), pevent->name, pevent->wd, pevent->mask);
221
222
2/2
✓ Branch 0 taken 118 times.
✓ Branch 1 taken 786 times.
904 if (pevent->mask & IN_IGNORED) {
223 // The kernel auto-removed this watch -- e.g. the file was deleted, or
224 // it was replaced via an atomic rename (write-temp-then-rename), which
225 // unlinks the old inode. Drop our stale wd mapping and clear the
226 // file's watch descriptor so a subsequent IN_CREATE/IN_MOVED_TO for
227 // the same name re-arms an inode watch on the new file.
228
1/2
✓ Branch 0 taken 118 times.
✗ Branch 1 not taken.
118 auto mit = fileMapping_.find(pevent->wd);
229
1/2
✓ Branch 0 taken 118 times.
✗ Branch 1 not taken.
118 if (mit != fileMapping_.end()) {
230
1/2
✓ Branch 0 taken 118 times.
✗ Branch 1 not taken.
118 auto fit = files_.find(mit->second);
231
5/6
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 113 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 113 times.
118 if (fit != files_.end() && fit->second.inotify_watch_fd == pevent->wd) {
232 5 fit->second.inotify_watch_fd = -1;
233 }
234
1/2
✓ Branch 0 taken 118 times.
✗ Branch 1 not taken.
118 fileMapping_.erase(mit);
235 }
236 118 continue;
237
2/2
✓ Branch 0 taken 264 times.
✓ Branch 1 taken 522 times.
904 } else if (pevent->mask & (IN_CREATE | IN_MOVED_TO)) {
238 // A watched file appeared: either created in place (IN_CREATE) or
239 // atomically renamed into place (IN_MOVED_TO). For a rename over an
240 // existing file the previous inode watch is gone (its IN_IGNORED is
241 // handled above or still queued), so re-arm an IN_CLOSE_WRITE watch on the
242 // current inode before notifying.
243
1/2
✓ Branch 0 taken 264 times.
✗ Branch 1 not taken.
264 path += pevent->name;
244
1/2
✓ Branch 0 taken 264 times.
✗ Branch 1 not taken.
264 auto it = files_.find(path);
245
2/2
✓ Branch 0 taken 125 times.
✓ Branch 1 taken 139 times.
264 if (it != files_.end()) {
246 125 bool notify = false;
247
2/2
✓ Branch 0 taken 114 times.
✓ Branch 1 taken 11 times.
125 if (pevent->mask & IN_MOVED_TO) {
248 // New inode in place: drop any stale watch on the replaced
249 // inode (the kernel auto-removes it) and always notify.
250
2/2
✓ Branch 0 taken 112 times.
✓ Branch 1 taken 2 times.
114 if (it->second.inotify_watch_fd >= 0) {
251
1/2
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
112 fileMapping_.erase(it->second.inotify_watch_fd);
252 112 it->second.inotify_watch_fd = -1;
253 }
254 114 notify = true;
255 }
256
1/2
✓ Branch 0 taken 125 times.
✗ Branch 1 not taken.
125 if (it->second.inotify_watch_fd < 0) {
257 125 int wd = inotify_add_watch(inotify_fd_, path.c_str(), IN_CLOSE_WRITE);
258
1/2
✓ Branch 0 taken 125 times.
✗ Branch 1 not taken.
125 if (wd >= 0) {
259 125 it->second.inotify_watch_fd = wd;
260
2/4
✓ Branch 0 taken 125 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 125 times.
✗ Branch 3 not taken.
125 fileMapping_[wd] = path;
261 }
262 125 notify = true;
263 }
264
1/2
✓ Branch 0 taken 125 times.
✗ Branch 1 not taken.
125 if (notify) {
265
2/2
✓ Branch 0 taken 132 times.
✓ Branch 1 taken 125 times.
257 for (const auto& callback : it->second.callbacks) {
266
1/2
✓ Branch 0 taken 132 times.
✗ Branch 1 not taken.
132 pendingCallbacks.push_back(callback.second);
267 }
268 }
269 }
270
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 516 times.
522 } else if (pevent->mask & IN_DELETE) {
271
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 path += pevent->name;
272
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 auto it = files_.find(path);
273
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 if (it != files_.end()) {
274
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (it->second.inotify_watch_fd >= 0) {
275 ✗ inotify_rm_watch(inotify_fd_, it->second.inotify_watch_fd);
276 ✗ fileMapping_.erase(it->second.inotify_watch_fd);
277 ✗ it->second.inotify_watch_fd = -1; // Reset watch descriptor
278 }
279 // Notify whether or not the file's own watch is still here:
280 // the unlink frees the inode first, so that watch's IN_IGNORED
281 // is usually queued ahead of this and has already cleared it.
282 // Gating on it meant a deleted file was never reported.
283
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 for (const auto& callback : it->second.callbacks) {
284
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 pendingCallbacks.push_back(callback.second);
285 }
286 }
287
1/2
✓ Branch 0 taken 516 times.
✗ Branch 1 not taken.
516 } else if (pevent->mask & IN_CLOSE_WRITE) {
288
1/2
✓ Branch 0 taken 516 times.
✗ Branch 1 not taken.
516 auto it = files_.find(path);
289
1/2
✓ Branch 0 taken 516 times.
✗ Branch 1 not taken.
516 if (it != files_.end()) {
290
2/2
✓ Branch 0 taken 576 times.
✓ Branch 1 taken 516 times.
1092 for (const auto& callback : it->second.callbacks) {
291
1/2
✓ Branch 0 taken 576 times.
✗ Branch 1 not taken.
576 pendingCallbacks.push_back(callback.second);
292 }
293 }
294 }
295
1/2
✓ Branch 0 taken 786 times.
✗ Branch 1 not taken.
786 lock.unlock();
296
2/2
✓ Branch 0 taken 644 times.
✓ Branch 1 taken 142 times.
786 if (!pendingCallbacks.empty()) {
297
1/2
✓ Branch 0 taken 644 times.
✗ Branch 1 not taken.
644 std::lock_guard<std::mutex> cbLock(callbackMutex_);
298
2/2
✓ Branch 0 taken 711 times.
✓ Branch 1 taken 644 times.
1355 for (auto& callback : pendingCallbacks) {
299
1/2
✓ Branch 0 taken 711 times.
✗ Branch 1 not taken.
711 callback();
300 }
301 644 }
302
2/2
✓ Branch 0 taken 666 times.
✓ Branch 1 taken 120 times.
786 if (buffer_i >= r) {
303
1/2
✓ Branch 0 taken 666 times.
✗ Branch 1 not taken.
666 r = read(inotify_fd_, buffer, sizeof(buffer));
304
2/2
✓ Branch 0 taken 607 times.
✓ Branch 1 taken 59 times.
666 if (r <= 0) {
305 607 break; // No more events to process
306 }
307 59 buffer_i = 0;
308 }
309
9/9
✓ Branch 0 taken 179 times.
✓ Branch 1 taken 118 times.
✓ Branch 2 taken 607 times.
✓ Branch 3 taken 179 times.
✓ Branch 4 taken 118 times.
✓ Branch 5 taken 607 times.
✓ Branch 6 taken 179 times.
✓ Branch 7 taken 118 times.
✓ Branch 8 taken 607 times.
2354 }
310 #else
311 std::vector<std::function<void()>> pendingCallbacks;
312 {
313 std::lock_guard<std::mutex> lock(mutex_);
314 for (auto& fileEntry : files_) {
315 const std::string& file = fileEntry.first;
316 FileMonitorInfo& fileInfo = fileEntry.second;
317 if (fileInfo.checkForChanges(file)) {
318 for (const auto& callback : fileInfo.callbacks) {
319 pendingCallbacks.push_back(callback.second);
320 }
321 }
322 }
323 }
324 if (!pendingCallbacks.empty()) {
325 std::lock_guard<std::mutex> cbLock(callbackMutex_);
326 for (auto& callback : pendingCallbacks) {
327 callback();
328 }
329 }
330 #endif
331 }
332