GCC Code Coverage Report


Directory: ./
File: MDNSBackendAvahi.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 303 345 87.8%
Functions: 33 35 94.3%
Branches: 159 277 57.4%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2026 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 LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 // The Avahi mDNS backend. Everything here is the Avahi half of MDNSManager;
14 // the shared half -- the service name, what counts as a newly discovered host,
15 // and what is done with one -- stays in MDNSManager.cpp. MDNSBackend.h says
16 // how one backend gets chosen.
17
18 #include "fpp-pch.h"
19
20 #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds
21
22 #include "MDNSBackend.h"
23 #include "MDNSManager.h"
24
25 #include "common.h"
26
27 #include <atomic>
28 #include "EPollManager.h"
29 #include "log.h"
30
31 #include <chrono>
32 #include <cerrno>
33 #include <cstdio>
34 #include <cstring>
35 #include <net/if.h>
36 #include <unistd.h>
37 #include <vector>
38
39 #if HAVE_AVAHI
40
41 #include <avahi-client/client.h>
42 #include <avahi-client/lookup.h>
43 #include <avahi-client/publish.h>
44 #include <avahi-common/error.h>
45 #include <avahi-common/malloc.h>
46 #include <avahi-common/strlst.h>
47 #include <avahi-common/timeval.h>
48 #include <avahi-common/alternative.h>
49 #include <sys/timerfd.h>
50
51 // ── Custom AvahiPoll adapter ────────────────────────────────────────────
52 // Avahi forward-declares AvahiWatch and AvahiTimeout as opaque types.
53 // The poll adapter provides the concrete definitions and wires them into
54 // the main FPP event loop via EPollManager.
55 // ─────────────────────────────────────────────────────────────────────────
56
57
58 struct AvahiWatch {
59 int fd;
60 AvahiWatchEvent requested;
61 AvahiWatchEvent last;
62 AvahiWatchCallback callback;
63 void* userdata;
64 std::function<bool(int)> epollCb;
65 };
66
67 struct AvahiTimeout {
68 int timerFd;
69 AvahiTimeoutCallback callback;
70 void* userdata;
71 std::function<bool(int)> epollCb;
72 bool registered;
73 };
74
75 12 static uint32_t avahiToEpoll(AvahiWatchEvent e) {
76 12 uint32_t ep = 0;
77
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if (e & AVAHI_WATCH_IN) ep |= EPOLLIN;
78
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (e & AVAHI_WATCH_OUT) ep |= EPOLLOUT;
79 // EPOLLERR and EPOLLHUP are always reported by epoll regardless of mask
80
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 return ep ? ep : EPOLLIN;
81 }
82
83 1087 static void armTimerFd(int tfd, const struct timeval* tv) {
84 1087 struct itimerspec its = {};
85
2/2
✓ Branch 0 taken 859 times.
✓ Branch 1 taken 228 times.
1087 if (tv) {
86 struct timeval now;
87 859 gettimeofday(&now, NULL);
88 859 int64_t delayUs = ((int64_t)tv->tv_sec - now.tv_sec) * 1000000
89 859 + (tv->tv_usec - now.tv_usec);
90
2/2
✓ Branch 0 taken 445 times.
✓ Branch 1 taken 414 times.
859 if (delayUs < 1)
91 445 delayUs = 1;
92 859 its.it_value.tv_sec = delayUs / 1000000;
93 859 its.it_value.tv_nsec = (delayUs % 1000000) * 1000;
94 }
95 // tv == NULL → its is all-zero → disarms the timer
96 1087 timerfd_settime(tfd, 0, &its, NULL);
97 1087 }
98
99 // ── AvahiPoll function table ──
100
101 12 static AvahiWatch* fpp_watch_new(const AvahiPoll* api, int fd,
102 AvahiWatchEvent events,
103 AvahiWatchCallback callback, void* userdata) {
104
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 auto* w = new AvahiWatch{fd, events, (AvahiWatchEvent)0, callback, userdata, {}};
105
106 283 w->epollCb = [w](int) -> bool {
107 259 w->last = w->requested;
108 259 w->callback(w, w->fd, w->last, w->userdata);
109 259 return false;
110 12 };
111
112 12 EPollManager::INSTANCE.addFileDescriptor(fd, w->epollCb);
113 12 uint32_t ep = avahiToEpoll(events);
114
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (ep != EPOLLIN) {
115 ✗ EPollManager::INSTANCE.updateFileDescriptorEvents(fd, ep);
116 }
117 12 return w;
118 }
119
120 ✗ static void fpp_watch_update(AvahiWatch* w, AvahiWatchEvent events) {
121 ✗ w->requested = events;
122 ✗ EPollManager::INSTANCE.updateFileDescriptorEvents(w->fd, avahiToEpoll(events));
123 ✗ }
124
125 ✗ static AvahiWatchEvent fpp_watch_get_events(AvahiWatch* w) {
126 ✗ return w->last;
127 }
128
129 12 static void fpp_watch_free(AvahiWatch* w) {
130
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (!w)
131 ✗ return;
132 12 EPollManager::INSTANCE.removeFileDescriptor(w->fd);
133
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 delete w;
134 }
135
136 425 static AvahiTimeout* fpp_timeout_new(const AvahiPoll* api,
137 const struct timeval* tv,
138 AvahiTimeoutCallback callback,
139 void* userdata) {
140
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 425 times.
425 auto* t = new AvahiTimeout{-1, callback, userdata, {}, false};
141 425 t->timerFd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
142
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 425 times.
425 if (t->timerFd < 0) {
143 ✗ LogWarn(VB_SYNC, "Failed to create timerfd for Avahi timeout\n");
144 ✗ delete t;
145 ✗ return nullptr;
146 }
147
148 1273 t->epollCb = [t](int) -> bool {
149 uint64_t expirations;
150
1/2
✓ Branch 0 taken 423 times.
✗ Branch 1 not taken.
423 ssize_t n = read(t->timerFd, &expirations, sizeof(expirations));
151 (void)n;
152
1/2
✓ Branch 0 taken 423 times.
✗ Branch 1 not taken.
423 t->callback(t, t->userdata);
153 423 return false;
154 425 };
155
156 425 EPollManager::INSTANCE.addFileDescriptor(t->timerFd, t->epollCb);
157 425 t->registered = true;
158 425 armTimerFd(t->timerFd, tv);
159 425 return t;
160 }
161
162 662 static void fpp_timeout_update(AvahiTimeout* t, const struct timeval* tv) {
163
2/4
✓ Branch 0 taken 662 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 662 times.
662 if (!t || t->timerFd < 0)
164 ✗ return;
165 662 armTimerFd(t->timerFd, tv);
166 }
167
168 425 static void fpp_timeout_free(AvahiTimeout* t) {
169
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 425 times.
425 if (!t)
170 ✗ return;
171
1/2
✓ Branch 0 taken 425 times.
✗ Branch 1 not taken.
425 if (t->registered) {
172 425 EPollManager::INSTANCE.removeFileDescriptor(t->timerFd);
173 }
174
1/2
✓ Branch 0 taken 425 times.
✗ Branch 1 not taken.
425 if (t->timerFd >= 0) {
175 425 close(t->timerFd);
176 }
177
1/2
✓ Branch 0 taken 425 times.
✗ Branch 1 not taken.
425 delete t;
178 }
179
180
181 // ── Avahi service callbacks ─────────────────────────────────────────────
182
183 75 static void resolve_callback(AvahiServiceResolver* r,
184 AvahiIfIndex interface,
185 AvahiProtocol protocol,
186 AvahiResolverEvent event,
187 const char* name,
188 const char* type,
189 const char* domain,
190 const char* host_name,
191 const AvahiAddress* a,
192 uint16_t port,
193 AvahiStringList* txt,
194 AvahiLookupResultFlags flags,
195 void* userdata) {
196 75 MDNSManager* mgr = static_cast<MDNSManager*>(userdata);
197
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 75 times.
75 if (!mgr)
198 ✗ return;
199
200
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 if (event == AVAHI_RESOLVER_FOUND) {
201 char addr_buf[AVAHI_ADDRESS_STR_MAX];
202
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 avahi_address_snprint(addr_buf, sizeof(addr_buf), a);
203
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 std::string ip(addr_buf);
204 // An IPv6 link-local address (fe80::/10) is only usable together with the
205 // interface it was discovered on, so carry the zone id ("%eth0") along.
206 // Without it curl can't pick an interface and the address can't be reached
207 // on IPv6-only networks. The zone is harmless to the IPv4-only ping paths
208 // (they already can't parse a v6 address).
209
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (a->proto == AVAHI_PROTO_INET6 && interface != AVAHI_IF_UNSPEC &&
210
8/10
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 62 times.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✓ Branch 9 taken 66 times.
88 ip.rfind("fe80", 0) == 0 && ip.find('%') == std::string::npos) {
211 9 char ifname[IF_NAMESIZE] = {0};
212
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (if_indextoname(interface, ifname)) {
213
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 ip += "%";
214
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 ip += ifname;
215 }
216 }
217 // WLED nodes (and FPP's own _wled._tcp advertisement) are probed over
218 // HTTP rather than the FPP ping protocol.
219
3/4
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37 times.
✓ Branch 3 taken 38 times.
75 bool isWled = (type != nullptr && strstr(type, "_wled._tcp") != nullptr);
220 // delegate to instance method to avoid accessing private members from C callback
221
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 mgr->HandleResolveIP(ip, isWled);
222 75 }
223
224 75 avahi_service_resolver_free(r);
225 }
226
227 142 static void browse_callback(AvahiServiceBrowser* b,
228 AvahiIfIndex interface,
229 AvahiProtocol protocol,
230 AvahiBrowserEvent event,
231 const char* name,
232 const char* type,
233 const char* domain,
234 AvahiLookupResultFlags flags,
235 void* userdata) {
236 142 AvahiClient* client = avahi_service_browser_get_client(b);
237 142 MDNSManager* mgr = static_cast<MDNSManager*>(userdata);
238
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 142 times.
142 if (!mgr)
239 ✗ return;
240
241
3/3
✓ Branch 0 taken 88 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 44 times.
142 switch (event) {
242 88 case AVAHI_BROWSER_NEW:
243 // resolve service to get address
244 88 avahi_service_resolver_new(client, interface, protocol, name, type, domain, AVAHI_PROTO_UNSPEC, (AvahiLookupFlags)0, resolve_callback, mgr);
245 88 break;
246 10 case AVAHI_BROWSER_REMOVE: {
247 // callers will remove entries when services disappear via monitor
248 10 } break;
249 44 default:
250 44 break;
251 }
252 }
253
254 54 static void entry_group_callback(AvahiEntryGroup* g, AvahiEntryGroupState state, void* userdata) {
255 54 MDNSManager* mgr = static_cast<MDNSManager*>(userdata);
256
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 54 times.
54 if (!mgr)
257 ✗ return;
258
259
3/4
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 39 times.
54 switch (state) {
260 11 case AVAHI_ENTRY_GROUP_ESTABLISHED:
261 11 LogInfo(VB_SYNC, "MDNS service registered successfully\n");
262 11 break;
263 4 case AVAHI_ENTRY_GROUP_COLLISION: {
264 4 AvahiClient* client = avahi_entry_group_get_client(g);
265
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (client) {
266
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string was = mgr->ServiceName();
267
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 avahi_entry_group_reset(g);
268 // Pick an alternative name to avoid re-colliding
269
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 mgr->PickAlternativeServiceName();
270
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogWarn(VB_SYNC, "MDNS service name collision on '%s', publishing as '%s'\n",
271 was.c_str(), mgr->ServiceName().c_str());
272
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 mgr->RegisterService(client);
273
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 mgr->ScheduleServiceNameRetry();
274 4 }
275
276 4 break;
277 }
278 ✗ case AVAHI_ENTRY_GROUP_FAILURE:
279 ✗ LogErr(VB_SYNC, "MDNS entry group failure\n");
280 ✗ break;
281 39 default:
282 39 break;
283 }
284 }
285
286 // Defined in fppd.cpp; cleared when FPP begins shutting down. Declared the same way
287 // httpAPI.cpp does rather than adding a header for one flag.
288 extern std::atomic_int runMainFPPDLoop;
289
290 // WarningHolder matches on the id AND the exact message text, so the add and the
291 // remove have to use the same string. Naming them once is what keeps a warning from
292 // becoming permanent because the two copies drifted apart.
293 static constexpr int MDNS_WARNING_ID = 43;
294 static constexpr const char* MDNS_WARN_CLIENT = "Network discovery: mDNS/Avahi client failure (is avahi-daemon running?)";
295 static constexpr const char* MDNS_WARN_FPPD = "Network discovery: could not browse for FPP devices (_fppd._udp)";
296 static constexpr const char* MDNS_WARN_WLED = "Network discovery: could not browse for WLED devices (_wled._tcp)";
297
298 // Reconnect backoff, and how long the daemon has to stay gone before it counts as an
299 // abnormal condition. A restart of avahi-daemon is back inside a second or two, and
300 // on the way to a reboot it goes down some seconds before fppd is told to stop -
301 // warning on either of those puts a red banner on screen for something that is not
302 // wrong. Retries keep running for as long as fppd does, so a real outage is still
303 // picked up the moment it ends.
304 static constexpr int MDNS_RECONNECT_MIN_MS = 2000;
305 static constexpr int MDNS_RECONNECT_MAX_MS = 30000;
306 static constexpr long long MDNS_WARN_AFTER_MS = 30000;
307
308 // Deliberately not the wall clock: these boxes have no RTC, so the NTP step early in
309 // a boot would otherwise read as a half-hour outage.
310 3270 static long long MDNSMonotonicMS() {
311
1/2
✓ Branch 0 taken 3270 times.
✗ Branch 1 not taken.
3270 return std::chrono::duration_cast<std::chrono::milliseconds>(
312 6540 std::chrono::steady_clock::now().time_since_epoch())
313 6540 .count();
314 }
315
316 // -1 is set during teardown, after the main loop is already gone, so it is not
317 // "running" for our purposes even though it is truthy.
318 142 static bool fppdIsRunning() {
319 142 return runMainFPPDLoop > 0;
320 }
321
322 16 static void client_callback(AvahiClient* c, AvahiClientState state, void* userdata) {
323 16 MDNSManager* mgr = static_cast<MDNSManager*>(userdata);
324
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (!mgr)
325 ✗ return;
326
327
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 3 times.
16 if (state == AVAHI_CLIENT_S_RUNNING) {
328 // The client is up, so whatever we said about it being down is no longer
329 // true. Without this the warning stayed on screen for the rest of the
330 // session.
331 13 mgr->NoteClientRunning();
332
333 // Register local service. Back under the host name: this is also where we
334 // land after avahi re-establishes its host records, which is exactly when a
335 // stale echo can have pushed us onto "<host> #2".
336 13 mgr->ResetServiceName();
337 13 mgr->RegisterService(c);
338
339 // Create a service browser for _fppd._udp
340 13 void* sb = avahi_service_browser_new(c, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_fppd._udp", NULL, (AvahiLookupFlags)0, browse_callback, mgr);
341
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (!sb) {
342 ✗ LogErr(VB_SYNC, "Failed to create Avahi service browser for _fppd._udp: %s\n",
343 avahi_strerror(avahi_client_errno(c)));
344 ✗ WarningHolder::AddWarning(MDNS_WARNING_ID, MDNS_WARN_FPPD);
345 } else {
346 13 mgr->SetServiceBrowser(sb);
347
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
52 WarningHolder::RemoveWarning(MDNS_WARNING_ID, MDNS_WARN_FPPD);
348 }
349
350 // Also browse for _wled._tcp so WLED nodes show up in the systems
351 // list. FPP nodes also advertise _wled._tcp; per-IP dedup in
352 // HandleResolveIP keeps them from being processed twice.
353 13 void* wsb = avahi_service_browser_new(c, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_wled._tcp", NULL, (AvahiLookupFlags)0, browse_callback, mgr);
354
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (!wsb) {
355 ✗ LogErr(VB_SYNC, "Failed to create Avahi service browser for _wled._tcp: %s\n",
356 avahi_strerror(avahi_client_errno(c)));
357 ✗ WarningHolder::AddWarning(MDNS_WARNING_ID, MDNS_WARN_WLED);
358 } else {
359 13 mgr->SetWLEDServiceBrowser(wsb);
360
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
52 WarningHolder::RemoveWarning(MDNS_WARNING_ID, MDNS_WARN_WLED);
361 }
362
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
3 } else if (state == AVAHI_CLIENT_S_REGISTERING || state == AVAHI_CLIENT_S_COLLISION) {
363 // The daemon is (re)registering its host records, e.g. after its host name
364 // changed. Our services point at the old host name, so withdraw them;
365 // S_RUNNING registers them again.
366 1 mgr->WithdrawService();
367
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 } else if (state == AVAHI_CLIENT_FAILURE) {
368 2 LogWarn(VB_SYNC, "Avahi client failure: %s - will rebuild the client\n",
369 avahi_strerror(avahi_client_errno(c)));
370 // This client is finished; everything it owns (browsers, entry group) died
371 // with it. Freeing it here would be freeing the object whose callback we are
372 // in, so hand the rebuild to the reconnect timer, which runs on the main loop
373 // once Avahi is off the stack.
374 2 mgr->NoteClientDown();
375 }
376 }
377
378 // Build the connection to avahi-daemon. Returns false (and schedules a retry) when
379 // the daemon isn't there to talk to.
380 3279 bool MDNSManager::StartAvahiClient() {
381
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3279 times.
3279 if (m_client)
382 ✗ return true;
383
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3279 times.
3279 if (!m_pollAdapter)
384 ✗ return false;
385
386 3279 int error = 0;
387
1/2
✓ Branch 0 taken 3279 times.
✗ Branch 1 not taken.
3279 AvahiClient* client = avahi_client_new(static_cast<AvahiPoll*>(m_pollAdapter),
388 (AvahiClientFlags)0, client_callback, this, &error);
389
2/2
✓ Branch 0 taken 3267 times.
✓ Branch 1 taken 12 times.
3279 if (!client) {
390
2/4
✓ Branch 0 taken 3267 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3267 times.
✗ Branch 3 not taken.
3267 LogWarn(VB_SYNC, "Failed to create Avahi client: %s\n", avahi_strerror(error));
391
1/2
✓ Branch 0 taken 3267 times.
✗ Branch 1 not taken.
3267 NoteClientDown();
392 3267 return false;
393 }
394 12 m_client = client;
395 12 return true;
396 }
397
398 13 void MDNSManager::NoteClientRunning() {
399
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 if (m_clientDownSinceMS) {
400 1 LogInfo(VB_SYNC, "Avahi client connected after %lld ms down\n",
401 MDNSMonotonicMS() - m_clientDownSinceMS);
402 }
403 13 m_clientDownSinceMS = 0;
404 13 m_reconnectDelayMS = MDNS_RECONNECT_MIN_MS;
405
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 if (m_reconnectTimerFd >= 0) {
406 1 struct itimerspec its = {}; // all-zero disarms
407 1 timerfd_settime(m_reconnectTimerFd, 0, &its, NULL);
408 }
409
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
39 WarningHolder::RemoveWarning(MDNS_WARNING_ID, MDNS_WARN_CLIENT);
410 13 }
411
412 3269 void MDNSManager::NoteClientDown() {
413 3269 long long now = MDNSMonotonicMS();
414
2/2
✓ Branch 0 taken 1876 times.
✓ Branch 1 taken 1393 times.
3269 if (m_clientDownSinceMS == 0) {
415 1876 m_clientDownSinceMS = now;
416 1876 m_reconnectDelayMS = MDNS_RECONNECT_MIN_MS;
417 } else {
418 1393 m_reconnectDelayMS *= 2;
419
2/2
✓ Branch 0 taken 142 times.
✓ Branch 1 taken 1251 times.
1393 if (m_reconnectDelayMS > MDNS_RECONNECT_MAX_MS)
420 142 m_reconnectDelayMS = MDNS_RECONNECT_MAX_MS;
421
422 // Only once the daemon has stayed gone, and never while fppd is on its way
423 // down - a shutdown takes avahi-daemon with it, and that is not a fault.
424
5/6
✓ Branch 0 taken 142 times.
✓ Branch 1 taken 1251 times.
✓ Branch 2 taken 142 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 142 times.
✓ Branch 5 taken 1251 times.
1393 if ((now - m_clientDownSinceMS) >= MDNS_WARN_AFTER_MS && fppdIsRunning()) {
425
2/4
✓ Branch 0 taken 142 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 142 times.
✗ Branch 3 not taken.
371 WarningHolder::AddWarning(MDNS_WARNING_ID, MDNS_WARN_CLIENT);
426 }
427 }
428 3269 ScheduleClientReconnect();
429 3269 }
430
431 3269 void MDNSManager::ScheduleClientReconnect() {
432
2/2
✓ Branch 0 taken 1875 times.
✓ Branch 1 taken 1394 times.
3269 if (m_reconnectTimerFd < 0) {
433 1875 m_reconnectTimerFd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
434
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1875 times.
1875 if (m_reconnectTimerFd < 0) {
435 ✗ LogWarn(VB_SYNC, "Failed to create timerfd for Avahi reconnect: %s\n",
436 FPPstrerror(errno));
437 ✗ return;
438 }
439 5144 m_reconnectCallback = [this](int) -> bool {
440 1394 uint64_t expirations = 0;
441
1/2
✓ Branch 0 taken 1394 times.
✗ Branch 1 not taken.
1394 ssize_t n = read(m_reconnectTimerFd, &expirations, sizeof(expirations));
442 (void)n;
443
1/2
✓ Branch 0 taken 1394 times.
✗ Branch 1 not taken.
1394 ReconnectAvahiClient();
444 1394 return false;
445 1875 };
446
1/2
✓ Branch 0 taken 1875 times.
✗ Branch 1 not taken.
1875 EPollManager::INSTANCE.addFileDescriptor(m_reconnectTimerFd, m_reconnectCallback);
447 }
448
449 3269 struct itimerspec its = {};
450 3269 its.it_value.tv_sec = m_reconnectDelayMS / 1000;
451 3269 its.it_value.tv_nsec = (m_reconnectDelayMS % 1000) * 1000000L;
452 3269 timerfd_settime(m_reconnectTimerFd, 0, &its, NULL);
453 }
454
455 // Runs on the main loop, not from inside an Avahi callback, so the dead client can
456 // safely be freed here.
457 1394 void MDNSManager::ReconnectAvahiClient() {
458 1394 CancelServiceNameRetry();
459
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1393 times.
1394 if (m_client) {
460 // avahi_client_free() frees every object the client owns, so drop our handles
461 // to the browsers and the entry group first rather than free them twice.
462 1 m_serviceBrowser = nullptr;
463 1 m_wledServiceBrowser = nullptr;
464 1 m_entryGroup = nullptr;
465 1 avahi_client_free(static_cast<AvahiClient*>(m_client));
466 1 m_client = nullptr;
467 }
468 1394 StartAvahiClient();
469 1394 }
470
471 13 void MDNSManager::SetServiceBrowser(void* sb) {
472
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 std::unique_lock<std::recursive_mutex> lk(m_callbackLock);
473
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 if (m_serviceBrowser) {
474
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 avahi_service_browser_free(static_cast<AvahiServiceBrowser*>(m_serviceBrowser));
475 1 m_serviceBrowser = nullptr;
476 }
477 13 m_serviceBrowser = sb;
478 13 }
479
480 13 void MDNSManager::SetWLEDServiceBrowser(void* sb) {
481
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 std::unique_lock<std::recursive_mutex> lk(m_callbackLock);
482
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 if (m_wledServiceBrowser) {
483
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 avahi_service_browser_free(static_cast<AvahiServiceBrowser*>(m_wledServiceBrowser));
484 1 m_wledServiceBrowser = nullptr;
485 }
486 13 m_wledServiceBrowser = sb;
487 13 }
488
489 4 void MDNSManager::PickAlternativeServiceName() {
490 4 char* alt = avahi_alternative_service_name(m_serviceName.c_str());
491
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (alt) {
492 4 m_serviceName = alt;
493 4 avahi_free(alt);
494 }
495 4 }
496
497 // How long to wait before each retry of the host name after a collision. An echo
498 // of our own records is gone by the first one; a real second box with this name
499 // is still there after the last, and then the alternative name stays.
500 static constexpr int MDNS_NAME_RETRY_DELAYS_SEC[] = { 30, 120, 600 };
501
502 2 static void name_retry_callback(AvahiTimeout* t, void* userdata) {
503 2 static_cast<MDNSManager*>(userdata)->RetryBaseServiceName();
504 2 }
505
506 1 void MDNSManager::WithdrawService() {
507 1 CancelServiceNameRetry();
508
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (m_entryGroup) {
509 1 avahi_entry_group_reset(static_cast<AvahiEntryGroup*>(m_entryGroup));
510 }
511 1 }
512
513 13 void MDNSManager::ResetServiceName() {
514 13 CancelServiceNameRetry();
515 13 m_nameRetryAttempts = 0;
516 13 m_serviceName = m_baseServiceName;
517 13 }
518
519 4 void MDNSManager::ScheduleServiceNameRetry() {
520
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
4 if (m_serviceName == m_baseServiceName || !m_pollAdapter) {
521 ✗ return;
522 }
523 4 constexpr int maxAttempts = sizeof(MDNS_NAME_RETRY_DELAYS_SEC) / sizeof(MDNS_NAME_RETRY_DELAYS_SEC[0]);
524
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (m_nameRetryAttempts >= maxAttempts) {
525 ✗ LogWarn(VB_SYNC, "MDNS service name '%s' is in use by another host, keeping '%s'\n",
526 m_baseServiceName.c_str(), m_serviceName.c_str());
527 ✗ return;
528 }
529 struct timeval tv;
530
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 avahi_elapse_time(&tv, MDNS_NAME_RETRY_DELAYS_SEC[m_nameRetryAttempts] * 1000, 0);
531 4 m_nameRetryAttempts++;
532
533 4 const AvahiPoll* poll = static_cast<const AvahiPoll*>(m_pollAdapter);
534
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (m_nameRetryTimeout) {
535
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 poll->timeout_update(static_cast<AvahiTimeout*>(m_nameRetryTimeout), &tv);
536 } else {
537
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 m_nameRetryTimeout = poll->timeout_new(poll, &tv, name_retry_callback, this);
538 }
539 }
540
541 2 void MDNSManager::RetryBaseServiceName() {
542 2 AvahiClient* c = static_cast<AvahiClient*>(m_client);
543
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
2 if (!c || avahi_client_get_state(c) != AVAHI_CLIENT_S_RUNNING || m_serviceName == m_baseServiceName) {
544 ✗ return;
545 }
546 2 LogInfo(VB_SYNC, "MDNS retrying service name '%s' (attempt %d)\n", m_baseServiceName.c_str(), m_nameRetryAttempts);
547 2 m_serviceName = m_baseServiceName;
548 2 RegisterService(c);
549 }
550
551 3733 void MDNSManager::CancelServiceNameRetry() {
552
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3730 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3733 if (m_nameRetryTimeout && m_pollAdapter) {
553 3 const AvahiPoll* poll = static_cast<const AvahiPoll*>(m_pollAdapter);
554 3 poll->timeout_free(static_cast<AvahiTimeout*>(m_nameRetryTimeout));
555 }
556 3733 m_nameRetryTimeout = nullptr;
557 3733 }
558
559 19 void MDNSManager::RegisterService(void* client) {
560 19 AvahiClient* c = static_cast<AvahiClient*>(client);
561
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (!c)
562 ✗ return;
563
564 // Free any existing entry group before creating a new one
565
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 12 times.
19 if (m_entryGroup) {
566 7 avahi_entry_group_free(static_cast<AvahiEntryGroup*>(m_entryGroup));
567 7 m_entryGroup = nullptr;
568 }
569
570 int error;
571 19 AvahiEntryGroup* group = avahi_entry_group_new(c, entry_group_callback, this);
572
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (!group) {
573 ✗ LogWarn(VB_SYNC, "Failed to create Avahi entry group\n");
574 ✗ return;
575 }
576
577 // Register _fppd._udp service on port 32320 (MultiSync port).
578 19 error = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, (AvahiPublishFlags)0,
579 m_serviceName.c_str(), "_fppd._udp", NULL, NULL, 32320, NULL);
580
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (error < 0) {
581 ✗ LogWarn(VB_SYNC, "Failed to add service: %s\n", avahi_strerror(error));
582 ✗ avahi_entry_group_free(group);
583 ✗ return;
584 }
585
586 // Also register _wled._tcp on the HTTP port so WLED-compatible apps
587 // (notably WLED-iOS / WLED-native) discover the FPP node and treat
588 // it as a WLED device. The mac= TXT record is what those apps key
589 // off for device identity.
590 {
591 // Both backends advertise the same record, and finding the MAC is a
592 // per-platform job rather than a per-library one, so it lives in
593 // MDNSManager.cpp. Already lowercase hex with no separators, which is
594 // the form WLED expects.
595
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::string macFlat = MDNSPrimaryMacAddress();
596
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::string macTxt = "mac=" + macFlat;
597
598 // Restrict _wled._tcp to IPv4 (AVAHI_PROTO_INET) even though
599 // Apache is dual-stack. The WLED iOS UI renders the device's
600 // address inline and a full IPv6 string wraps awkwardly; the
601 // shorter v4 dotted-quad fits the layout cleanly.
602
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 AvahiStringList* txt = avahi_string_list_new(macTxt.c_str(), NULL);
603
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 int werror = avahi_entry_group_add_service_strlst(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET,
604 (AvahiPublishFlags)0, m_serviceName.c_str(),
605 "_wled._tcp", NULL, NULL, 80, txt);
606
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 avahi_string_list_free(txt);
607
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (werror < 0) {
608 ✗ LogWarn(VB_SYNC, "Failed to add _wled._tcp service: %s\n", avahi_strerror(werror));
609 } else {
610
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 LogDebug(VB_SYNC, "MDNS _wled._tcp on port 80 (mac=%s)\n", macFlat.c_str());
611 }
612 19 }
613
614 // Commit the entry group to make the service visible
615 19 error = avahi_entry_group_commit(group);
616
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (error < 0) {
617 ✗ LogWarn(VB_SYNC, "Failed to commit entry group: %s\n", avahi_strerror(error));
618 ✗ avahi_entry_group_free(group);
619 ✗ return;
620 }
621
622 19 m_entryGroup = group;
623 19 LogInfo(VB_SYNC, "MDNS service registration initiated for %s._fppd._udp on port 32320\n", m_serviceName.c_str());
624 }
625
626 // ── Backend entry points ────────────────────────────────────────────────
627
628 1885 void MDNSManager::StartBackend() {
629 // Build a custom AvahiPoll that delegates to EPollManager
630
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
1885 auto* poll = new AvahiPoll{};
631 1885 poll->watch_new = fpp_watch_new;
632 1885 poll->watch_update = fpp_watch_update;
633 1885 poll->watch_get_events = fpp_watch_get_events;
634 1885 poll->watch_free = fpp_watch_free;
635 1885 poll->timeout_new = fpp_timeout_new;
636 1885 poll->timeout_update = fpp_timeout_update;
637 1885 poll->timeout_free = fpp_timeout_free;
638 1885 poll->userdata = this;
639 1885 m_pollAdapter = poll;
640 1885 m_running = true;
641
642 // Failing here is not fatal and not permanent: fppd starts before avahi-daemon
643 // on a cold boot often enough that giving up would leave mDNS off for the whole
644 // session. StartAvahiClient() arms the retry.
645 1885 StartAvahiClient();
646 1885 }
647
648 2325 void MDNSManager::StopBackend() {
649 // Stop the reconnect retries first: it must not fire (and rebuild the client we
650 // are about to free) part way through this.
651
2/2
✓ Branch 0 taken 1857 times.
✓ Branch 1 taken 468 times.
2325 if (m_reconnectTimerFd >= 0) {
652 1857 EPollManager::INSTANCE.removeFileDescriptor(m_reconnectTimerFd);
653 1857 close(m_reconnectTimerFd);
654 1857 m_reconnectTimerFd = -1;
655 }
656 2325 m_clientDownSinceMS = 0;
657 // Lives on the poll adapter, not the client, so freeing the client won't take it.
658 2325 CancelServiceNameRetry();
659
660 // Free Avahi objects in dependency order.
661 // Freeing the client triggers watch_free / timeout_free for its internal
662 // watches and timeouts, which removes them from EPollManager automatically.
663
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 2314 times.
2325 if (m_entryGroup) {
664 // Reset the group first, which forces the daemon to send mDNS
665 // withdraw packets *before* the group is destroyed. Without this,
666 // peers can keep our advertised services cached until the record
667 // TTL expires (often >1 hour) — and on a force-killed fppd they'd
668 // miss the goodbye entirely.
669 11 avahi_entry_group_reset(static_cast<AvahiEntryGroup*>(m_entryGroup));
670 11 avahi_entry_group_free(static_cast<AvahiEntryGroup*>(m_entryGroup));
671 11 m_entryGroup = nullptr;
672 }
673
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 2314 times.
2325 if (m_serviceBrowser) {
674 11 avahi_service_browser_free(static_cast<AvahiServiceBrowser*>(m_serviceBrowser));
675 11 m_serviceBrowser = nullptr;
676 }
677
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 2314 times.
2325 if (m_wledServiceBrowser) {
678 11 avahi_service_browser_free(static_cast<AvahiServiceBrowser*>(m_wledServiceBrowser));
679 11 m_wledServiceBrowser = nullptr;
680 }
681
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 2314 times.
2325 if (m_client) {
682 11 avahi_client_free(static_cast<AvahiClient*>(m_client));
683 11 m_client = nullptr;
684 }
685
2/2
✓ Branch 0 taken 1867 times.
✓ Branch 1 taken 458 times.
2325 if (m_pollAdapter) {
686
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 delete static_cast<AvahiPoll*>(m_pollAdapter);
687 1867 m_pollAdapter = nullptr;
688 }
689 2325 m_running = false;
690 2325 }
691
692 #endif // HAVE_AVAHI
693