GCC Code Coverage Report


Directory: ./
File: MultiSync.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 2415 2819 85.7%
Functions: 150 154 97.4%
Branches: 2543 4842 52.5%

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 LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include "EPollManager.h"
18 #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds
19
20 #include <arpa/inet.h>
21 #include <curl/curl.h>
22 #include <net/if.h>
23 #include <netinet/in.h>
24 #include <sys/types.h>
25 #include <algorithm>
26 #include <chrono>
27 #include <cmath>
28 #include <condition_variable>
29 #include <cstddef>
30 #include <cstdint>
31 #include <cstdlib>
32 #include <ctime>
33 #include <ctype.h>
34 #include <deque>
35 #include <errno.h>
36 #include <functional>
37 #include <ifaddrs.h>
38 #include <list>
39 #include <map>
40 #include <math.h>
41 #include <memory>
42 #include <string_view>
43 #include <mutex>
44 #include <netdb.h>
45 #include <set>
46 #include <stdio.h>
47 #include <string.h>
48 #include <string>
49 #include <strings.h>
50 #include <thread>
51 #include <unistd.h>
52 #include <utility>
53 #include <vector>
54
55 #include "CurlManager.h"
56 #include "FileMonitor.h"
57 #include "NetworkController.h"
58 #include "NetworkMonitor.h"
59 #include "Player.h"
60 #include "Plugins.h"
61 #include "Sequence.h"
62 #include "command.h"
63 #include "common.h"
64 #include "fppversion.h"
65 #include "log.h"
66 #include "settings.h"
67 #include "channeloutput/ChannelOutputSetup.h"
68 #include "channeloutput/channeloutputthread.h"
69 #include "e131bridge.h"
70 #include "commands/Commands.h"
71 #include "mediaoutput/MediaOutputBase.h"
72 #include "mediaoutput/mediaoutput.h"
73
74 #include "MultiSync.h"
75 #include "Timers.h"
76
77 // Quiescence for the MultiSync settings listeners. The settings code copies
78 // the listeners out and calls them with no lock held, so unregistering one
79 // does not wait for a call already in progress (or about to start from a
80 // copy). Each call counts itself in here and gives up once ShutdownSync() has
81 // set the flag; ShutdownSync() waits for the count to drain before it closes
82 // the sockets ReloadSyncDestinations() works on. Defined ahead of INSTANCE so
83 // they outlive it during static destruction.
84 static std::mutex s_listenerQuiesceLock;
85 static std::condition_variable s_listenerQuiesceCV;
86 static int s_listenersInFlight = 0;
87 static bool s_listenersShutdown = false;
88
89 // Serializes OpenControlSockets(): it is reached lazily from several threads
90 // (the main loop, command and API threads), which could each see no socket
91 // and open one, leaking all but the last.
92 static std::mutex s_controlSockOpenLock;
93
94 // Addresses with an HTTP re-ping queued or running. PeriodicPing() re-queues
95 // every stale non-MultiSync remote on each pass, and those probes mostly sit
96 // out a connect timeout against boxes that are down, so without this the
97 // queue grows faster than it drains. Guarded by m_httpProbeLock.
98 static std::set<std::string> s_httpPingPending;
99
100 // The most HTTP re-pings that may wait in the queue. Every path that queues
101 // one is fed from the network, and the Twinkly discovery replies by anyone
102 // on the LAN, so the queue needs a bound of its own; real remotes and lights
103 // come to far fewer.
104 #define HTTP_PING_MAX_QUEUED 512
105
106 // caller holds m_httpProbeLock
107 158 static void QueueHTTPPing(std::deque<std::string>& queue, const std::string& address) {
108
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 158 times.
158 if (queue.size() >= HTTP_PING_MAX_QUEUED) {
109 ✗ LogDebug(VB_SYNC, "Not probing %s over HTTP: %d probes already queued\n", address.c_str(), (int)queue.size());
110 ✗ return;
111 }
112
1/4
✓ Branch 0 taken 158 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
158 if (s_httpPingPending.insert(address).second) {
113 158 queue.push_back(address);
114 }
115 }
116
117 MultiSync MultiSync::INSTANCE;
118 MultiSync* multiSync = &MultiSync::INSTANCE;
119
120 static const char* MULTISYNC_MULTICAST_ADDRESS = "239.70.80.80"; // 239.F.P.P
121
122 // Ping packet extraData lengths (see docs/ControlProtocol.txt). Each version
123 // only appends to the one before, and receivers parse by length, never by the
124 // version byte: every FPP release and xLights treat an unknown version as v3.
125 static constexpr int PING_V3_LEN = 294;
126 static constexpr int PING_UUID_MAX = 64; // plus its NUL
127 static constexpr int PING_V4_LEN = PING_V3_LEN + PING_UUID_MAX + 1;
128 static uint32_t MULTISYNC_MULTICAST_ADD = inet_addr(MULTISYNC_MULTICAST_ADDRESS);
129
130 2032 NetInterfaceInfo::NetInterfaceInfo() :
131 2032 address(0),
132 2032 broadcastAddress(0),
133 2032 multicastSocket(-1) {
134 2032 }
135 223 NetInterfaceInfo::~NetInterfaceInfo() {
136
2/2
✓ Branch 0 taken 200 times.
✓ Branch 1 taken 23 times.
223 if (multicastSocket != -1) {
137 200 LogDebug(VB_SYNC, "Closing multicast socket for %s\n", interfaceName.c_str());
138 200 close(multicastSocket);
139 }
140 223 }
141
142 10995 static bool GetIPForHost(std::string& target) {
143 // ResolveHostToIPv4() is reentrant (gethostbyname()/inet_ntoa() return
144 // pointers into static, per-process buffers, and MultiSync resolves from
145 // several threads concurrently -- DiscoverIPViaHTTP runs from a curl
146 // completion while the main-loop ProcessControlPacket path resolves too).
147 // It also caches, which matters here: this is called once per configured
148 // output address from three separate points during startup, and a name that
149 // does not resolve costs seconds every time. We still rewrite target as a
150 // dotted-quad, because callers depend on that form (split(target, '.') and
151 // inet_addr(target)).
152
1/2
✓ Branch 0 taken 10995 times.
✗ Branch 1 not taken.
10995 std::string ip = ResolveHostToIPv4(target);
153
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 10974 times.
10995 if (ip.empty()) {
154 21 return false;
155 }
156
1/2
✓ Branch 0 taken 10974 times.
✗ Branch 1 not taken.
10974 target = ip;
157 10974 return true;
158 10995 }
159
160 // 169.254/16: what an interface holds when DHCP hasn't answered (yet).
161 2736865 static bool IsIPv4LinkLocal(const std::string& address) {
162
2/4
✓ Branch 0 taken 2736865 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2736865 times.
✗ Branch 3 not taken.
5473730 return startsWith(address, "169.254.");
163 }
164
165 // Adds the hosts in a comma-separated remote list setting (MultiSyncRemotes,
166 // MultiSyncExtraRemotes) to `remotes`. Quotes are stripped from each end of an
167 // entry on its own, not only as a pair: the web UI used to PUT
168 // MultiSyncRemotes as a JSON string, which stores the whole list quoted
169 // ("a,b,c", so the first entry has only an opening quote and the last only a
170 // closing one) and an empty list as a bare pair of quotes, while an entry
171 // typed with its own quotes has both. Left in, a quote fails inet_addr(), and
172 // a lone "" becomes a host name that costs a full DNS timeout (~4s, on the
173 // settings-reload thread) on every reload.
174 1963 static void AddRemotesFromSetting(const char* setting, std::set<std::string>& remotes) {
175
4/6
✓ Branch 0 taken 1963 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1963 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 55 times.
✓ Branch 5 taken 1963 times.
2018 for (auto& token : split(getSetting(setting), ',')) {
176
1/2
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
55 TrimWhiteSpace(token);
177
8/8
✓ Branch 0 taken 75 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 57 times.
✓ Branch 3 taken 18 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 53 times.
✓ Branch 6 taken 22 times.
✓ Branch 7 taken 55 times.
77 while (!token.empty() && (token.front() == '"' || token.back() == '"')) {
178
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 4 times.
22 if (token.front() == '"') {
179
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 token.erase(0, 1);
180 }
181
5/6
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 18 times.
✓ Branch 5 taken 4 times.
22 if (!token.empty() && token.back() == '"') {
182 18 token.pop_back();
183 }
184
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 TrimWhiteSpace(token);
185 }
186
3/4
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✓ Branch 3 taken 2 times.
55 if (token != "") {
187
1/2
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
53 remotes.insert(token);
188 }
189 1963 }
190 1963 }
191
192 // Whether `sys` is the device reporting (hostname, fppMode, type, uuid) from
193 // some other address. A ping packet carries no UUID, so a ping alone can only
194 // be matched on what it does carry; when both sides do know a UUID of the same
195 // kind (real or MAC-derived) that decides it instead, which keeps two
196 // controllers left on the same default hostname apart.
197 2 static bool IsSameDevice(const MultiSyncSystem& sys, const std::string& hostname,
198 FPPMode fppMode, MultiSyncSystemType type, const std::string& uuid) {
199 // update() stores MASTER_MODE as PLAYER_MODE
200
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (fppMode == MASTER_MODE) {
201 ✗ fppMode = PLAYER_MODE;
202 }
203
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 (hostname.empty() || sys.hostname != hostname || sys.fppMode != fppMode) {
204 ✗ return false;
205 }
206
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 auto known = [](const std::string& u) { return !u.empty() && u != "Unknown"; };
207
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
4 if (known(uuid) && known(sys.uuid) &&
208
10/28
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 2 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 2 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 2 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
10 startsWith(uuid, MAC_UUID_PREFIX) == startsWith(sys.uuid, MAC_UUID_PREFIX)) {
209 2 return uuid == sys.uuid;
210 }
211 ✗ return sys.type == type;
212 }
213
214 9089 void MultiSyncSystem::update(MultiSyncSystemType type,
215 unsigned int majorVersion, unsigned int minorVersion,
216 FPPMode fppMode,
217 const std::string& address,
218 const std::string& hostname,
219 const std::string& version,
220 const std::string& model,
221 const std::string& ranges,
222 const std::string& uuid,
223 const bool multiSync,
224 const bool sendingMultiSync) {
225 // UUID precedence. A device that reports its own UUID always wins; the
226 // MAC-derived identity below is only ever a stand-in for one, so it must
227 // never overwrite a real UUID, and a real UUID arriving later must replace
228 // it.
229
7/10
✓ Branch 0 taken 9089 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 108 times.
✓ Branch 3 taken 8981 times.
✓ Branch 4 taken 108 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 108 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 108 times.
✓ Branch 9 taken 8981 times.
9089 if (uuid != "Unknown" && uuid != "") {
230
7/16
✓ Branch 0 taken 108 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 108 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✓ Branch 5 taken 94 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 14 times.
✓ Branch 8 taken 108 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 108 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
432 if (!startsWith(uuid, MAC_UUID_PREFIX) || this->uuid.empty() ||
231
3/16
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 108 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 108 times.
✓ Branch 10 taken 108 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
108 startsWith(this->uuid, MAC_UUID_PREFIX)) {
232
1/2
✓ Branch 0 taken 108 times.
✗ Branch 1 not taken.
108 this->uuid = uuid;
233 }
234 }
235
236 // Nothing reported a UUID for this device. That is the normal case for
237 // every controller that isn't full FPP: the ping packet carries no UUID
238 // field at all, so ProcessPingPacket() has none to pass on, and
239 // NetworkController only fills one in for an FPP instance. Without an
240 // identity the UI has to key the row on the hostname, which collides as
241 // soon as two controllers ship with the same default name and changes
242 // under it whenever someone renames one.
243 //
244 // The MAC is the stable identity such a device does have, and the kernel
245 // already knows it for anything on a directly attached subnet. Only look
246 // it up while we still have nothing -- once an identity of either kind is
247 // recorded this stops running, so it costs one small read per device
248 // rather than one per ping.
249
2/2
✓ Branch 0 taken 4534 times.
✓ Branch 1 taken 4555 times.
9089 if (this->uuid.empty()) {
250 // The parameter, not this->address: on a system being created that
251 // member is still empty here, and is only assigned further down.
252
1/2
✓ Branch 0 taken 4534 times.
✗ Branch 1 not taken.
4534 std::string mac = GetMacForAddress(address);
253
2/2
✓ Branch 0 taken 192 times.
✓ Branch 1 taken 4342 times.
4534 if (!mac.empty()) {
254
1/2
✓ Branch 0 taken 192 times.
✗ Branch 1 not taken.
192 this->uuid = MAC_UUID_PREFIX + mac;
255 }
256 4534 }
257
258 // If this record is from info learned via the MultiSync protocol,
259 // don't allow it to be overwritten by data discovered elsewhere
260 // except for uuid.
261
3/4
✓ Branch 0 taken 3935 times.
✓ Branch 1 taken 5154 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3935 times.
9089 if (this->multiSync && !multiSync)
262 ✗ return;
263
264 9089 this->fppMode = fppMode;
265 9089 this->sendingMultiSync = sendingMultiSync;
266
2/2
✓ Branch 0 taken 130 times.
✓ Branch 1 taken 8959 times.
9089 if (fppMode == MASTER_MODE) {
267 130 this->fppMode = PLAYER_MODE;
268 130 this->sendingMultiSync = true;
269 }
270
271 9089 this->type = type;
272 9089 this->majorVersion = majorVersion;
273 9089 this->minorVersion = minorVersion;
274
1/2
✓ Branch 0 taken 9089 times.
✗ Branch 1 not taken.
9089 this->address = address;
275
1/2
✓ Branch 0 taken 9089 times.
✗ Branch 1 not taken.
9089 this->hostname = hostname;
276
1/2
✓ Branch 0 taken 9089 times.
✗ Branch 1 not taken.
9089 this->version = version;
277
1/2
✓ Branch 0 taken 9089 times.
✗ Branch 1 not taken.
9089 this->model = model;
278
9/10
✓ Branch 0 taken 4745 times.
✓ Branch 1 taken 4344 times.
✓ Branch 2 taken 4745 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 213 times.
✓ Branch 5 taken 4532 times.
✓ Branch 6 taken 205 times.
✓ Branch 7 taken 8 times.
✓ Branch 8 taken 4737 times.
✓ Branch 9 taken 4352 times.
9089 if (!ranges.empty() && (ranges != "0-0" || this->ranges.empty())) {
279
1/2
✓ Branch 0 taken 4737 times.
✗ Branch 1 not taken.
4737 this->ranges = ranges;
280 }
281
282
4/4
✓ Branch 0 taken 5154 times.
✓ Branch 1 taken 3935 times.
✓ Branch 2 taken 4944 times.
✓ Branch 3 taken 210 times.
9089 if (!this->multiSync && multiSync) {
283 4944 this->multiSync = true;
284 }
285
286
1/2
✓ Branch 0 taken 9089 times.
✗ Branch 1 not taken.
9089 std::vector<std::string> parts = split(address, '.');
287 // ipa-ipd hold the legacy IPv4 octets used in the (IPv4-only) ping packet.
288 // An IPv6 peer has no such octets, so skip the IPv4 hostname lookup that
289 // would only fail and log an error; the octets stay zeroed below.
290
4/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9087 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 9089 times.
9089 if (parts.size() != 4 && address.find(':') == std::string::npos) {
291 ✗ if (!GetIPForHost(this->address)) {
292 ✗ LogErr(VB_SYNC, "Error looking up hostname: %s\n", address.c_str());
293 } else {
294 ✗ parts = split(this->address, '.');
295 }
296 }
297
2/2
✓ Branch 0 taken 9087 times.
✓ Branch 1 taken 2 times.
9089 if (parts.size() >= 4) {
298 9087 this->ipa = atoi(parts[0].c_str());
299 9087 this->ipb = atoi(parts[1].c_str());
300 9087 this->ipc = atoi(parts[2].c_str());
301 9087 this->ipd = atoi(parts[3].c_str());
302 } else {
303 2 this->ipa = 0;
304 2 this->ipb = 0;
305 2 this->ipc = 0;
306 2 this->ipd = 0;
307 }
308
309 // A system is a valid target for "Send MultiSync to ALL KNOWN remotes via
310 // Unicast" only if it's a full FPP instance (type below the 0x80 boundary
311 // that separates full-FPP systems from external controllers) running in
312 // Remote mode. This deliberately excludes WLED, ESPixelStick, Falcon and
313 // Experience controllers, xSchedule, and any FPP instance acting as a
314 // player/master. We intentionally do NOT require that the system was
315 // learned via the UDP MultiSync protocol so that FPP remotes discovered
316 // via HTTP (e.g. on a network segment where multicast is blocked - the
317 // very case this mode exists for) are still targeted.
318
2/2
✓ Branch 0 taken 9032 times.
✓ Branch 1 taken 57 times.
18121 this->supportsUnicast = (this->type < kSysTypeFalconController) &&
319
2/2
✓ Branch 0 taken 98 times.
✓ Branch 1 taken 8934 times.
9032 (this->fppMode == REMOTE_MODE);
320 9089 }
321
322 141 static void SetIfNotEmpty(Json::Value& v, const char* key, const std::string& s) {
323
2/2
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 86 times.
141 if (!s.empty()) {
324
2/4
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✗ Branch 3 not taken.
55 v[key] = s;
325 }
326 141 }
327
328 2 Json::Value MultiSyncSystemInfo::toJSON() const {
329 2 Json::Value v;
330
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "Platform", platform);
331
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "Variant", variant);
332
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "SubPlatform", subPlatform);
333
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "OSVersion", osVersion);
334
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "OSRelease", osRelease);
335
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "Kernel", kernel);
336
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "HostDescription", hostDescription);
337
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "backgroundColor", backgroundColor);
338
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "Branch", branch);
339
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "LocalGitVersion", localGitVersion);
340
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "RemoteGitVersion", remoteGitVersion);
341
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetIfNotEmpty(v, "UpgradeSource", upgradeSource);
342
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (channelInputsEnabled >= 0) {
343 ✗ v["channelInputsEnabled"] = channelInputsEnabled ? true : false;
344 }
345
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (channelOutputsEnabled >= 0) {
346
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 v["channelOutputsEnabled"] = channelOutputsEnabled ? true : false;
347 }
348
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!ips.empty()) {
349
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value a(Json::arrayValue);
350
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& ip : ips) {
351
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 a.append(ip);
352 }
353
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 v["IPs"] = a;
354 2 }
355 2 return v;
356 ✗ }
357
358 35994 Json::Value MultiSyncCapeInfo::toJSON() const {
359
1/2
✓ Branch 0 taken 35994 times.
✗ Branch 1 not taken.
35994 Json::Value v;
360
2/4
✓ Branch 0 taken 35994 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35994 times.
✗ Branch 3 not taken.
35994 v["present"] = present;
361
2/2
✓ Branch 0 taken 35981 times.
✓ Branch 1 taken 13 times.
35994 if (!present) {
362 35981 return v;
363 }
364
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(v, "id", id);
365
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(v, "name", name);
366
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(v, "description", description);
367
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(v, "version", version);
368
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(v, "designer", designer);
369 // Only emitted when the cape opts out, so the common case adds no bytes.
370
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (!sendStats) {
371 ✗ v["sendStats"] = 0;
372 }
373
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 Json::Value vendor;
374
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(vendor, "name", vendorName);
375
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(vendor, "url", vendorURL);
376
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(vendor, "email", vendorEmail);
377
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 SetIfNotEmpty(vendor, "image", vendorImage);
378
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 1 times.
13 if (!vendor.empty()) {
379
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 v["vendor"] = vendor;
380 }
381 13 return v;
382 13 }
383
384 // A field of what a remote sent us (system info, cape info), read so that a
385 // value of the wrong type gives the default instead of throwing: the reply is
386 // parsed in a curl callback on the main loop, and {"Platform":{}} ended fppd.
387 62 static std::string ReplyString(const Json::Value& v, const char* key, const std::string& dflt = "") {
388
2/4
✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 62 times.
✗ Branch 3 not taken.
62 const Json::Value f = v.get(key, Json::Value());
389
9/16
✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 39 times.
✓ Branch 3 taken 23 times.
✓ Branch 4 taken 39 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 39 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 39 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 39 times.
✓ Branch 12 taken 23 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 39 times.
✗ Branch 15 not taken.
124 return (f.isString() || f.isNumeric() || f.isBool()) ? f.asString() : dflt;
390 62 }
391 1 static bool ReplyBool(const Json::Value& v, const char* key, bool dflt) {
392
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 const Json::Value f = v.get(key, Json::Value());
393
4/10
✓ 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.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
3 return (f.isBool() || f.isNumeric()) ? f.asBool() : dflt;
394 2 }
395 6 static int ReplyInt(const Json::Value& v, const char* key, int dflt) {
396
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 const Json::Value f = v.get(key, Json::Value());
397
5/10
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
12 return f.isConvertibleTo(Json::intValue) && !f.isNull() ? f.asInt() : dflt;
398 6 }
399
400 6 MultiSyncCapeInfo MultiSyncCapeInfo::fromCapeJSON(const Json::Value& v) {
401 6 MultiSyncCapeInfo cape;
402 6 cape.valid = true;
403 6 cape.present = true;
404
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
12 cape.id = ReplyString(v, "id");
405
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
12 cape.name = ReplyString(v, "name");
406
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
12 cape.description = ReplyString(v, "description");
407
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
12 cape.version = ReplyString(v, "version");
408
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 cape.designer = ReplyString(v, "designer");
409 // Absent (including on every unsigned cape) means no opt-out.
410
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 cape.sendStats = ReplyInt(v, "sendStats", 1) != 0;
411
11/22
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 5 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 6 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 6 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 5 times.
✓ Branch 17 taken 1 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
18 if (JsonHas(v, "vendor") && v["vendor"].isObject()) {
412
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 const Json::Value& vendor = v["vendor"];
413
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 cape.vendorName = ReplyString(vendor, "name");
414
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 cape.vendorURL = ReplyString(vendor, "url");
415
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 cape.vendorEmail = ReplyString(vendor, "email");
416
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 cape.vendorImage = ReplyString(vendor, "image");
417 }
418 6 return cape;
419 ✗ }
420
421 75674 Json::Value MultiSyncSystem::toJSON(bool local, bool timestamps) {
422 75674 Json::Value system;
423
424
3/6
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 75674 times.
✗ Branch 5 not taken.
75674 system["type"] = MultiSync::GetTypeString(type, local);
425
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["typeId"] = (int)type;
426
2/2
✓ Branch 0 taken 71836 times.
✓ Branch 1 taken 3838 times.
75674 if (timestamps) {
427
2/4
✓ Branch 0 taken 71836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71836 times.
✗ Branch 3 not taken.
71836 system["lastSeen"] = (Json::UInt64)lastSeen;
428
2/4
✓ Branch 0 taken 71836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71836 times.
✗ Branch 3 not taken.
71836 system["lastSeenStr"] = lastSeenStr;
429 }
430
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["majorVersion"] = majorVersion;
431
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["minorVersion"] = minorVersion;
432
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["uuid"] = uuid;
433
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["fppMode"] = fppMode;
434
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["multisync"] = sendingMultiSync;
435
436
1/2
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
75674 char* s = modeToString(fppMode);
437
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["fppModeString"] = s;
438 75674 free(s);
439
440
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["address"] = address;
441
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["hostname"] = hostname;
442
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["version"] = version;
443
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["model"] = model;
444
2/4
✓ Branch 0 taken 75674 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
75674 system["channelRanges"] = ranges;
445
446
4/6
✓ Branch 0 taken 35993 times.
✓ Branch 1 taken 39681 times.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 75674 times.
✗ Branch 5 not taken.
75674 system["local"] = local ? 1 : 0;
447
448
4/6
✓ Branch 0 taken 63127 times.
✓ Branch 1 taken 12547 times.
✓ Branch 2 taken 75674 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 75674 times.
✗ Branch 5 not taken.
75674 system["multiSyncCapable"] = multiSync ? 1 : 0;
449
450
2/2
✓ Branch 0 taken 35993 times.
✓ Branch 1 taken 39681 times.
75674 if (local) {
451
3/6
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35993 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35993 times.
✗ Branch 5 not taken.
35993 system["HostDescription"] = getSetting("HostDescription");
452
3/6
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35993 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35993 times.
✗ Branch 5 not taken.
35993 system["channelInputsEnabled"] = InputsEnabled();
453
3/6
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35993 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35993 times.
✗ Branch 5 not taken.
35993 system["channelOutputsEnabled"] = HasUniverseOutputs();
454 }
455
456 // Detail fetched over HTTP from FPP remotes (see CheckSystemInfoRefreshes).
457 // Absent for the local systems -- a browser rendering the multisync page
458 // already has all of this for the box serving the page -- and for anything
459 // that isn't a full FPP instance.
460
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 75672 times.
75674 if (systemInfo.valid) {
461
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 system["systemInfo"] = systemInfo.toJSON();
462 }
463
2/2
✓ Branch 0 taken 35994 times.
✓ Branch 1 taken 39680 times.
75674 if (capeInfo.valid) {
464
2/4
✓ Branch 0 taken 35994 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35994 times.
✗ Branch 3 not taken.
35994 system["capeInfo"] = capeInfo.toJSON();
465 }
466
467 75674 return system;
468 ✗ }
469
470 /*
471 *
472 */
473 2346 MultiSync::MultiSync() :
474 2346 m_broadcastSock(-1),
475 2346 m_controlSock(-1),
476 2346 m_receiveSock(-1),
477 2346 m_lastMediaHalfSecond(0),
478 2346 m_remoteOffset(0.0),
479 2346 m_lastPingTime(0),
480 2346 m_lastCheckTime(0),
481 2346 m_lastFrame(0),
482 2346 m_sendMulticast(false),
483
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 m_sendBroadcast(false) {
484 2346 memset(rcvBuffers, 0, sizeof(rcvBuffers));
485 2346 memset(rcvCmbuf, 0, sizeof(rcvCmbuf));
486 2346 }
487
488 /*
489 *
490 */
491 458 MultiSync::~MultiSync() {
492 458 ShutdownSync();
493
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 458 times.
458 for (auto& a : m_syncStats) {
494 ✗ MultiSyncStats* stats = (MultiSyncStats*)a.second;
495 ✗ delete stats;
496 }
497 458 m_syncStats.clear();
498 458 }
499
500 /*
501 *
502 */
503 1888 int MultiSync::Init(void) {
504
3/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37 times.
✓ Branch 3 taken 1851 times.
1888 if (getFPPmode() == REMOTE_MODE) {
505 37 m_multiSyncEnabled = false;
506 } else {
507
1/2
✓ Branch 0 taken 1851 times.
✗ Branch 1 not taken.
1851 m_multiSyncEnabled = getSettingInt("MultiSyncEnabled", 0);
508 }
509
510
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 FillInInterfaces();
511
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 FillLocalSystemInfo();
512
513 // Keep the configured-output-range cache (used to backfill ranges for
514 // non-FPP remotes in GetSystems()) in sync with co-universes.json. Watching
515 // the file confines the file read + hostname resolution to actual config
516 // changes instead of every GetSystems() call. TriggerFileChanged() does the
517 // initial load now.
518
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
2048 std::string coUniversesFile = FPP_DIR_CONFIG("/co-universes.json");
519
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
3776 FileMonitor::INSTANCE.AddFile("MultiSync/co-universes.json", coUniversesFile,
520 4003 [this]() { ReloadConfiguredOutputRanges(); })
521
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
3776 .TriggerFileChanged("MultiSync/co-universes.json", coUniversesFile);
522
523
3/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1887 times.
1888 if (!OpenReceiveSocket())
524 1 return 0;
525
526
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1887 times.
1887 if (!OpenBroadcastSocket())
527 ✗ return 0;
528
529
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 1855 times.
1887 if (m_multiSyncEnabled) {
530
2/4
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 32 times.
32 if (!OpenControlSockets())
531 ✗ return 0;
532 }
533
534 5451 std::function<void(NetworkMonitor::NetEventType i, int up, const std::string&)> f = [this](NetworkMonitor::NetEventType i, int up, const std::string& name) {
535 1677 LogDebug(VB_SYNC, "MultiSync::NetworkChanged - Interface: %s Up: %d Msg: %d\n", name.c_str(), up, i);
536
3/4
✓ Branch 0 taken 741 times.
✓ Branch 1 taken 936 times.
✓ Branch 2 taken 741 times.
✗ Branch 3 not taken.
1677 if (i == NetworkMonitor::NetEventType::DEL_ADDR && !up) {
537 741 RemoveInterface(name);
538 // The interface may still hold another address -- the DHCP lease
539 // that replaced a 169.254 fallback, typically -- so pick it back up
540 // rather than leave the interface out, and drop the deleted address
541 // from what we announce.
542
2/2
✓ Branch 0 taken 189 times.
✓ Branch 1 taken 552 times.
741 if (FillInInterfaces()) {
543 189 setupMulticastReceive(true);
544 }
545 741 FillLocalSystemInfo();
546
3/4
✓ Branch 0 taken 673 times.
✓ Branch 1 taken 263 times.
✓ Branch 2 taken 673 times.
✗ Branch 3 not taken.
936 } else if (i == NetworkMonitor::NetEventType::NEW_ADDR && up) {
547 673 bool changed = FillInInterfaces();
548 673 setupMulticastReceive(true);
549 // Always, not just when an interface appeared: a new address on an
550 // interface we already had can retire its 169.254 fallback.
551 673 changed |= FillLocalSystemInfo();
552
2/2
✓ Branch 0 taken 205 times.
✓ Branch 1 taken 468 times.
673 if (changed) {
553 205 Ping(0, false);
554 }
555 }
556 1887 };
557
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 NetworkMonitor::INSTANCE.registerCallback(f);
558
559 // The sync send methods and the unicast remote list are picked up live.
560 // fppd already re-reads /media/settings whenever the web UI writes it
561 // (FileMonitor in fppd.cpp), so all that was missing was rebuilding the
562 // destination list these settings feed -- editing the remote list on the
563 // MultiSync page no longer needs an fppd restart to take effect. The
564 // control socket itself is unaffected by any of them; if it isn't open yet
565 // (MultiSync disabled, or nothing has needed it), OpenControlSockets()
566 // reads the current settings when it does open.
567 2687 for (const char* s : { "MultiSyncRemotes", "MultiSyncExtraRemotes",
568
2/2
✓ Branch 0 taken 9435 times.
✓ Branch 1 taken 1887 times.
13209 "MultiSyncBroadcast", "MultiSyncMulticast", "MultiSyncUnicast" }) {
569
3/6
✓ Branch 0 taken 9435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9435 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9435 times.
✗ Branch 5 not taken.
46375 registerSettingsListener("MultiSync", s, [this](const std::string& v) {
570 {
571
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::unique_lock<std::mutex> lock(s_listenerQuiesceLock);
572
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (s_listenersShutdown) {
573 ✗ return;
574 }
575 7 ++s_listenersInFlight;
576
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 }
577 struct InFlight {
578 7 ~InFlight() {
579 7 std::unique_lock<std::mutex> lock(s_listenerQuiesceLock);
580
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 if (--s_listenersInFlight == 0) {
581 7 s_listenerQuiesceCV.notify_all();
582 }
583 7 }
584 } inFlight;
585
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 if (m_controlSock >= 0) {
586
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 ReloadSyncDestinations();
587 }
588 7 });
589 }
590
591 1887 return 1;
592 1888 }
593
594 /*
595 *
596 */
597 9092 void MultiSync::UpdateSystem(MultiSyncSystemType type,
598 unsigned int majorVersion, unsigned int minorVersion,
599 FPPMode fppMode,
600 const std::string& address,
601 const std::string& hostname,
602 const std::string& version,
603 const std::string& model,
604 const std::string& ranges,
605 const std::string& uuid,
606 const bool multiSync,
607 const bool sendingMultiSync) {
608
3/4
✓ Branch 0 taken 8882 times.
✓ Branch 1 taken 210 times.
✓ Branch 2 taken 9092 times.
✗ Branch 3 not taken.
9092 LogDebug(VB_SYNC, "UpdateSystem(%d, %u, %u, %d, '%s', '%s', '%s', '%s', '%s', '%s', %s)\n",
609 (int)type, majorVersion, minorVersion, (int)fppMode,
610 address.c_str(), hostname.c_str(), version.c_str(), model.c_str(),
611 ranges.c_str(), uuid.c_str(), multiSync ? "true" : "false");
612
613 char timeStr[34];
614 9092 memset(timeStr, 0, sizeof(timeStr));
615 9092 time_t t = time(NULL);
616 struct tm tm;
617 9092 localtime_r(&t, &tm);
618 9092 snprintf(timeStr, sizeof(timeStr), "%4d-%.2d-%.2d %.2d:%.2d:%.2d",
619 9092 1900 + tm.tm_year, tm.tm_mon + 1, tm.tm_mday,
620 tm.tm_hour, tm.tm_min, tm.tm_sec);
621
622
1/2
✓ Branch 0 taken 9092 times.
✗ Branch 1 not taken.
9092 std::string ipForAddress = address;
623
1/2
✓ Branch 0 taken 9092 times.
✗ Branch 1 not taken.
9092 GetIPForHost(ipForAddress);
624
625 // Loopback is this box finding itself, not a discoverable peer. Avahi
626 // resolves our own _fppd._udp advertisement to 127.0.0.1, so mDNS hands it
627 // straight back to us; a channel output or an HTTP discovery subnet aimed
628 // at localhost does the same. The resulting entry is useless to every
629 // consumer of the systems list -- no peer can reach it, and the multisync
630 // page drops the row rather than render a link a browser cannot follow.
631 // Reject it here, at the one place m_remoteSystems ever grows, so no
632 // discovery path can reintroduce it. Local interface addresses come from
633 // FillInInterfaces(), which already skips "lo", so this never rejects one
634 // of our own entries.
635
7/10
✓ Branch 0 taken 9092 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9090 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 9090 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 9090 times.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 9090 times.
9092 if (IsLoopbackAddress(address) || IsLoopbackAddress(ipForAddress)) {
636
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogDebug(VB_SYNC, "Ignoring loopback address %s in discovery\n", address.c_str());
637 2 return;
638 }
639
640
1/2
✓ Branch 0 taken 9090 times.
✗ Branch 1 not taken.
9090 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
641 9090 bool found = false;
642 9090 bool unicastChanged = false;
643
644 // A device that started before DHCP answered announced itself from a
645 // 169.254 address, and keeps that entry here after it has a real one. The
646 // link-local address is only reachable from the same link, so once the
647 // device reports from a routable address that is the address to list: move
648 // the link-local entry over to it (or drop it, if the routable address
649 // already has its own entry) rather than listing the device twice.
650
2/2
✓ Branch 0 taken 9088 times.
✓ Branch 1 taken 2 times.
9090 if (ipForAddress.find(':') == std::string::npos) {
651 9088 MultiSyncSystem* existing = nullptr;
652
2/2
✓ Branch 0 taken 2712868 times.
✓ Branch 1 taken 7583 times.
2720451 for (auto& sys : m_remoteSystems) {
653
5/6
✓ Branch 0 taken 2711363 times.
✓ Branch 1 taken 1505 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2711363 times.
✓ Branch 4 taken 1505 times.
✓ Branch 5 taken 2711363 times.
2712868 if (address == sys.address || ipForAddress == sys.address) {
654 1505 existing = &sys;
655 1505 break;
656 }
657 }
658 // A ping carries no UUID, but the entry already at this address may know one
659
1/2
✓ Branch 0 taken 9088 times.
✗ Branch 1 not taken.
9088 std::string knownUUID = uuid;
660
8/10
✓ Branch 0 taken 9088 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9088 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8981 times.
✓ Branch 5 taken 107 times.
✓ Branch 6 taken 1428 times.
✓ Branch 7 taken 7553 times.
✓ Branch 8 taken 1428 times.
✓ Branch 9 taken 7660 times.
9088 if ((knownUUID.empty() || knownUUID == "Unknown") && existing) {
661
1/2
✓ Branch 0 taken 1428 times.
✗ Branch 1 not taken.
1428 knownUUID = existing->uuid;
662 }
663
664
3/4
✓ Branch 0 taken 9088 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 9086 times.
9088 if (IsIPv4LinkLocal(ipForAddress)) {
665 // The other direction: a sender that still announces its old
666 // 169.254 address alongside the new one (an older FPP) would
667 // otherwise re-add the entry on every ping. Only while the
668 // routable entry is current, so a device that genuinely fell back
669 // to link-local (lost its lease) is still picked up.
670
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto& sys : m_remoteSystems) {
671
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!existing && sys.address.find(':') == std::string::npos &&
672
5/10
✓ 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.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
3 !IsIPv4LinkLocal(sys.address) && sys.lastSeen + 120 >= (unsigned long)t &&
673
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 IsSameDevice(sys, hostname, fppMode, type, knownUUID)) {
674
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_SYNC, "Ignoring link-local %s for %s, also seen at %s\n",
675 address.c_str(), hostname.c_str(), sys.address.c_str());
676 1 return;
677 }
678 }
679 } else {
680 9086 bool haveEntry = existing != nullptr;
681
2/2
✓ Branch 0 taken 2713230 times.
✓ Branch 1 taken 9086 times.
2722316 for (auto it = m_remoteSystems.begin(); it != m_remoteSystems.end();) {
682
7/10
✓ Branch 0 taken 2713230 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2713229 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 2713229 times.
✓ Branch 9 taken 1 times.
2713230 if (!IsIPv4LinkLocal(it->address) || !IsSameDevice(*it, hostname, fppMode, type, knownUUID)) {
683 2713229 ++it;
684 2713229 continue;
685 }
686
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (haveEntry) {
687 ✗ LogInfo(VB_SYNC, "Removing link-local %s for %s, now at %s\n",
688 it->address.c_str(), hostname.c_str(), ipForAddress.c_str());
689 ✗ unicastChanged |= it->supportsUnicast;
690 ✗ it = m_remoteSystems.erase(it);
691 ✗ continue;
692 }
693
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogInfo(VB_SYNC, "Moving %s from link-local %s to %s\n",
694 hostname.c_str(), it->address.c_str(), ipForAddress.c_str());
695 // Set here rather than left to update() below: update() leaves
696 // a multisync-learned entry alone when the report isn't one.
697
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 it->address = ipForAddress;
698
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::vector<std::string> parts = split(ipForAddress, '.');
699
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (parts.size() == 4) {
700 1 it->ipa = atoi(parts[0].c_str());
701 1 it->ipb = atoi(parts[1].c_str());
702 1 it->ipc = atoi(parts[2].c_str());
703 1 it->ipd = atoi(parts[3].c_str());
704 }
705 // Never fetched while link-local (CheckSystemInfoRefreshes skips it)
706 1 it->infoNextFetch = 0;
707 1 unicastChanged |= it->supportsUnicast;
708 1 haveEntry = true;
709 1 ++it;
710 1 }
711 }
712
2/2
✓ Branch 0 taken 9087 times.
✓ Branch 1 taken 1 times.
9088 }
713
714
2/2
✓ Branch 0 taken 2713230 times.
✓ Branch 1 taken 9089 times.
2722319 for (auto& sys : m_remoteSystems) {
715
7/8
✓ Branch 0 taken 2711410 times.
✓ Branch 1 taken 1820 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2711410 times.
✓ Branch 4 taken 316 times.
✓ Branch 5 taken 1504 times.
✓ Branch 6 taken 1504 times.
✓ Branch 7 taken 2711726 times.
2715366 if ((address == sys.address || ipForAddress == sys.address) &&
716
1/2
✓ Branch 0 taken 316 times.
✗ Branch 1 not taken.
2136 ((hostname == sys.hostname) ||
717
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 316 times.
632 (address == sys.hostname) ||
718 316 (hostname == sys.address))) {
719 1504 found = true;
720 1504 bool wasUnicast = sys.supportsUnicast;
721
1/2
✓ Branch 0 taken 1504 times.
✗ Branch 1 not taken.
1504 sys.update(type, majorVersion, minorVersion, fppMode, address, hostname, version, model, ranges, uuid, multiSync, sendingMultiSync);
722
1/2
✓ Branch 0 taken 1504 times.
✗ Branch 1 not taken.
1504 ApplyUUIDHint(sys);
723
1/2
✓ Branch 0 taken 1504 times.
✗ Branch 1 not taken.
1504 sys.lastSeenStr = timeStr;
724 1504 sys.lastSeen = t;
725 1504 unicastChanged |= (wasUnicast != sys.supportsUnicast);
726 }
727 }
728
2/2
✓ Branch 0 taken 10119 times.
✓ Branch 1 taken 9089 times.
19208 for (auto& sys : m_localSystems) {
729
6/6
✓ Branch 0 taken 4235 times.
✓ Branch 1 taken 5884 times.
✓ Branch 2 taken 4233 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 4233 times.
✓ Branch 5 taken 5886 times.
14354 if ((address == sys.address) &&
730 4235 (hostname == sys.hostname)) {
731 4233 found = true;
732
1/2
✓ Branch 0 taken 4233 times.
✗ Branch 1 not taken.
4233 sys.update(type, majorVersion, minorVersion, fppMode, address, hostname, version, model, ranges, uuid, multiSync, sendingMultiSync);
733 4233 sys.lastSeen = t;
734
1/2
✓ Branch 0 taken 4233 times.
✗ Branch 1 not taken.
4233 sys.lastSeenStr = timeStr;
735 }
736 }
737
2/2
✓ Branch 0 taken 3352 times.
✓ Branch 1 taken 5737 times.
9089 if (!found) {
738 // Every ping carrying a new address adds a system, and pings come
739 // from anyone on the LAN; entries are otherwise only pruned after two
740 // hours. Past the cap, the system seen longest ago makes room.
741 static constexpr size_t MAX_REMOTE_SYSTEMS = 2048;
742
2/2
✓ Branch 0 taken 52 times.
✓ Branch 1 taken 3300 times.
3352 if (m_remoteSystems.size() >= MAX_REMOTE_SYSTEMS) {
743
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 auto oldest = std::min_element(m_remoteSystems.begin(), m_remoteSystems.end(),
744 106444 [](const MultiSyncSystem& a, const MultiSyncSystem& b) { return a.lastSeen < b.lastSeen; });
745
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 LogWarn(VB_SYNC, "More than %zu remote systems known; forgetting %s to make room\n",
746 MAX_REMOTE_SYSTEMS, oldest->address.c_str());
747 52 unicastChanged |= oldest->supportsUnicast;
748
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 m_remoteSystems.erase(oldest);
749 }
750 3352 MultiSyncSystem sys;
751
1/2
✓ Branch 0 taken 3352 times.
✗ Branch 1 not taken.
3352 sys.update(type, majorVersion, minorVersion, fppMode, address, hostname, version, model, ranges, uuid, multiSync, sendingMultiSync);
752
1/2
✓ Branch 0 taken 3352 times.
✗ Branch 1 not taken.
3352 ApplyUUIDHint(sys);
753
1/2
✓ Branch 0 taken 3352 times.
✗ Branch 1 not taken.
3352 sys.lastSeenStr = timeStr;
754 3352 sys.lastSeen = t;
755 3352 unicastChanged |= sys.supportsUnicast;
756
1/2
✓ Branch 0 taken 3352 times.
✗ Branch 1 not taken.
3352 m_remoteSystems.push_back(sys);
757 3352 }
758
759
1/2
✓ Branch 0 taken 9089 times.
✗ Branch 1 not taken.
9089 ReconcileDeviceIdentity(hostname, fppMode);
760
761 // If a remote became (or stopped being) a unicast target, refresh the
762 // cached "all known remotes" destination list. The snapshot is taken here
763 // while we still hold m_systemsLock, but the rebuild has to run after it is
764 // released: UpdateUnicastDestinations() takes m_unicastUpdateLock, which is
765 // never acquired while m_systemsLock is held.
766
4/4
✓ Branch 0 taken 127 times.
✓ Branch 1 taken 8962 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 125 times.
9089 bool rebuildUnicast = m_sendUnicast && unicastChanged;
767 9089 std::vector<std::string> unicastAddrs;
768
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9087 times.
9089 if (rebuildUnicast) {
769
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& sys : m_remoteSystems) {
770
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (sys.supportsUnicast) {
771
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 unicastAddrs.push_back(sys.address);
772 }
773 }
774 }
775
1/2
✓ Branch 0 taken 9089 times.
✗ Branch 1 not taken.
9089 lock.unlock();
776
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9087 times.
9089 if (rebuildUnicast) {
777
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 UpdateUnicastDestinations(unicastAddrs);
778 }
779
4/4
✓ Branch 0 taken 9089 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 9089 times.
✓ Branch 3 taken 3 times.
9093 }
780
781 /*
782 *
783 */
784 3310 MultiSyncSystemType MultiSync::ModelStringToType(std::string model) {
785
8/20
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3310 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 3310 times.
✓ Branch 10 taken 3310 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3310 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3310 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
9930 if (startsWith(model, "Raspberry Pi Model A Rev") || (model == "Raspberry Pi Model A"))
786 ✗ return kSysTypeFPPRaspberryPiA;
787
8/20
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3310 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 3310 times.
✓ Branch 10 taken 3310 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3310 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3310 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
9930 if (startsWith(model, "Raspberry Pi Model B Rev") || (model == "Raspberry Pi Model B"))
788 ✗ return kSysTypeFPPRaspberryPiB;
789
8/20
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3310 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 3310 times.
✓ Branch 10 taken 3310 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3310 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3310 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
9930 if (startsWith(model, "Raspberry Pi Model A Plus") || (model == "Raspberry Pi Model A+"))
790 ✗ return kSysTypeFPPRaspberryPiAPlus;
791
8/20
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3310 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 3310 times.
✓ Branch 10 taken 3310 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3310 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3310 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
9930 if (startsWith(model, "Raspberry Pi Model B Plus") || (model == "Raspberry Pi Model B+"))
792 ✗ return kSysTypeFPPRaspberryPiBPlus;
793
5/14
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3310 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 3310 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
16550 if ((startsWith(model, "Raspberry Pi 2 Model B 1.1")) ||
794
6/16
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3310 times.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3310 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3310 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
9930 (startsWith(model, "Raspberry Pi 2 Model B 1.0")))
795 ✗ return kSysTypeFPPRaspberryPi2B;
796
3/6
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3310 times.
6620 if (startsWith(model, "Raspberry Pi 2 Model B"))
797 ✗ return kSysTypeFPPRaspberryPi2BNew;
798
8/20
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3310 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 3310 times.
✓ Branch 10 taken 3310 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3310 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3310 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
9930 if (startsWith(model, "Raspberry Pi 3 Model B Rev") || (model == "Raspberry Pi 3 Model B"))
799 ✗ return kSysTypeFPPRaspberryPi3B;
800
8/20
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3310 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 3310 times.
✓ Branch 10 taken 3310 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3310 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3310 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
9930 if (startsWith(model, "Raspberry Pi 3 Model B Plus") || (model == "Raspberry Pi 3 Model B+"))
801 ✗ return kSysTypeFPPRaspberryPi3BPlus;
802
3/6
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3310 times.
6620 if (startsWith(model, "Raspberry Pi Zero Rev"))
803 ✗ return kSysTypeFPPRaspberryPiZero;
804
3/6
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3310 times.
6620 if (startsWith(model, "Raspberry Pi Zero W"))
805 ✗ return kSysTypeFPPRaspberryPiZeroW;
806
3/6
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3310 times.
6620 if (startsWith(model, "Raspberry Pi Zero 2 W"))
807 ✗ return kSysTypeFPPRaspberryPiZero2W;
808
8/20
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3310 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3310 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 3310 times.
✓ Branch 10 taken 3310 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3310 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3310 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
9930 if (startsWith(model, "Raspberry Pi 3 Model A Plus") || (model == "Raspberry Pi 3 Model A+"))
809 ✗ return kSysTypeFPPRaspberryPi3APlus;
810
15/30
✓ Branch 0 taken 3310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3310 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3307 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 3307 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3307 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 3307 times.
✓ Branch 12 taken 3307 times.
✓ Branch 13 taken 3 times.
✓ Branch 14 taken 3307 times.
✓ Branch 15 taken 3 times.
✓ Branch 16 taken 3310 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 3310 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 3 times.
✓ Branch 21 taken 3307 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
16544 if (startsWith(model, "Raspberry Pi 4") || startsWith(model, "Raspberry Pi Compute Module 4"))
811 3 return kSysTypeFPPRaspberryPi4;
812
15/30
✓ Branch 0 taken 3307 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3307 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3253 times.
✓ Branch 5 taken 54 times.
✓ Branch 6 taken 3253 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3253 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 3253 times.
✓ Branch 12 taken 3253 times.
✓ Branch 13 taken 54 times.
✓ Branch 14 taken 3253 times.
✓ Branch 15 taken 54 times.
✓ Branch 16 taken 3307 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 3307 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 54 times.
✓ Branch 21 taken 3253 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
16427 if (startsWith(model, "Raspberry Pi 5") || startsWith(model, "Raspberry Pi Compute Module 5"))
813 54 return kSysTypeFPPRaspberryPi5;
814
3/6
✓ Branch 0 taken 3253 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3253 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3253 times.
6506 if (startsWith(model, "SanCloud BeagleBone Enhanced"))
815 ✗ return kSysTypeFPPSanCloudBeagleBoneEnhanced;
816
4/6
✓ Branch 0 taken 3253 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3253 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✓ Branch 5 taken 3232 times.
6506 if (contains(model, "BeagleBone Black")) {
817
3/6
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 21 times.
42 if (contains(model, "Wireless")) {
818 ✗ return kSysTypeFPPBeagleBoneBlackWireless;
819 }
820 21 return kSysTypeFPPBeagleBoneBlack;
821 }
822
3/6
✓ Branch 0 taken 3232 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3232 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3232 times.
6464 if (contains(model, "BeagleBone Green")) {
823 ✗ if (contains(model, "Wireless")) {
824 ✗ return kSysTypeFPPBeagleBoneGreenWireless;
825 }
826 ✗ return kSysTypeFPPBeagleBoneGreen;
827 }
828 // This is fed both the device-tree model ("BeagleBoard.org PocketBeagle2")
829 // and, for HTTP-discovered controllers, a remote's 'Variant' setting, which
830 // spells the board with a space ("PocketBeagle 2", "PocketBeagle 2
831 // Industrial"). Both spellings have to be caught here, and before the
832 // PocketBeagle test below -- otherwise a PocketBeagle 2 falls through and
833 // gets reported as a PocketBeagle 1.
834
14/28
✓ Branch 0 taken 3232 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3232 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3158 times.
✓ Branch 5 taken 74 times.
✓ Branch 6 taken 3158 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3158 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3156 times.
✓ Branch 11 taken 2 times.
✓ Branch 12 taken 3158 times.
✓ Branch 13 taken 74 times.
✓ Branch 14 taken 3232 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 3232 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 76 times.
✓ Branch 19 taken 3156 times.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
22400 if (contains(model, "PocketBeagle2") || contains(model, "PocketBeagle 2") ||
835
9/16
✓ Branch 0 taken 3156 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3156 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3156 times.
✓ Branch 6 taken 3156 times.
✓ Branch 7 taken 76 times.
✓ Branch 8 taken 3156 times.
✓ Branch 9 taken 76 times.
✓ Branch 10 taken 3158 times.
✓ Branch 11 taken 74 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
9544 contains(model, "BeaglePlay")) {
836 76 return kSysTypeFPPPocketBeagle2;
837 }
838
4/6
✓ Branch 0 taken 3156 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3156 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 3145 times.
6312 if (contains(model, "PocketBeagle")) {
839 11 return kSysTypeFPPPocketBeagle;
840 }
841
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3145 times.
3145 if (model == "MacOS") {
842 ✗ return kSysTypeMacOS;
843 }
844
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3145 times.
3145 if (model == "Armbian") {
845 ✗ return kSysTypeFPPArmbian;
846 }
847 // FIXME, fill in the rest of the types
848
849 3145 return kSysTypeFPP;
850 }
851
852 /*
853 *
854 */
855 3302 bool MultiSync::FillLocalSystemInfo(void) {
856 3302 MultiSyncSystem newSystem;
857
858
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 std::string model = GetHardwareModel();
859 #ifdef PLATFORM_ARMBIAN
860 MultiSyncSystemType type = ModelStringToType("Armbian");
861 #else
862
2/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3302 times.
✗ Branch 3 not taken.
3302 MultiSyncSystemType type = ModelStringToType(model);
863 #endif
864
865
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 std::string multiSyncInterface = getSetting("MultiSyncInterface");
866 char addressBuf[128];
867 3302 std::list<std::string> addresses;
868
869 3302 std::string dockerAddress;
870
3/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3140 times.
✓ Branch 3 taken 162 times.
3302 if (FileExists("/etc/fpp/container")) {
871
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3140 times.
3140 if (getenv("FPP_DOCKER_IP")) {
872 ✗ dockerAddress = getenv("FPP_DOCKER_IP");
873 ✗ addresses.push_back(dockerAddress);
874 }
875
876
1/2
✓ Branch 0 taken 3140 times.
✗ Branch 1 not taken.
3140 std::string a = getSetting("MultiSyncExternalIPAddress");
877
6/8
✓ Branch 0 taken 3140 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3139 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 3139 times.
3140 if (a != "" && a != dockerAddress) {
878
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 addresses.push_back(a);
879
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 dockerAddress = a;
880 }
881 3140 }
882
883
7/10
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3302 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3302 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3301 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 3301 times.
✓ Branch 9 taken 1 times.
3302 if (multiSyncInterface == "" && dockerAddress == "") {
884 // get all the addresses
885 struct ifaddrs *interfaces, *tmp;
886 3301 getifaddrs(&interfaces);
887 3301 tmp = interfaces;
888 3301 std::vector<std::pair<std::string, std::string>> ifAddrs; // interface, address
889 3301 std::set<std::string> routableIfs;
890
2/2
✓ Branch 0 taken 17850 times.
✓ Branch 1 taken 3301 times.
21151 while (tmp) {
891
3/4
✓ Branch 0 taken 17850 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6923 times.
✓ Branch 3 taken 10927 times.
17850 if (tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_INET) {
892
1/2
✓ Branch 0 taken 6923 times.
✗ Branch 1 not taken.
6923 if (strncmp("usb", tmp->ifa_name, 3) != 0) {
893 // skip the usb* interfaces as we won't support multisync on those
894 6923 memset(addressBuf, 0, sizeof(addressBuf));
895
896 6923 struct sockaddr_in* sa = (struct sockaddr_in*)(tmp->ifa_addr);
897 6923 inet_ntop(AF_INET, &sa->sin_addr, addressBuf, INET_ADDRSTRLEN);
898
3/4
✓ Branch 0 taken 6923 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3618 times.
✓ Branch 3 taken 3305 times.
6923 if (isSupportedForMultisync(addressBuf, tmp->ifa_name)) {
899
1/2
✓ Branch 0 taken 3618 times.
✗ Branch 1 not taken.
3618 ifAddrs.emplace_back(tmp->ifa_name, addressBuf);
900
3/6
✓ Branch 0 taken 3618 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3618 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3618 times.
✗ Branch 5 not taken.
7236 if (!IsIPv4LinkLocal(addressBuf)) {
901
2/4
✓ Branch 0 taken 3618 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3618 times.
✗ Branch 3 not taken.
7236 routableIfs.insert(tmp->ifa_name);
902 }
903 }
904 }
905
1/2
✓ Branch 0 taken 10927 times.
✗ Branch 1 not taken.
10927 } else if (tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_INET6) {
906 // FIXME for ipv6 multisync
907 }
908 17850 tmp = tmp->ifa_next;
909 }
910 3301 freeifaddrs(interfaces);
911 // A 169.254 address alongside a DHCP/static one on the same interface
912 // is only the fallback from before the lease arrived. Announcing it
913 // too just lists this box twice on every peer, once at an address
914 // nothing off this link can reach.
915
2/2
✓ Branch 0 taken 3618 times.
✓ Branch 1 taken 3301 times.
6919 for (auto& [intf, addr] : ifAddrs) {
916
3/10
✓ Branch 0 taken 3618 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3618 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 3618 times.
✗ Branch 9 not taken.
3618 if (!IsIPv4LinkLocal(addr) || !routableIfs.contains(intf)) {
917
1/2
✓ Branch 0 taken 3618 times.
✗ Branch 1 not taken.
3618 addresses.push_back(addr);
918 }
919 }
920
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
3302 } else if (dockerAddress == "") {
921 ✗ memset(addressBuf, 0, sizeof(addressBuf));
922 ✗ GetInterfaceAddress(multiSyncInterface.c_str(), addressBuf, NULL, NULL);
923 ✗ addresses.push_back(addressBuf);
924 }
925
926
3/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✓ Branch 3 taken 1414 times.
3302 if (m_hostname == "") {
927
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 m_hostname = getSetting("HostName");
928 }
929
930
2/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3302 times.
3302 if (m_hostname == "") {
931 ✗ m_hostname = "FPP";
932 }
933
934 3302 newSystem.lastSeen = (unsigned long)time(NULL);
935 3302 newSystem.type = type;
936
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 newSystem.majorVersion = atoi(getFPPMajorVersion());
937
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 newSystem.minorVersion = atoi(getFPPMinorVersion());
938
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 newSystem.hostname = m_hostname;
939
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 newSystem.fppMode = getFPPmode();
940 3302 newSystem.sendingMultiSync = m_multiSyncEnabled;
941
2/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3302 times.
✗ Branch 3 not taken.
3302 newSystem.version = getFPPVersion();
942
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 newSystem.model = model;
943 3302 newSystem.ipa = 0;
944 3302 newSystem.ipb = 0;
945 3302 newSystem.ipc = 0;
946 3302 newSystem.ipd = 0;
947
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 newSystem.uuid = getSetting("SystemUUID");
948
949
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 LogDebug(VB_SYNC, "Host name: %s\n", newSystem.hostname.c_str());
950
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 LogDebug(VB_SYNC, "Version: %s\n", newSystem.version.c_str());
951
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 LogDebug(VB_SYNC, "Model: %s\n", newSystem.model.c_str());
952
953 // The local system's cape. Remotes are probed for theirs over HTTP (see
954 // FetchCapeInfo); the local entry is never probed, so without this the
955 // systems list showed a cape on every box except the one serving the page.
956 // Same file the local /api/cape answers from; absent means no cape, which
957 // is an answer (present=false), not "unknown".
958 {
959
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 Json::Value capeJson;
960
2/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3302 times.
✗ Branch 3 not taken.
3302 std::string capeFile = FPP_DIR_MEDIA("/tmp/cape-info.json");
961
9/14
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 3297 times.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 5 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 5 times.
✓ Branch 13 taken 3297 times.
3302 if (FileExists(capeFile) && LoadJsonFromFile(capeFile, capeJson) && capeJson.isObject()) {
962
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 newSystem.capeInfo = MultiSyncCapeInfo::fromCapeJSON(capeJson);
963 } else {
964 3297 newSystem.capeInfo.valid = true;
965 3297 newSystem.capeInfo.present = false;
966 }
967 3302 }
968
969 3302 bool changed = false;
970
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
971
972 // Addresses this box no longer has. Every ping goes out once per local
973 // system, so an entry left here keeps announcing an address that is gone
974 // -- typically the 169.254 one held until DHCP answered.
975
2/2
✓ Branch 0 taken 1882 times.
✓ Branch 1 taken 3302 times.
5184 for (auto it = m_localSystems.begin(); it != m_localSystems.end();) {
976
3/4
✓ Branch 0 taken 1882 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 427 times.
✓ Branch 3 taken 1455 times.
1882 if (std::find(addresses.begin(), addresses.end(), it->address) == addresses.end()) {
977
1/2
✓ Branch 0 taken 427 times.
✗ Branch 1 not taken.
427 LogDebug(VB_SYNC, "Removing Local System Address: %s\n", it->address.c_str());
978 427 changed = true;
979
1/2
✓ Branch 0 taken 427 times.
✗ Branch 1 not taken.
427 it = m_localSystems.erase(it);
980 } else {
981 1455 ++it;
982 }
983 }
984
985
3/4
✓ Branch 0 taken 3619 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3619 times.
✓ Branch 3 taken 3302 times.
6921 for (auto address : addresses) {
986
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3619 times.
3619 if (address.empty()) {
987 // A failed GetInterfaceAddress()/inet_ntop() leaves the buffer empty;
988 // skip it rather than record a bogus local system.
989 ✗ continue;
990 }
991 3619 bool found = false;
992
2/2
✓ Branch 0 taken 1976 times.
✓ Branch 1 taken 3619 times.
5595 for (auto& sys : m_localSystems) {
993
2/2
✓ Branch 0 taken 1455 times.
✓ Branch 1 taken 521 times.
1976 if (sys.address == address) {
994 1455 found = true;
995
1/2
✓ Branch 0 taken 1455 times.
✗ Branch 1 not taken.
1455 sys.capeInfo = newSystem.capeInfo;
996 }
997 }
998
2/2
✓ Branch 0 taken 2164 times.
✓ Branch 1 taken 1455 times.
3619 if (!found) {
999
1/2
✓ Branch 0 taken 2164 times.
✗ Branch 1 not taken.
2164 LogDebug(VB_SYNC, "Adding Local System Address: %s\n", address.c_str());
1000 2164 changed = true;
1001
1/2
✓ Branch 0 taken 2164 times.
✗ Branch 1 not taken.
2164 newSystem.address = address;
1002
1/2
✓ Branch 0 taken 2164 times.
✗ Branch 1 not taken.
2164 std::vector<std::string> parts = split(newSystem.address, '.');
1003
2/2
✓ Branch 0 taken 2163 times.
✓ Branch 1 taken 1 times.
2164 if (parts.size() >= 4) {
1004 2163 newSystem.ipa = atoi(parts[0].c_str());
1005 2163 newSystem.ipb = atoi(parts[1].c_str());
1006 2163 newSystem.ipc = atoi(parts[2].c_str());
1007 2163 newSystem.ipd = atoi(parts[3].c_str());
1008 } else {
1009 1 newSystem.ipa = 0;
1010 1 newSystem.ipb = 0;
1011 1 newSystem.ipc = 0;
1012 1 newSystem.ipd = 0;
1013 }
1014
1/2
✓ Branch 0 taken 2164 times.
✗ Branch 1 not taken.
2164 m_localSystems.push_back(newSystem);
1015 2164 }
1016
1/2
✓ Branch 0 taken 3619 times.
✗ Branch 1 not taken.
3619 }
1017 3302 return changed;
1018 3302 }
1019
1020 /*
1021 *
1022 */
1023 3302 std::string MultiSync::GetHardwareModel(void) {
1024 #ifdef PLATFORM_OSX
1025 return "MacOS";
1026 #else
1027 3302 std::string result;
1028 3302 std::string filename;
1029
1030
3/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 160 times.
✓ Branch 3 taken 3142 times.
3302 if (FileExists("/sys/firmware/devicetree/base/model"))
1031
1/2
✓ Branch 0 taken 160 times.
✗ Branch 1 not taken.
160 filename = "/sys/firmware/devicetree/base/model";
1032
2/4
✓ Branch 0 taken 3142 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3142 times.
✗ Branch 3 not taken.
3142 else if (FileExists("/sys/class/dmi/id/product_name"))
1033
1/2
✓ Branch 0 taken 3142 times.
✗ Branch 1 not taken.
3142 filename = "/sys/class/dmi/id/product_name";
1034
1035
2/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3302 times.
✗ Branch 3 not taken.
3302 if (filename != "") {
1036 char buf[128];
1037
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 FILE* fd = fopen(filename.c_str(), "r");
1038
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 if (fd) {
1039
2/4
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3302 times.
✗ Branch 3 not taken.
3302 if (fgets(buf, 127, fd))
1040
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 result = buf;
1041 else
1042 ✗ result = "Unknown Hardware Platform";
1043
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 fclose(fd);
1044 } else {
1045 ✗ result = "Unknown Hardware Platform";
1046 }
1047 } else {
1048 ✗ result = "Unknown Hardware Platform";
1049 }
1050
1051
4/6
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3302 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3142 times.
✓ Branch 5 taken 160 times.
6604 if (endsWith(result, "\n")) {
1052
3/6
✓ Branch 0 taken 3142 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3142 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3142 times.
✗ Branch 5 not taken.
12568 replaceAll(result, "\n", "");
1053 }
1054
1055
1/2
✓ Branch 0 taken 3302 times.
✗ Branch 1 not taken.
3302 TrimWhiteSpace(result);
1056
1057 3302 return result;
1058 #endif
1059 3302 }
1060
1061 /*
1062 *
1063 */
1064 75675 std::string MultiSync::GetTypeString(MultiSyncSystemType type, bool local) {
1065
3/4
✓ Branch 0 taken 35993 times.
✓ Branch 1 taken 39682 times.
✓ Branch 2 taken 35993 times.
✗ Branch 3 not taken.
75675 if (local && type == kSysTypeFPP) {
1066 // unknown hardware, but we can figure out the OS version
1067
1/2
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
35993 if (FileExists("/etc/os-release")) {
1068
2/4
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35993 times.
✗ Branch 3 not taken.
35993 std::string file = GetFileContents("/etc/os-release");
1069
1/2
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
35993 std::vector<std::string> lines = split(file, '\n');
1070 35993 std::map<std::string, std::string> values;
1071
2/2
✓ Branch 0 taken 359930 times.
✓ Branch 1 taken 35993 times.
395923 for (auto& str : lines) {
1072 359930 size_t pos = str.find("=");
1073
1/2
✓ Branch 0 taken 359930 times.
✗ Branch 1 not taken.
359930 if (pos != std::string::npos) {
1074
1/2
✓ Branch 0 taken 359930 times.
✗ Branch 1 not taken.
359930 std::string key = str.substr(0, pos);
1075
1/2
✓ Branch 0 taken 359930 times.
✗ Branch 1 not taken.
359930 std::string val = str.substr(pos + 1);
1076
2/2
✓ Branch 0 taken 251951 times.
✓ Branch 1 taken 107979 times.
359930 if (val[0] == '"') {
1077
1/2
✓ Branch 0 taken 251951 times.
✗ Branch 1 not taken.
251951 val = val.substr(1, val.length() - 2);
1078 }
1079
1/2
✓ Branch 0 taken 359930 times.
✗ Branch 1 not taken.
359930 TrimWhiteSpace(key);
1080
1/2
✓ Branch 0 taken 359930 times.
✗ Branch 1 not taken.
359930 TrimWhiteSpace(val);
1081
2/4
✓ Branch 0 taken 359930 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 359930 times.
✗ Branch 3 not taken.
359930 values[key] = val;
1082 359930 }
1083 }
1084
4/8
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35993 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35993 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 35993 times.
✗ Branch 7 not taken.
71986 if (values["NAME"] != "") {
1085
4/8
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35993 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35993 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 35993 times.
✗ Branch 7 not taken.
71986 return "FPP (" + values["NAME"] + ")";
1086 }
1087 ✗ if (values["PRETTY_NAME"] != "") {
1088 ✗ return "FPP (" + values["PRETTY_NAME"] + ")";
1089 }
1090
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 35993 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 35993 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 35993 times.
107979 }
1091 ✗ return "FPP (unknown hardware)";
1092 }
1093
15/59
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 39527 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✓ Branch 14 taken 6 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✓ Branch 24 taken 3 times.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✓ Branch 35 taken 1 times.
✗ Branch 36 not taken.
✓ Branch 37 taken 10 times.
✗ Branch 38 not taken.
✗ Branch 39 not taken.
✗ Branch 40 not taken.
✗ Branch 41 not taken.
✗ Branch 42 not taken.
✗ Branch 43 not taken.
✓ Branch 44 taken 2 times.
✗ Branch 45 not taken.
✗ Branch 46 not taken.
✓ Branch 47 taken 2 times.
✓ Branch 48 taken 31 times.
✓ Branch 49 taken 12 times.
✗ Branch 50 not taken.
✗ Branch 51 not taken.
✓ Branch 52 taken 4 times.
✓ Branch 53 taken 20 times.
✓ Branch 54 taken 12 times.
✗ Branch 55 not taken.
✓ Branch 56 taken 20 times.
✓ Branch 57 taken 6 times.
✗ Branch 58 not taken.
39682 switch (type) {
1094 26 case kSysTypeUnknown:
1095
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
52 return "Unknown System Type";
1096 39527 case kSysTypeFPP:
1097
1/2
✓ Branch 0 taken 39527 times.
✗ Branch 1 not taken.
79054 return "FPP (unknown hardware)";
1098 ✗ case kSysTypeFPPRaspberryPiA:
1099 ✗ return "Raspberry Pi A";
1100 ✗ case kSysTypeFPPRaspberryPiB:
1101 ✗ return "Raspberry Pi B";
1102 ✗ case kSysTypeFPPRaspberryPiAPlus:
1103 ✗ return "Raspberry Pi A Plus";
1104 ✗ case kSysTypeFPPRaspberryPiBPlus:
1105 ✗ return "Raspberry Pi B+";
1106 ✗ case kSysTypeFPPRaspberryPi2B:
1107 ✗ return "Raspberry Pi 2 B";
1108 ✗ case kSysTypeFPPRaspberryPi2BNew:
1109 ✗ return "Raspberry Pi 2 B v1.2+";
1110 ✗ case kSysTypeFPPRaspberryPi3B:
1111 ✗ return "Raspberry Pi 3 B";
1112 ✗ case kSysTypeFPPRaspberryPi3BPlus:
1113 ✗ return "Raspberry Pi 3 B+";
1114 ✗ case kSysTypeFPPRaspberryPiZero:
1115 ✗ return "Raspberry Pi Zero";
1116 ✗ case kSysTypeFPPRaspberryPiZeroW:
1117 ✗ return "Raspberry Pi Zero W";
1118 ✗ case kSysTypeFPPRaspberryPiZero2W:
1119 ✗ return "Raspberry Pi Zero 2 W";
1120 ✗ case kSysTypeFPPRaspberryPi3APlus:
1121 ✗ return "Raspberry Pi 3 A+";
1122 6 case kSysTypeFPPRaspberryPi4:
1123
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 return "Raspberry Pi 4";
1124 ✗ case kSysTypeFPPRaspberryPi5:
1125 ✗ return "Raspberry Pi 5";
1126 ✗ case kSysTypeFalconController:
1127 ✗ return "Falcon Controller";
1128 ✗ case kSysTypeFalconF16v2:
1129 ✗ return "Falcon F16v2";
1130 ✗ case kSysTypeFalconF4v2_64Mb:
1131 ✗ return "Falcon F4v2_64Mb";
1132 ✗ case kSysTypeFalconF16v2R:
1133 ✗ return "Falcon F16v2R";
1134 ✗ case kSysTypeFalconF4v2:
1135 ✗ return "Falcon F4v2";
1136 ✗ case kSysTypeFalconF4v3:
1137 ✗ return "Falcon F4v3";
1138 ✗ case kSysTypeFalconF16v3:
1139 ✗ return "Falcon F16v3";
1140 ✗ case kSysTypeFalconF48:
1141 ✗ return "Falcon F48";
1142 3 case kSysTypeFalconF16v4:
1143
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
6 return "Falcon F16v4";
1144 ✗ case kSysTypeFalconF48v4:
1145 ✗ return "Falcon F48v4";
1146 ✗ case kSysTypeFalconF16v5:
1147 ✗ return "Falcon F16v5";
1148 ✗ case kSysTypeFalconF32v5:
1149 ✗ return "Falcon F32v5";
1150 ✗ case kSysTypeFalconF48v5:
1151 ✗ return "Falcon F48v5";
1152 ✗ case kSysTypeExperienceGP16:
1153 ✗ return "Genius Pixel 16";
1154 ✗ case kSysTypeExperienceGP8:
1155 ✗ return "Genius Pixel 8";
1156 ✗ case kSysTypeExperienceGLR:
1157 ✗ return "Genius Long Range";
1158 ✗ case kSysTypeExperienceG16Pro:
1159 ✗ return "Genius PRO 16";
1160 ✗ case kSysTypeExperienceG32Pro:
1161 ✗ return "Genius PRO 32";
1162 ✗ case kSysTypeExperienceAURORACORE16:
1163 ✗ return "LOR AURORA CORE 16";
1164 1 case kSysTypeExperienceVIVID8:
1165
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 return "YPS VIVID 8";
1166 ✗ case kSysTypeExperiencePIXELLINK4:
1167 ✗ return "Genius PIXEL LINK 4";
1168 10 case kSysTypeExperienceGenius:
1169
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
20 return "Genius Controller";
1170 ✗ case kSysTypeOtherSystem:
1171 ✗ return "Other Unknown System";
1172 ✗ case kSysTypeFPPBeagleBoneBlack:
1173 ✗ return "BeagleBone Black";
1174 ✗ case kSysTypeFPPBeagleBoneBlackWireless:
1175 ✗ return "BeagleBone Black Wireless";
1176 ✗ case kSysTypeFPPBeagleBoneGreen:
1177 ✗ return "BeagleBone Green";
1178 ✗ case kSysTypeFPPBeagleBoneGreenWireless:
1179 ✗ return "BeagleBone Green Wireless";
1180 ✗ case kSysTypeFPPPocketBeagle:
1181 ✗ return "PocketBeagle";
1182 2 case kSysTypeFPPPocketBeagle2:
1183
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 return "PocketBeagle2";
1184 ✗ case kSysTypeFPPSanCloudBeagleBoneEnhanced:
1185 ✗ return "SanCloud BeagleBone Enhanced";
1186 ✗ case kSysTypexSchedule:
1187 ✗ return "xSchedule";
1188 2 case kSysTypeESPixelStick:
1189
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 return "ESPixelStick-ESP8266";
1190 31 case kSysTypeESPixelStickESP32:
1191
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
62 return "ESPixelStick-ESP32";
1192 12 case kSysTypeBaldrick:
1193
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
24 return "Baldrick";
1194 ✗ case kSysTypeMacOS:
1195 ✗ return "MacOS";
1196 ✗ case kSysTypeFPPArmbian:
1197 ✗ return "Armbian";
1198 4 case kSysTypeSanDevices:
1199
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
8 return "SanDevices";
1200 20 case kSysTypeAlphaPix:
1201
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
40 return "AlphaPix";
1202 12 case kSysTypeHinksPix:
1203
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
24 return "HinksPix";
1204 ✗ case kSysTypeDIYLEDExpress:
1205 ✗ return "DIYLEDExpress";
1206 20 case kSysTypeWLED:
1207
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
40 return "WLED";
1208 6 case kSysTypeTwinkly:
1209
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 return "Twinkly";
1210 ✗ default:
1211 ✗ return "Unknown System Type";
1212 }
1213 }
1214
1215 25463 static std::string createRanges(std::vector<std::pair<uint32_t, uint32_t>> ranges, int limit) {
1216 25463 bool first = true;
1217
1/2
✓ Branch 0 taken 25463 times.
✗ Branch 1 not taken.
25463 std::string range("");
1218 char buf[64];
1219 25463 memset(buf, 0, sizeof(buf));
1220
2/2
✓ Branch 0 taken 25633 times.
✓ Branch 1 taken 25463 times.
51096 for (auto& a : ranges) {
1221
2/2
✓ Branch 0 taken 170 times.
✓ Branch 1 taken 25463 times.
25633 if (!first) {
1222
1/2
✓ Branch 0 taken 170 times.
✗ Branch 1 not taken.
170 range += ",";
1223 }
1224 25633 snprintf(buf, sizeof(buf), "%d-%d", a.first, (a.first + a.second - 1));
1225
1/2
✓ Branch 0 taken 25633 times.
✗ Branch 1 not taken.
25633 range += buf;
1226 25633 first = false;
1227 }
1228
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 25463 times.
25472 while (range.size() > limit) {
1229 // range won't fit within the space in the Ping packet, we need to shrink the range
1230 // we'll find the smallest gap and combine into a larger range
1231 9 int minGap = 9999999;
1232 9 int minIdx = -1;
1233 9 int last = ranges[0].first + ranges[0].second - 1;
1234
2/2
✓ Branch 0 taken 90 times.
✓ Branch 1 taken 9 times.
99 for (int x = 1; x < ranges.size(); x++) {
1235 90 int gap = ranges[x].first - last;
1236
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 72 times.
90 if (gap < minGap) {
1237 18 minIdx = x;
1238 18 minGap = gap;
1239 }
1240 90 last = ranges[x].first + ranges[x].second - 1;
1241 }
1242
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (minIdx > 0) {
1243 9 int newLast = ranges[minIdx].first + ranges[minIdx].second;
1244 9 ranges[minIdx - 1].second = newLast - ranges[minIdx - 1].first;
1245
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 ranges.erase(ranges.begin() + minIdx);
1246 }
1247
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 range = createRanges(ranges, 999999);
1248 }
1249 25463 return range;
1250 ✗ }
1251
1252 // Build a map of configured-output controller address -> channel range string.
1253 // Output-only controllers (WLED, Falcon, etc.) generally don't report their own
1254 // channel ranges, but FPP knows them from its universe output configuration, so
1255 // we backfill the ranges from our own config (issue #2667). Each address is
1256 // keyed both by the raw configured value and by its resolved IP so that we can
1257 // match a system regardless of whether it was discovered/stored by hostname or
1258 // by IP.
1259 1955 static std::map<std::string, std::string> GetConfiguredOutputRanges() {
1260 1955 std::map<std::string, std::vector<std::pair<uint32_t, uint32_t>>> byAddress;
1261
1262
6/10
✓ Branch 0 taken 1955 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1955 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1955 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1955 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 796 times.
✓ Branch 9 taken 1159 times.
3910 if (FileExists(FPP_DIR_CONFIG("/co-universes.json"))) {
1263
4/8
✓ Branch 0 taken 796 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 796 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 796 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 796 times.
✗ Branch 7 not taken.
796 Json::Value outputs = LoadJsonFromFile(FPP_DIR_CONFIG("/co-universes.json"), JsonRoot::Object);
1264
1/2
✓ Branch 0 taken 796 times.
✗ Branch 1 not taken.
796 const Json::Value& cos = outputs["channelOutputs"];
1265
8/10
✓ Branch 0 taken 1589 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1588 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1588 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 793 times.
✓ Branch 7 taken 795 times.
✓ Branch 8 taken 793 times.
✓ Branch 9 taken 796 times.
1589 for (Json::ArrayIndex i = 0; cos.isArray() && i < cos.size(); i++) {
1266
1/2
✓ Branch 0 taken 793 times.
✗ Branch 1 not taken.
793 const Json::Value& co = cos[i];
1267
6/12
✓ Branch 0 taken 793 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 793 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 793 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 793 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 793 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 793 times.
793 if (!co.isObject() || !co["universes"].isArray()) {
1268 ✗ continue;
1269 }
1270
8/14
✓ Branch 0 taken 793 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 793 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 793 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 903 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 903 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1696 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 903 times.
✓ Branch 13 taken 793 times.
1696 for (const auto& u : co["universes"]) {
1271 // Read at startup: a universe with a field of the wrong type is
1272 // left out here (ChannelOutputSetup reports it), not thrown on.
1273
5/8
✓ Branch 0 taken 903 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 902 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 902 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 901 times.
✓ Branch 7 taken 1 times.
1805 if (!u.isObject() || !u["address"].isString() ||
1274
7/12
✓ Branch 0 taken 901 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 901 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 900 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 900 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 900 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 900 times.
✗ Branch 11 not taken.
901 !u["startChannel"].isIntegral() || !u["channelCount"].isIntegral() ||
1275
7/10
✓ Branch 0 taken 902 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 900 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 900 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 900 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 900 times.
2705 !u["startChannel"].isConvertibleTo(Json::uintValue) ||
1276
3/6
✓ Branch 0 taken 900 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 900 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 900 times.
900 !u["channelCount"].isConvertibleTo(Json::uintValue)) {
1277 4 continue;
1278 }
1279
2/4
✓ Branch 0 taken 900 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 900 times.
✗ Branch 3 not taken.
900 std::string address = u["address"].asString();
1280
2/4
✓ Branch 0 taken 900 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 900 times.
✗ Branch 3 not taken.
900 uint32_t count = u["channelCount"].asUInt();
1281
2/4
✓ Branch 0 taken 900 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 900 times.
✗ Branch 3 not taken.
900 uint32_t start = u["startChannel"].asUInt();
1282
6/8
✓ Branch 0 taken 900 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 900 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 899 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 899 times.
900 if (address.empty() || count == 0 || start == 0) {
1283 1 continue;
1284 }
1285 // channelRanges is a zero-based convention everywhere else --
1286 // see the GetRequiredChannelRange() implementations, which all
1287 // report (startChannel - 1). The universes in co-universes.json
1288 // store the user-facing one-based start channel, so convert here
1289 // or every backfilled remote is reported one channel high.
1290
2/4
✓ Branch 0 taken 899 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 899 times.
✗ Branch 3 not taken.
899 byAddress[address].emplace_back(start - 1, count);
1291
2/2
✓ Branch 0 taken 899 times.
✓ Branch 1 taken 1 times.
900 }
1292 }
1293 796 }
1294
1295 1955 std::map<std::string, std::string> result;
1296
2/2
✓ Branch 0 taken 849 times.
✓ Branch 1 taken 1955 times.
2804 for (auto& [address, ranges] : byAddress) {
1297
1/2
✓ Branch 0 taken 849 times.
✗ Branch 1 not taken.
849 std::sort(ranges.begin(), ranges.end());
1298 // merge contiguous/overlapping ranges so the reported string is clean
1299 849 std::vector<std::pair<uint32_t, uint32_t>> merged;
1300
2/2
✓ Branch 0 taken 899 times.
✓ Branch 1 taken 849 times.
1748 for (const auto& r : ranges) {
1301
6/6
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 849 times.
✓ Branch 2 taken 39 times.
✓ Branch 3 taken 11 times.
✓ Branch 4 taken 39 times.
✓ Branch 5 taken 860 times.
899 if (!merged.empty() && r.first <= (merged.back().first + merged.back().second)) {
1302 39 uint32_t end = std::max(merged.back().first + merged.back().second, r.first + r.second);
1303 39 merged.back().second = end - merged.back().first;
1304 } else {
1305
1/2
✓ Branch 0 taken 860 times.
✗ Branch 1 not taken.
860 merged.push_back(r);
1306 }
1307 }
1308
2/4
✓ Branch 0 taken 849 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 849 times.
✗ Branch 3 not taken.
849 std::string rangeStr = createRanges(merged, 999999);
1309
2/4
✓ Branch 0 taken 849 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 849 times.
✗ Branch 3 not taken.
849 result[address] = rangeStr;
1310
1/2
✓ Branch 0 taken 849 times.
✗ Branch 1 not taken.
849 std::string ip = address;
1311
7/8
✓ Branch 0 taken 849 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 843 times.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 841 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 847 times.
849 if (GetIPForHost(ip) && ip != address) {
1312
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 result[ip] = rangeStr;
1313 }
1314 849 }
1315 1955 return result;
1316 1955 }
1317
1318 1955 void MultiSync::ReloadConfiguredOutputRanges() {
1319 // Build the map outside the lock: GetConfiguredOutputRanges() reads
1320 // co-universes.json and may run blocking DNS lookups to resolve configured
1321 // output hostnames. Then swap it in under the lock that guards GetSystems().
1322 // This runs once at startup and again only when co-universes.json changes.
1323
1/2
✓ Branch 0 taken 1955 times.
✗ Branch 1 not taken.
1955 std::map<std::string, std::string> ranges = GetConfiguredOutputRanges();
1324
1/2
✓ Branch 0 taken 1955 times.
✗ Branch 1 not taken.
1955 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
1325 1955 m_configuredOutputRanges = std::move(ranges);
1326 1955 }
1327
1328 20341 Json::Value MultiSync::GetSystems(bool localOnly, bool timestamps) {
1329
1/2
✓ Branch 0 taken 20341 times.
✗ Branch 1 not taken.
20341 Json::Value result;
1330
1/2
✓ Branch 0 taken 20341 times.
✗ Branch 1 not taken.
20341 Json::Value systems(Json::arrayValue);
1331
1332
1/2
✓ Branch 0 taken 20341 times.
✗ Branch 1 not taken.
20341 auto ranges = GetOutputRangesSnapshot(true);
1333
2/4
✓ Branch 0 taken 20341 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20341 times.
✗ Branch 3 not taken.
20341 std::string range = createRanges(*ranges, 999999);
1334
1335
1/2
✓ Branch 0 taken 20341 times.
✗ Branch 1 not taken.
20341 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
1336
2/2
✓ Branch 0 taken 35993 times.
✓ Branch 1 taken 20341 times.
56334 for (auto& sys : m_localSystems) {
1337
1/2
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
35993 sys.ranges = range;
1338
2/4
✓ Branch 0 taken 35993 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35993 times.
✗ Branch 3 not taken.
35993 systems.append(sys.toJSON(true, timestamps));
1339 }
1340
2/2
✓ Branch 0 taken 16307 times.
✓ Branch 1 taken 4034 times.
20341 if (!localOnly) {
1341 // m_configuredOutputRanges is rebuilt only when co-universes.json changes
1342 // (see ReloadConfiguredOutputRanges()), so this is a plain in-memory map
1343 // lookup -- no per-call file read and, crucially, no blocking DNS. (This
1344 // endpoint is polled frequently by the UI and is also invoked indirectly
1345 // by the config backup generated on every settings change, so the old
1346 // resolve-per-remote-per-call cost was very visible.) We only backfill a
1347 // range for remotes that didn't advertise one of their own, and we do it
1348 // in the emitted JSON rather than mutating the stored system so a later
1349 // co-universes.json change is still reflected.
1350
2/2
✓ Branch 0 taken 39681 times.
✓ Branch 1 taken 16307 times.
55988 for (auto& sys : m_remoteSystems) {
1351
1/2
✓ Branch 0 taken 39681 times.
✗ Branch 1 not taken.
39681 Json::Value sysJson = sys.toJSON(false, timestamps);
1352
9/10
✓ Branch 0 taken 12258 times.
✓ Branch 1 taken 27423 times.
✓ Branch 2 taken 12258 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 114 times.
✓ Branch 5 taken 12144 times.
✓ Branch 6 taken 14 times.
✓ Branch 7 taken 27523 times.
✓ Branch 8 taken 14 times.
✓ Branch 9 taken 39667 times.
39681 if ((sys.ranges.empty() || sys.ranges == "0-0") && !m_configuredOutputRanges.empty()) {
1353
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 auto it = m_configuredOutputRanges.find(sys.address);
1354
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 7 times.
14 if (it == m_configuredOutputRanges.end()) {
1355
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 it = m_configuredOutputRanges.find(sys.hostname);
1356 }
1357
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 7 times.
14 if (it != m_configuredOutputRanges.end()) {
1358
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 sysJson["channelRanges"] = it->second;
1359 }
1360 }
1361
1/2
✓ Branch 0 taken 39681 times.
✗ Branch 1 not taken.
39681 systems.append(sysJson);
1362 39681 }
1363 }
1364
2/4
✓ Branch 0 taken 20341 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20341 times.
✗ Branch 3 not taken.
20341 result["systems"] = systems;
1365 20341 return result;
1366 20341 }
1367
1368 // The packet counters are bumped as packets are processed, on the main loop,
1369 // without m_statsLock (only the lookup of a sender's stats takes it), while
1370 // GetSyncStats() reads them and ResetSyncStats() zeroes them from API threads.
1371 // So every access is a relaxed atomic one; MultiSyncStats keeps plain fields,
1372 // as MultiSync.h is a plugin-facing header.
1373 11360 static inline void BumpStat(uint32_t& c) {
1374 11360 __atomic_fetch_add(&c, 1, __ATOMIC_RELAXED);
1375 11360 }
1376
1377 89 Json::Value MultiSync::GetSyncStats() {
1378
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 Json::Value result;
1379
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 Json::Value systems(Json::arrayValue);
1380
1381
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 std::unique_lock<std::recursive_mutex> slock(m_statsLock);
1382
2/2
✓ Branch 0 taken 2130 times.
✓ Branch 1 taken 89 times.
2219 for (auto& a : m_syncStats) {
1383 2130 MultiSyncStats* stats = (MultiSyncStats*)a.second;
1384
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 systems.append(stats->toJSON());
1385 }
1386 // m_syncMaster is rewritten from the sync-receive path on the main loop, so
1387 // it is only ever touched under m_statsLock. Copy it out here and use the
1388 // copy below: m_statsLock and m_systemsLock are never held together.
1389
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 std::string syncMaster = m_syncMaster;
1390
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 slock.unlock();
1391
1392
2/4
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
89 result["systems"] = systems;
1393
1394 89 std::string masterHostname;
1395
1396
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
1397
2/2
✓ Branch 0 taken 462 times.
✓ Branch 1 taken 89 times.
551 for (auto& sys : m_remoteSystems) {
1398
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 462 times.
462 if (sys.address == syncMaster)
1399 ✗ masterHostname = sys.hostname;
1400 }
1401
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 lock.unlock();
1402
1403
2/4
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
89 result["masterIP"] = syncMaster;
1404
2/4
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
89 result["masterHostname"] = masterHostname;
1405
1406 89 return result;
1407 89 }
1408
1409 2 void MultiSync::ResetSyncStats() {
1410
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::unique_lock<std::recursive_mutex> slock(m_statsLock);
1411
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& a : m_syncStats) {
1412 2 MultiSyncStats* stats = (MultiSyncStats*)a.second;
1413 2 __atomic_store_n(&stats->pktCommand, 0, __ATOMIC_RELAXED);
1414 2 __atomic_store_n(&stats->pktSyncSeqOpen, 0, __ATOMIC_RELAXED);
1415 2 __atomic_store_n(&stats->pktSyncSeqStart, 0, __ATOMIC_RELAXED);
1416 2 __atomic_store_n(&stats->pktSyncSeqStop, 0, __ATOMIC_RELAXED);
1417 2 __atomic_store_n(&stats->pktSyncSeqSync, 0, __ATOMIC_RELAXED);
1418 2 __atomic_store_n(&stats->pktSyncMedOpen, 0, __ATOMIC_RELAXED);
1419 2 __atomic_store_n(&stats->pktSyncMedStart, 0, __ATOMIC_RELAXED);
1420 2 __atomic_store_n(&stats->pktSyncMedStop, 0, __ATOMIC_RELAXED);
1421 2 __atomic_store_n(&stats->pktSyncMedSync, 0, __ATOMIC_RELAXED);
1422 2 __atomic_store_n(&stats->pktBlank, 0, __ATOMIC_RELAXED);
1423 2 __atomic_store_n(&stats->pktPing, 0, __ATOMIC_RELAXED);
1424 2 __atomic_store_n(&stats->pktPlugin, 0, __ATOMIC_RELAXED);
1425 2 __atomic_store_n(&stats->pktFPPCommand, 0, __ATOMIC_RELAXED);
1426 2 __atomic_store_n(&stats->pktError, 0, __ATOMIC_RELAXED);
1427 }
1428 2 }
1429
1430 1885 void MultiSync::Discover() {
1431 1885 Ping(1);
1432 1885 PerformHTTPDiscovery();
1433 1885 PerformTwinklyDiscovery();
1434 1885 }
1435
1436 1885 void MultiSync::PerformHTTPDiscovery() {
1437
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 std::string subnetsStr = getSetting("MultiSyncHTTPSubnets");
1438
1439
6/10
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1885 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 729 times.
✓ Branch 9 taken 1156 times.
3770 if (FileExists(FPP_DIR_CONFIG("/co-universes.json"))) {
1440
4/8
✓ Branch 0 taken 729 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 729 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 729 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 729 times.
✗ Branch 7 not taken.
729 Json::Value outputs = LoadJsonFromFile(FPP_DIR_CONFIG("/co-universes.json"), JsonRoot::Object);
1441
1/2
✓ Branch 0 taken 729 times.
✗ Branch 1 not taken.
729 const Json::Value& cos = outputs["channelOutputs"];
1442
8/10
✓ Branch 0 taken 1455 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1454 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1454 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 726 times.
✓ Branch 7 taken 728 times.
✓ Branch 8 taken 726 times.
✓ Branch 9 taken 729 times.
1455 for (Json::ArrayIndex co = 0; cos.isArray() && co < cos.size(); co++) {
1443
8/16
✓ Branch 0 taken 726 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 726 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 726 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 726 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 726 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 726 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 726 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 726 times.
726 if (!cos[co].isObject() || !cos[co]["universes"].isArray()) {
1444 ✗ continue;
1445 }
1446
2/4
✓ Branch 0 taken 726 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 726 times.
✗ Branch 3 not taken.
726 const Json::Value& universes = cos[co]["universes"];
1447
3/4
✓ Branch 0 taken 1538 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 812 times.
✓ Branch 3 taken 726 times.
1538 for (Json::ArrayIndex i = 0; i < universes.size(); i++) {
1448 // shape-checked: this runs at startup, and a wrong type threw
1449
11/16
✓ Branch 0 taken 812 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 812 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 811 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 811 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 811 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 811 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 810 times.
✓ Branch 13 taken 1 times.
✓ Branch 14 taken 810 times.
✓ Branch 15 taken 2 times.
812 if (universes[i].isObject() && universes[i]["address"].isString()) {
1450
3/4
✓ Branch 0 taken 810 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✓ Branch 3 taken 726 times.
810 if (subnetsStr != "") {
1451
1/2
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
84 subnetsStr += ",";
1452 }
1453
4/8
✓ Branch 0 taken 810 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 810 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 810 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 810 times.
✗ Branch 7 not taken.
810 subnetsStr += universes[i]["address"].asString();
1454 }
1455 }
1456 }
1457 729 }
1458
1459
3/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 746 times.
✓ Branch 3 taken 1139 times.
1885 if (subnetsStr != "") {
1460
1/2
✓ Branch 0 taken 746 times.
✗ Branch 1 not taken.
746 std::vector<std::string> tokens = split(subnetsStr, ',');
1461 746 std::set<std::string> subnets;
1462 746 std::set<std::string> exacts;
1463 // This runs at startup: an unbounded "10.0.0.0/8" (16.7M addresses,
1464 // held twice) or "[1-100000000]" ran fppd out of memory on every
1465 // start. A /16 is the most a scan may cover, octet ranges stay
1466 // within 0..255, and the total is capped.
1467 static constexpr size_t MAX_DISCOVERY_ADDRESSES = 65536;
1468 746 bool truncated = false;
1469 488 auto addAddress = [&subnets, &truncated](const std::string& a) {
1470
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 488 times.
488 if (subnets.size() >= MAX_DISCOVERY_ADDRESSES) {
1471 ✗ truncated = true;
1472 ✗ return false;
1473 }
1474
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
488 subnets.insert(a);
1475 488 return true;
1476 746 };
1477
3/4
✓ Branch 0 taken 864 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 864 times.
✓ Branch 3 taken 746 times.
1610 for (std::string token : tokens) {
1478
1/2
✓ Branch 0 taken 864 times.
✗ Branch 1 not taken.
864 TrimWhiteSpace(token);
1479
2/4
✓ Branch 0 taken 864 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 864 times.
✗ Branch 3 not taken.
864 if (token != "") {
1480 864 size_t pos = token.find('[');
1481 864 size_t posSlash = token.find('/');
1482
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 862 times.
864 if (pos != std::string::npos) {
1483
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string rng = token.substr(pos + 1);
1484
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 token = token.substr(0, pos);
1485 2 size_t pos2 = rng.find(']');
1486
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string postfix = "";
1487
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (pos2 != (rng.size() - 1)) {
1488 ✗ postfix = rng.substr(pos2 + 1);
1489 }
1490
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::vector<std::string> r1 = split(rng, ',');
1491
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (const auto& a : r1) {
1492
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::vector<std::string> r = split(a, '-');
1493
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (r.empty()) {
1494 ✗ continue;
1495 }
1496
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 int start = std::clamp(atoi(r[0].c_str()), 0, 255);
1497
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 int end = r.size() == 2 ? std::clamp(atoi(r[1].c_str()), 0, 255) : start;
1498
2/2
✓ Branch 0 taken 306 times.
✓ Branch 1 taken 2 times.
308 for (int x = start; x <= end; x++) {
1499
4/8
✓ Branch 0 taken 306 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 306 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 306 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 306 times.
306 if (!addAddress(token + std::to_string(x) + postfix)) {
1500 ✗ break;
1501 }
1502 }
1503
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 }
1504
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 861 times.
864 } else if (posSlash != std::string::npos) {
1505
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::vector<std::string> parts = split(token, '/');
1506 1 int prefix = 32;
1507
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (parts.size() == 2) {
1508 1 prefix = atoi(parts[1].c_str());
1509 }
1510
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 if (prefix < 16 || prefix > 32) {
1511
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_SYNC, "MultiSync HTTP discovery: ignoring %s, only /16 to /32 can be scanned\n",
1512 token.c_str());
1513 1 continue;
1514 }
1515 ✗ int ips = 1 << (32 - prefix);
1516 ✗ unsigned long firstIP = htonl(inet_addr(parts[0].c_str()));
1517 // If prefix is a /31 or /32, we scan all IPs, otherwise skip network
1518 // and broadcast
1519 ✗ if (prefix <= 30) {
1520 ✗ ips -= 2;
1521 ✗ firstIP++;
1522 }
1523 struct in_addr ia;
1524 char ip[16];
1525 ✗ for (int i = 0; i < ips; i++) {
1526 ✗ ia.s_addr = ntohl(firstIP + i);
1527 ✗ inet_ntop(AF_INET, &ia, ip, sizeof(ip));
1528 ✗ if (!addAddress(ip)) {
1529 ✗ break;
1530 }
1531 }
1532
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 } else {
1533
1/2
✓ Branch 0 taken 861 times.
✗ Branch 1 not taken.
861 std::string address2 = token;
1534
1/2
✓ Branch 0 taken 861 times.
✗ Branch 1 not taken.
861 GetIPForHost(address2);
1535
7/10
✓ Branch 0 taken 861 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 182 times.
✓ Branch 3 taken 679 times.
✓ Branch 4 taken 182 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 182 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 182 times.
✓ Branch 9 taken 679 times.
861 if (isSupportedForMultisync(address2.c_str(), "") && addAddress(token)) {
1536
1/2
✓ Branch 0 taken 182 times.
✗ Branch 1 not taken.
182 exacts.insert(token);
1537 }
1538 861 }
1539 }
1540
2/2
✓ Branch 0 taken 863 times.
✓ Branch 1 taken 1 times.
864 }
1541
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 746 times.
746 if (truncated) {
1542 ✗ LogWarn(VB_SYNC, "MultiSync HTTP discovery: more than %zu addresses configured, scanning only the first %zu\n",
1543 MAX_DISCOVERY_ADDRESSES, MAX_DISCOVERY_ADDRESSES);
1544 }
1545
2/2
✓ Branch 0 taken 101 times.
✓ Branch 1 taken 645 times.
746 if (!subnets.empty()) {
1546
1/2
✓ Branch 0 taken 101 times.
✗ Branch 1 not taken.
101 DiscoverViaHTTP(subnets, exacts);
1547 }
1548 746 }
1549 1885 }
1550
1551 // Twinkly's own discovery protocol: a short datagram to UDP 5555, to which every
1552 // light on the segment replies with its address and name. One broadcast
1553 // enumerates all of them, with none of the per-address cost of an HTTP sweep -
1554 // which matters here because they cannot be found any other way. They publish
1555 // nothing over mDNS or SSDP and their embedded server answers "/" with a 404, so
1556 // a light that is not already a configured channel output is otherwise invisible.
1557 //
1558 // Nothing here (or in the detector this hands off to) authenticates. That is
1559 // deliberate: a Twinkly keeps exactly ONE verified token, so logging in would
1560 // invalidate the token a running Twinkly channel output is streaming with.
1561 #define TWINKLY_DISCOVERY_PORT 5555
1562 static const char TWINKLY_DISCOVERY_MSG[] = { 0x01, 'd', 'i', 's', 'c', 'o', 'v', 'e', 'r' };
1563 // Replies are taken only this long after a discovery broadcast, and from at
1564 // most this many addresses per broadcast. The socket stays open for the life
1565 // of fppd and its port is in every broadcast, so without them any host on the
1566 // LAN could grow the seen set and the probe queue by one entry per source
1567 // address it sent from, for as long as fppd ran. The lights answer within
1568 // milliseconds.
1569 #define TWINKLY_REPLY_WINDOW_MS 5000
1570 #define TWINKLY_MAX_REPLIES 128
1571 static std::atomic<long long> s_twinklyRepliesUntilMS{ 0 };
1572
1573 1885 bool MultiSync::OpenTwinklyDiscoverySocket() {
1574
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
1885 if (m_twinklyDiscoverySock >= 0) {
1575 ✗ return true;
1576 }
1577 1885 int sock = OpenSocket(AF_INET, SOCK_DGRAM, 0);
1578
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
1885 if (sock < 0) {
1579 ✗ LogWarn(VB_SYNC, "Could not open Twinkly discovery socket: %s\n", FPPstrerror(errno));
1580 ✗ return false;
1581 }
1582 1885 int broadcast = 1;
1583
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
1885 if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) {
1584 ✗ LogWarn(VB_SYNC, "Could not set SO_BROADCAST for Twinkly discovery: %s\n", FPPstrerror(errno));
1585 ✗ close(sock);
1586 ✗ return false;
1587 }
1588 1885 m_twinklyDiscoverySock = sock;
1589 3805 std::function<bool(int)> f = [this](int i) {
1590 35 return ProcessTwinklyDiscoveryReplies();
1591 1885 };
1592
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 EPollManager::INSTANCE.addFileDescriptor(m_twinklyDiscoverySock, f);
1593 1885 return true;
1594 1885 }
1595
1596 1885 void MultiSync::PerformTwinklyDiscovery() {
1597
2/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1885 times.
1885 if (!OpenTwinklyDiscoverySocket()) {
1598 ✗ return;
1599 }
1600 {
1601
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 std::unique_lock<std::mutex> lock(m_httpProbeLock);
1602 1885 m_twinklyDiscoverySeen.clear();
1603 1885 }
1604
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 s_twinklyRepliesUntilMS = GetMonotonicTimeMS() + TWINKLY_REPLY_WINDOW_MS;
1605
1606 // Under m_socketLock, which the sync sends need every frame: never block.
1607
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 std::unique_lock<std::mutex> lock(m_socketLock);
1608
2/2
✓ Branch 0 taken 1802 times.
✓ Branch 1 taken 1885 times.
3687 for (auto& a : m_interfaces) {
1609 struct sockaddr_in bda;
1610 1802 memset((void*)&bda, 0, sizeof(struct sockaddr_in));
1611 1802 bda.sin_family = AF_INET;
1612 1802 bda.sin_port = htons(TWINKLY_DISCOVERY_PORT);
1613 1802 bda.sin_addr.s_addr = a.second.broadcastAddress;
1614
1615
1/2
✓ Branch 0 taken 1802 times.
✗ Branch 1 not taken.
1802 if (sendto(m_twinklyDiscoverySock, TWINKLY_DISCOVERY_MSG, sizeof(TWINKLY_DISCOVERY_MSG), MSG_DONTWAIT,
1616
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1802 times.
1802 (struct sockaddr*)&bda, sizeof(struct sockaddr_in)) < 0) {
1617 ✗ LogDebug(VB_SYNC, "Could not send Twinkly discovery on %s: %s\n",
1618 a.second.interfaceName.c_str(), FPPstrerror(errno));
1619 }
1620 }
1621 1885 }
1622
1623 // A reply is the device's own address (4 bytes, least significant first), then
1624 // "OK", then its name. The source address of the datagram is what gets probed
1625 // rather than that embedded copy: it is the address that actually answered, and
1626 // so the one we have to talk to. The payload is only inspected far enough to
1627 // be reasonably sure this is a light rather than something else listening on
1628 // 5555 -- the HTTP detector is what confirms it.
1629 //
1630 // Always returns false: a true return from an EPollManager callback is fppd's
1631 // signal that bridge data is ready to push out, which this is not.
1632 35 bool MultiSync::ProcessTwinklyDiscoveryReplies() {
1633 char buf[256];
1634 // a bounded number of replies a wakeup, like the other sockets the main
1635 // loop reads; the socket is level-triggered, so the rest come next pass
1636
1/2
✓ Branch 0 taken 379 times.
✗ Branch 1 not taken.
379 for (int n = 0; n < 1024; n++) {
1637 struct sockaddr_in src;
1638 379 memset(&src, 0, sizeof(src));
1639 379 socklen_t slen = sizeof(src);
1640
1/2
✓ Branch 0 taken 379 times.
✗ Branch 1 not taken.
379 ssize_t len = recvfrom(m_twinklyDiscoverySock, buf, sizeof(buf), MSG_DONTWAIT,
1641 (struct sockaddr*)&src, &slen);
1642
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 344 times.
379 if (len <= 0) {
1643 35 return false;
1644 }
1645
8/12
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 344 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 344 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 20 times.
✓ Branch 9 taken 324 times.
✓ Branch 10 taken 20 times.
✓ Branch 11 taken 324 times.
344 if (len < 7 || buf[4] != 'O' || buf[5] != 'K' || GetMonotonicTimeMS() > s_twinklyRepliesUntilMS) {
1646 192 continue;
1647 }
1648 324 char ipStr[INET_ADDRSTRLEN] = { 0 };
1649
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 324 times.
324 if (!inet_ntop(AF_INET, &src.sin_addr, ipStr, sizeof(ipStr))) {
1650 ✗ continue;
1651 }
1652
1/2
✓ Branch 0 taken 324 times.
✗ Branch 1 not taken.
324 std::string address(ipStr);
1653 {
1654
1/2
✓ Branch 0 taken 324 times.
✗ Branch 1 not taken.
324 std::unique_lock<std::mutex> lock(m_httpProbeLock);
1655
2/2
✓ Branch 0 taken 172 times.
✓ Branch 1 taken 152 times.
324 if (m_twinklyDiscoverySeen.size() >= TWINKLY_MAX_REPLIES) {
1656 172 continue;
1657 }
1658
2/4
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 152 times.
152 if (!m_twinklyDiscoverySeen.insert(address).second) {
1659 // the lights answer each broadcast more than once
1660 ✗ continue;
1661 }
1662
2/2
✓ Branch 0 taken 152 times.
✓ Branch 1 taken 172 times.
324 }
1663
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 LogInfo(VB_SYNC, "Twinkly device answered discovery at %s\n", address.c_str());
1664 // Through the queue, under the in-flight cap. Started directly, one
1665 // per answering address, a burst of replies -- or of datagrams
1666 // claiming to be replies, from as many addresses -- started that many
1667 // HTTP probes at once.
1668 {
1669
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 std::unique_lock<std::mutex> lock(m_httpProbeLock);
1670
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 QueueHTTPPing(m_httpPingQueue, address);
1671 152 }
1672
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 PumpHTTPPings();
1673
2/2
✓ Branch 0 taken 152 times.
✓ Branch 1 taken 172 times.
324 }
1674 ✗ return false;
1675 }
1676
1677 115 void MultiSync::DiscoverIPViaHTTP(const std::string& ip, const std::string& html, bool allowUnknown) {
1678
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 LogDebug(VB_SYNC, "Checking HTTP response from %s\n", ip.c_str());
1679
1680
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 if (html.size()) {
1681
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 std::string d = html;
1682
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 114 times.
115 if (d.size() > 500) {
1683
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 d = d.substr(0, 500);
1684 }
1685
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 LogExcess(VB_SYNC, "IP: %s Resp: %s\n", ip.c_str(), d.c_str());
1686 115 }
1687
1688
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 std::string address2 = ip;
1689
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 GetIPForHost(address2);
1690
1691
5/10
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 115 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 115 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 115 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 115 times.
115 if (!isSupportedForMultisync(ip.c_str(), "") || !isSupportedForMultisync(address2.c_str(), "")) {
1692 ✗ if (allowUnknown) {
1693 ✗ UpdateSystem(kSysTypeUnknown, 0, 0, UNKNOWN_MODE, ip, ip, "Unknown", "Unknown", "0-0", "Unknown", false, false);
1694 }
1695 ✗ FinishHTTPDiscovery();
1696 ✗ return;
1697 }
1698
1699
3/8
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 115 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 115 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
115 NetworkController::DetectControllerViaHTML(ip, html, [this, ip, allowUnknown](NetworkController* nc) {
1700
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 68 times.
108 if (nc) {
1701 40 UpdateSystem(nc->typeId, nc->majorVersion, nc->minorVersion,
1702 40 nc->systemMode, nc->ip, nc->hostname, nc->version,
1703 40 nc->typeStr, nc->ranges, nc->uuid, false, nc->sendingMultiSync);
1704 40 ApplyPeerUUIDs(nc->peerUUIDs);
1705
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 delete nc;
1706
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 50 times.
68 } else if (allowUnknown) {
1707
5/10
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 18 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 18 times.
✗ Branch 9 not taken.
162 UpdateSystem(kSysTypeUnknown, 0, 0, UNKNOWN_MODE, ip, ip, "Unknown", "Unknown", "0-0", "Unknown", false, false);
1708 }
1709 108 FinishHTTPDiscovery();
1710 108 });
1711
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 }
1712
1713 // How many addresses in the configured HTTP scan are probed at once. The
1714 // version this replaced put every address of every configured subnet on one
1715 // curl multi handle in a single burst and blocked until the last one answered;
1716 // a /24 is 254 sockets, and several subnets are common. That is now spread out,
1717 // because it shares the multi handle with the rest of fppd rather than owning a
1718 // private one -- but it can still be wide, since almost every address in a scan
1719 // has nothing on it and simply burns the connect timeout.
1720 #define HTTP_DISCOVERY_MAX_IN_FLIGHT 48
1721
1722 // The discovery probe cannot be a plain addGet(): it has to keep the options
1723 // the hand-rolled multi handle set. HTTP09_ALLOWED is the load-bearing one --
1724 // some of the older controllers this is trying to identify answer with a bare
1725 // HTTP/0.9 body, which curl rejects by default. The timeouts are far tighter
1726 // than CurlManager's defaults for the reason above: most addresses are dead.
1727 354 static void discoveryGet(const std::string& url, std::function<void(int rc, const std::string& resp)>&& callback) {
1728 354 CURL* curl = CurlManager::INSTANCE.createCurl(url);
1729 // a device merely on the LAN can stream an endless body; detection only
1730 // ever looks at the start of a page, so a cut-off one is still fine
1731 354 CurlManager::limitResponseSize(curl);
1732 354 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 1000L);
1733 354 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5000L);
1734 354 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
1735 354 curl_easy_setopt(curl, CURLOPT_TCP_FASTOPEN, 1L);
1736 354 curl_easy_setopt(curl, CURLOPT_HTTP09_ALLOWED, 1L);
1737
4/8
✓ Branch 0 taken 354 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 354 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 354 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 354 times.
✗ Branch 7 not taken.
354 CurlManager::INSTANCE.addCURL(url, curl, [callback](CURL* c) {
1738 291 CurlManager::CurlPrivateData* data = nullptr;
1739 291 long rc = 0;
1740
1/2
✓ Branch 0 taken 291 times.
✗ Branch 1 not taken.
291 curl_easy_getinfo(c, CURLINFO_PRIVATE, &data);
1741
1/2
✓ Branch 0 taken 291 times.
✗ Branch 1 not taken.
291 curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &rc);
1742 291 std::string resp;
1743
5/6
✓ Branch 0 taken 291 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 115 times.
✓ Branch 3 taken 176 times.
✓ Branch 4 taken 115 times.
✓ Branch 5 taken 176 times.
291 if (data && !data->resp.empty()) {
1744
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 resp.assign(reinterpret_cast<char*>(data->resp.data()), data->resp.size());
1745 }
1746
1/2
✓ Branch 0 taken 291 times.
✗ Branch 1 not taken.
291 callback((int)rc, resp);
1747 291 });
1748 354 }
1749
1750 101 void MultiSync::DiscoverViaHTTP(const std::set<std::string>& ipSet, const std::set<std::string>& exacts) {
1751 {
1752
1/2
✓ Branch 0 taken 101 times.
✗ Branch 1 not taken.
101 std::unique_lock<std::mutex> lock(m_httpProbeLock);
1753
2/2
✓ Branch 0 taken 465 times.
✓ Branch 1 taken 101 times.
566 for (auto& ip : ipSet) {
1754
1/2
✓ Branch 0 taken 465 times.
✗ Branch 1 not taken.
465 LogExcess(VB_SYNC, " %s\n", ip.c_str());
1755
2/4
✓ Branch 0 taken 465 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 465 times.
✗ Branch 3 not taken.
465 m_httpDiscoveryQueue.emplace_back(ip, exacts.find(ip) != exacts.end());
1756 }
1757 101 }
1758 101 PumpHTTPDiscovery();
1759 101 }
1760
1761 // Starts scan probes until the cap is reached or the queue runs dry. Called
1762 // from DiscoverViaHTTP() and again from each completion.
1763 739 void MultiSync::PumpHTTPDiscovery() {
1764 while (true) {
1765 739 std::string ip;
1766 739 bool exact = false;
1767 {
1768
1/2
✓ Branch 0 taken 739 times.
✗ Branch 1 not taken.
739 std::unique_lock<std::mutex> lock(m_httpProbeLock);
1769
6/6
✓ Branch 0 taken 454 times.
✓ Branch 1 taken 285 times.
✓ Branch 2 taken 100 times.
✓ Branch 3 taken 354 times.
✓ Branch 4 taken 385 times.
✓ Branch 5 taken 354 times.
739 if (m_httpDiscoveryQueue.empty() || m_httpDiscoveriesInFlight >= HTTP_DISCOVERY_MAX_IN_FLIGHT) {
1770 770 return;
1771 }
1772
1/2
✓ Branch 0 taken 354 times.
✗ Branch 1 not taken.
354 std::tie(ip, exact) = m_httpDiscoveryQueue.front();
1773 354 m_httpDiscoveryQueue.pop_front();
1774 354 ++m_httpDiscoveriesInFlight;
1775
2/2
✓ Branch 0 taken 354 times.
✓ Branch 1 taken 385 times.
739 }
1776
1777 // ip may be a hostname, so this must not be a fixed-size buffer
1778 // (a fixed buffer would silently truncate hostnames and they'd
1779 // never get discovered - see issue #2667)
1780
5/12
✓ Branch 0 taken 354 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 354 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 354 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 354 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 354 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
1062 discoveryGet(buildHttpURL(ip, "/"), [this, ip, exact](int rc, const std::string& resp) {
1781 // A 200 is the normal answer. rc == 0 with a body is the HTTP/0.9
1782 // case -- there is no status line to report -- and is equally good;
1783 // rc == 0 with nothing is a transfer that never connected. This is
1784 // the same accept/reject the CURLcode-based version made.
1785 //
1786 // An `exact` address is one that was configured rather than swept
1787 // for - a channel output's destination, or an entry the user typed -
1788 // so anything that answered at all is worth identifying. Twinkly
1789 // lights need that: they have no web UI and serve a 404 at "/", and
1790 // only a follow-up call to their own API says what they are. The
1791 // broad sweep stays 200-only, so a subnet full of unrelated devices
1792 // that 404 does not drag the whole detector chain through each one.
1793
11/12
✓ Branch 0 taken 185 times.
✓ Branch 1 taken 106 times.
✓ Branch 2 taken 176 times.
✓ Branch 3 taken 9 times.
✓ Branch 4 taken 176 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 89 times.
✓ Branch 7 taken 96 times.
✓ Branch 8 taken 9 times.
✓ Branch 9 taken 80 times.
✓ Branch 10 taken 115 times.
✓ Branch 11 taken 176 times.
291 if (rc == 200 || (rc == 0 && !resp.empty()) || (exact && !resp.empty())) {
1794 115 LogDebug(VB_SYNC, "IP %s completed with code %d\n", ip.c_str(), rc);
1795 115 DiscoverIPViaHTTP(ip, resp, exact);
1796 115 return; // DiscoverIPViaHTTP() releases the slot
1797 }
1798 176 LogDebug(VB_SYNC, "No/Error response from %s. Response code: %d\n", ip.c_str(), rc);
1799 176 FinishHTTPDiscovery();
1800 });
1801
2/2
✓ Branch 0 taken 354 times.
✓ Branch 1 taken 385 times.
1093 }
1802 }
1803
1804 284 void MultiSync::FinishHTTPDiscovery() {
1805 {
1806
1/2
✓ Branch 0 taken 284 times.
✗ Branch 1 not taken.
284 std::unique_lock<std::mutex> lock(m_httpProbeLock);
1807 284 --m_httpDiscoveriesInFlight;
1808 284 }
1809 284 PumpHTTPDiscovery();
1810 284 }
1811
1812 4034 void MultiSync::WriteRuntimeInfoFile() {
1813
1/2
✓ Branch 0 taken 4034 times.
✗ Branch 1 not taken.
4034 Json::Value v = GetSystems(true, false);
1814
2/4
✓ Branch 0 taken 4034 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4034 times.
✗ Branch 3 not taken.
4034 std::string filename = FPP_DIR_MEDIA("/fpp-info.json");
1815
2/4
✓ Branch 0 taken 4034 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4034 times.
✗ Branch 3 not taken.
4034 Json::Value systems = v["systems"];
1816
1817 // If there is no network, then delete the file
1818 // to avoid confusion.
1819
3/4
✓ Branch 0 taken 4034 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 363 times.
✓ Branch 3 taken 3671 times.
4034 if (systems.empty()) {
1820 // remove() returns 0 on success; a file that was never written is
1821 // not a failure.
1822
3/6
✓ Branch 0 taken 363 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 363 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 363 times.
363 if (remove(filename.c_str()) != 0 && errno != ENOENT) {
1823 ✗ LogWarn(VB_ALL, "Failed to remove file %s: %s\n", filename.c_str(), FPPstrerror(errno));
1824 }
1825
1/2
✓ Branch 0 taken 363 times.
✗ Branch 1 not taken.
363 LogInfo(VB_ALL, "Not creating %s as no IP addresses found\n", filename.c_str());
1826 363 return;
1827 }
1828
1829
1/2
✓ Branch 0 taken 3671 times.
✗ Branch 1 not taken.
3671 std::string addresses = "";
1830
3/4
✓ Branch 0 taken 7509 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3838 times.
✓ Branch 3 taken 3671 times.
7509 for (int x = 0; x < systems.size(); x++) {
1831
3/4
✓ Branch 0 taken 3838 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 167 times.
✓ Branch 3 taken 3671 times.
3838 if (addresses != "") {
1832
1/2
✓ Branch 0 taken 167 times.
✗ Branch 1 not taken.
167 addresses += ",";
1833 }
1834
4/8
✓ Branch 0 taken 3838 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3838 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3838 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3838 times.
✗ Branch 7 not taken.
3838 addresses += systems[x]["address"].asString();
1835 }
1836
2/4
✓ Branch 0 taken 3671 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3671 times.
✗ Branch 3 not taken.
3671 Json::Value local = systems[0];
1837
1/2
✓ Branch 0 taken 3671 times.
✗ Branch 1 not taken.
3671 local.removeMember("address");
1838
2/4
✓ Branch 0 taken 3671 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3671 times.
✗ Branch 3 not taken.
3671 local["addresses"] = addresses;
1839
1840
2/4
✓ Branch 0 taken 3671 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3671 times.
✗ Branch 3 not taken.
3671 SaveJsonToFile(local, filename);
1841
6/6
✓ Branch 0 taken 3671 times.
✓ Branch 1 taken 363 times.
✓ Branch 2 taken 3671 times.
✓ Branch 3 taken 363 times.
✓ Branch 4 taken 3671 times.
✓ Branch 5 taken 363 times.
4760 }
1842
1843 /*
1844 *
1845 */
1846 4264 void MultiSync::Ping(int discover, bool broadcast) {
1847
1/2
✓ Branch 0 taken 4264 times.
✗ Branch 1 not taken.
4264 LogDebug(VB_SYNC, "MultiSync::Ping(%d, %d)\n", discover, broadcast);
1848 4264 time_t t = time(NULL);
1849 4264 m_lastPingTime = (unsigned long)t;
1850
1851
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4264 times.
4264 if (m_broadcastSock < 0) {
1852 ✗ LogErr(VB_SYNC, "ERROR: Tried to send ping packet but control socket is not open.\n");
1853 ✗ return;
1854 }
1855
1/2
✓ Branch 0 taken 4264 times.
✗ Branch 1 not taken.
4264 setupMulticastReceive(discover ? true : false);
1856
1857 // update the range for local systems so it's accurate
1858
1/2
✓ Branch 0 taken 4264 times.
✗ Branch 1 not taken.
4264 auto ranges = GetOutputRangesSnapshot(true);
1859
2/4
✓ Branch 0 taken 4264 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4264 times.
✗ Branch 3 not taken.
4264 std::string range = createRanges(*ranges, 120);
1860
1861 // Build every packet under m_systemsLock and send once it is released.
1862 // Dropping the lock mid-loop instead would leave the loop iterating
1863 // m_localSystems while a network change (FillLocalSystemInfo) rewrites it.
1864 4264 std::vector<std::vector<char>> packets;
1865 {
1866
1/2
✓ Branch 0 taken 4264 times.
✗ Branch 1 not taken.
4264 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
1867
2/2
✓ Branch 0 taken 4256 times.
✓ Branch 1 taken 4264 times.
8520 for (auto& sys : m_localSystems) {
1868
1/2
✓ Branch 0 taken 4256 times.
✗ Branch 1 not taken.
4256 std::vector<char> outBuf(768, 0);
1869
1/2
✓ Branch 0 taken 4256 times.
✗ Branch 1 not taken.
4256 sys.ranges = range;
1870
2/4
✓ Branch 0 taken 4256 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4256 times.
✗ Branch 3 not taken.
4256 outBuf.resize(CreatePingPacket(sys, outBuf.data(), discover));
1871
1/2
✓ Branch 0 taken 4256 times.
✗ Branch 1 not taken.
4256 packets.push_back(std::move(outBuf));
1872 4256 }
1873 4264 }
1874
2/2
✓ Branch 0 taken 4256 times.
✓ Branch 1 taken 4264 times.
8520 for (auto& outBuf : packets) {
1875
2/2
✓ Branch 0 taken 3867 times.
✓ Branch 1 taken 389 times.
4256 if (broadcast) {
1876
1/2
✓ Branch 0 taken 3867 times.
✗ Branch 1 not taken.
3867 SendBroadcastPacket(outBuf.data(), outBuf.size());
1877 } else {
1878
1/2
✓ Branch 0 taken 389 times.
✗ Branch 1 not taken.
389 SendMulticastPacket(outBuf.data(), outBuf.size());
1879 }
1880 }
1881
1882
2/2
✓ Branch 0 taken 1885 times.
✓ Branch 1 taken 2379 times.
4264 if (discover) {
1883 1885 std::set<std::string> remotes;
1884
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 AddRemotesFromSetting("MultiSyncExtraRemotes", remotes);
1885
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1885 times.
1897 for (auto& remote : remotes) {
1886
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 PingSingleRemote(remote.c_str(), 1);
1887 }
1888 1885 }
1889 4264 }
1890 25736 void MultiSync::PeriodicPing() {
1891 25736 time_t t = time(NULL);
1892
2/2
✓ Branch 0 taken 1194 times.
✓ Branch 1 taken 24542 times.
25736 if (m_lastCheckTime == 0) {
1893 1194 m_lastCheckTime = (unsigned long)t;
1894 }
1895 25736 unsigned long lpt = m_lastPingTime + 60 * 60;
1896
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 25724 times.
25736 if (lpt < (unsigned long)t) {
1897 // once an hour, we'll send a ping letting everyone know we're still here
1898 // mark ourselves as seen
1899
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
1900
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 12 times.
25 for (auto& sys : m_localSystems) {
1901 13 sys.lastSeen = (unsigned long)t;
1902 }
1903
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 lock.unlock();
1904
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 Ping();
1905 12 }
1906 // every minute we'll loop through real quick and check for remote instances
1907 // we haven't heard from in a while
1908 25736 lpt = m_lastCheckTime + 60;
1909 25736 bool superLongGap = false;
1910
2/2
✓ Branch 0 taken 51 times.
✓ Branch 1 taken 25685 times.
25736 if (lpt < (unsigned long)t) {
1911 51 m_lastCheckTime = (unsigned long)t;
1912 // anything we haven't heard from in 80 minutes we will re-ping to force
1913 51 unsigned long timeoutRePing = (unsigned long)t - 60 * 80;
1914 // anything we haven't heard from in 2 hours we will remove. That would
1915 // have caused at least 4 pings to have been sent. If it has responded
1916 // to any of those 4, it's got to be down/gone. Remove it.
1917 51 unsigned long timeoutRemove = (unsigned long)t - 60 * 120;
1918 // if we hadn't heard from them in 10 hours, it's likely a clock change
1919 // event, we'll resend a ping out to all to hopefully get everything
1920 // updated and new timestamps
1921 51 unsigned long timeoutRePingAll = (unsigned long)t - 60 * 600;
1922
1/2
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
51 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
1923 51 bool unicastChanged = false;
1924 // Collect the addresses (by value) and queue the HTTP probes after the
1925 // loop rather than from inside it. The probes are asynchronous now, but
1926 // the reason for the two-step is unchanged: a probe that resolves fast
1927 // reaches UpdateSystem, which can push_back onto m_remoteSystems (the
1928 // recursive mutex does not protect against our own thread) and
1929 // reallocate the vector out from under `it`.
1930 51 std::vector<std::string> httpPingAddresses;
1931
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 51 times.
59 for (auto it = m_remoteSystems.begin(); it != m_remoteSystems.end();) {
1932
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (it->lastSeen < timeoutRemove) {
1933 ✗ LogInfo(VB_SYNC, "Have not seen %s in over 2 hours, removing\n", it->address.c_str());
1934 ✗ if (it->lastSeen < timeoutRePingAll) {
1935 ✗ superLongGap = true;
1936 }
1937 ✗ unicastChanged |= it->supportsUnicast;
1938 ✗ it = m_remoteSystems.erase(it);
1939
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8 } else if (it->lastSeen < timeoutRePing) {
1940
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (it->multiSync) {
1941 ✗ PingSingleRemote(*it, 1);
1942 } else {
1943
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 httpPingAddresses.push_back(it->address);
1944 }
1945 6 ++it;
1946 } else {
1947 2 ++it;
1948 }
1949 }
1950 // Drop any removed remotes from the cached unicast destination list.
1951 // Snapshot the surviving targets under m_systemsLock, rebuild after
1952 // releasing it -- m_unicastUpdateLock is never acquired while
1953 // m_systemsLock is held.
1954
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
51 bool rebuildUnicast = m_sendUnicast && unicastChanged;
1955 51 std::vector<std::string> unicastAddrs;
1956
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (rebuildUnicast) {
1957 ✗ for (auto& sys : m_remoteSystems) {
1958 ✗ if (sys.supportsUnicast) {
1959 ✗ unicastAddrs.push_back(sys.address);
1960 }
1961 }
1962 }
1963
1/2
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
51 lock.unlock();
1964
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (rebuildUnicast) {
1965 ✗ UpdateUnicastDestinations(unicastAddrs);
1966 }
1967
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 50 times.
51 if (!httpPingAddresses.empty()) {
1968 // HTTP probes of remotes we haven't heard from in a while. These
1969 // used to be blocking curl calls run on a detached thread, because
1970 // doing them inline on the fppd main loop -- the same thread that
1971 // drains the MultiSync control socket -- stalled sync-packet
1972 // processing for the length of the timeouts, which on a remote
1973 // showed up as the output freezing for a second or two and then
1974 // jumping to catch up. They now go through CurlManager, so there is
1975 // nothing to stall and no thread to keep off the loop.
1976 {
1977
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::mutex> plock(m_httpProbeLock);
1978
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
7 for (auto& address : httpPingAddresses) {
1979
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 QueueHTTPPing(m_httpPingQueue, address);
1980 }
1981 1 }
1982
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 PumpHTTPPings();
1983 }
1984 51 }
1985
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25736 times.
25736 if (superLongGap) {
1986 ✗ Ping(1);
1987 }
1988
1989 25736 CheckSystemInfoRefreshes();
1990 25736 }
1991
1992 // How often a remote's HTTP-fetched detail is refreshed, how soon a failed
1993 // attempt is retried, and how many fetches may be outstanding at once. The
1994 // cap matters on a large show network: without it the first sweep after
1995 // discovery would open a connection to every remote at the same moment, from a
1996 // player that may be a single-core BeagleBone. The rest are simply picked up
1997 // by a later sweep.
1998 #define INFO_REFRESH_INTERVAL (30 * 60)
1999 #define INFO_RETRY_INTERVAL (5 * 60)
2000 #define INFO_SCAN_INTERVAL 5
2001 #define INFO_MAX_IN_FLIGHT 4
2002
2003 // Every fetch is delayed by a random slice of a window that grows with the size
2004 // of the known fleet. INFO_MAX_IN_FLIGHT bounds what one player sends, but the
2005 // dangerous direction is the other one: every peer sees the same discover ping,
2006 // so on a 100-instance show a single fppd restart would otherwise land ~200 HTTP
2007 // requests on the box that just came up -- the box least able to serve them, and
2008 // on a BeagleBone that is a real stall. A whole-show power-on is the same
2009 // pattern squared, with every box both hammering and being hammered. Budgeting
2010 // roughly one second of window per known system holds the aggregate arrival rate
2011 // at the target near two requests a second no matter how large the show is.
2012 #define INFO_JITTER_BASE 5
2013 #define INFO_JITTER_MAX 300
2014
2015 // The multisync page needs a fair amount of slow-changing detail about every
2016 // remote -- OS version, host description, background color, git branch/commit,
2017 // whether channel inputs/outputs are enabled, what cape is installed. It used
2018 // to fetch all of it from the browser, at least one HTTP request per remote per
2019 // page load, which is why the table visibly reflowed as the answers trickled
2020 // in. fppd already knows every remote and can collect it once per remote per
2021 // INFO_REFRESH_INTERVAL over the async CurlManager, so the UI gets it in the
2022 // very first GetSystems() response.
2023 //
2024 // Runs on the fppd main loop (from PeriodicPing). It must stay non-blocking:
2025 // the same loop feeds the sync output, which is why every fetch goes through
2026 // CurlManager.
2027 25736 void MultiSync::CheckSystemInfoRefreshes() {
2028 25736 time_t now = time(nullptr);
2029
2/2
✓ Branch 0 taken 23123 times.
✓ Branch 1 taken 2613 times.
25736 if (now < m_nextInfoScan) {
2030 23123 return;
2031 }
2032 2613 m_nextInfoScan = now + INFO_SCAN_INTERVAL;
2033
2034 2613 std::vector<std::string> toFetch;
2035 {
2036
1/2
✓ Branch 0 taken 2613 times.
✗ Branch 1 not taken.
2613 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
2037 // We routinely discover ourselves as a "remote" -- over loopback, and
2038 // over any address that isn't one of the interface addresses in
2039 // m_localSystems (a global IPv6 address, typically). Those entries
2040 // merge into the local row in the UI, which already has everything
2041 // about the box serving the page, so probing them is pure waste.
2042 2613 std::set<std::string> localUUIDs;
2043
2/2
✓ Branch 0 taken 2377 times.
✓ Branch 1 taken 2613 times.
4990 for (auto& sys : m_localSystems) {
2044
4/8
✓ Branch 0 taken 2377 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2377 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2377 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 2377 times.
2377 if (!sys.uuid.empty() && sys.uuid != "Unknown") {
2045 ✗ localUUIDs.insert(sys.uuid);
2046 }
2047 }
2048
2/2
✓ Branch 0 taken 1307 times.
✓ Branch 1 taken 2613 times.
3920 for (size_t idx = 0; idx < m_remoteSystems.size(); ++idx) {
2049 1307 MultiSyncSystem& sys = m_remoteSystems[idx];
2050 // Only full FPP instances: these are FPP's own API endpoints, and
2051 // anything at or above kSysTypeFalconController is a third-party
2052 // controller that would just 404 (or worse, be confused by) them.
2053
4/4
✓ Branch 0 taken 950 times.
✓ Branch 1 taken 357 times.
✓ Branch 2 taken 55 times.
✓ Branch 3 taken 895 times.
1307 if (sys.type == kSysTypeUnknown || sys.type >= kSysTypeFalconController) {
2054 1304 continue;
2055 }
2056
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 895 times.
895 if (sys.infoFetchPending) {
2057 ✗ continue;
2058 }
2059
2/4
✓ Branch 0 taken 895 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 895 times.
895 if (localUUIDs.find(sys.uuid) != localUUIDs.end()) {
2060 ✗ continue;
2061 }
2062 // Link-local addresses are reachable from here but not from a
2063 // browser (the IPv6 zone id is specific to this host), which is why
2064 // the multisync page drops those rows. Loopback never reaches this
2065 // list at all -- UpdateSystem() rejects it.
2066
15/30
✓ Branch 0 taken 895 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 895 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 894 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 894 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 894 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 894 times.
✓ Branch 12 taken 894 times.
✓ Branch 13 taken 1 times.
✓ Branch 14 taken 894 times.
✓ Branch 15 taken 1 times.
✓ Branch 16 taken 895 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 895 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 1 times.
✓ Branch 21 taken 894 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
4473 if (startsWith(sys.address, "169.254.") || startsWith(sys.address, "fe80:")) {
2067 1 continue;
2068 }
2069 // A device with more than one NIC has one entry per address. Act
2070 // on it once and stamp every sibling entry the same way, otherwise
2071 // the next sweep would pick the siblings up separately;
2072 // ForEachRemoteMatching() fans the single answer back out to all of
2073 // them.
2074
1/2
✓ Branch 0 taken 894 times.
✗ Branch 1 not taken.
894 std::string uuid = sys.uuid;
2075
2/4
✓ Branch 0 taken 894 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 894 times.
894 if (uuid == "Unknown") {
2076 ✗ uuid.clear();
2077 }
2078
1/2
✓ Branch 0 taken 894 times.
✗ Branch 1 not taken.
894 std::string address = sys.address;
2079 892 auto stampSiblings = [&](const std::function<void(MultiSyncSystem&)>& apply) {
2080
2/2
✓ Branch 0 taken 599862 times.
✓ Branch 1 taken 892 times.
600754 for (auto& other : m_remoteSystems) {
2081
8/8
✓ Branch 0 taken 598970 times.
✓ Branch 1 taken 892 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 598962 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 7 times.
✓ Branch 6 taken 893 times.
✓ Branch 7 taken 598969 times.
599862 if (other.address == address || (!uuid.empty() && other.uuid == uuid)) {
2082
1/2
✓ Branch 0 taken 893 times.
✗ Branch 1 not taken.
893 apply(other);
2083 }
2084 }
2085 892 };
2086
2087 // infoNextFetch == 0 means due but unscheduled: a remote we have
2088 // just discovered, or one InvalidateSystemInfo() marked stale after
2089 // its discover ping. A changed version string means the remote was
2090 // upgraded under us, which rewrites most of what we cached -- and on
2091 // a fleet upgrade that fires everywhere at once, so it earns the
2092 // same treatment. Either way, pick a jittered time and come back
2093 // for it on a later sweep instead of fetching right now.
2094
5/6
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 889 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 889 times.
✓ Branch 5 taken 5 times.
894 if (sys.infoNextFetch == 0 || sys.infoFetchedVersion != sys.version) {
2095 // Entry count, not device count: a multi-NIC box contributes
2096 // several, as do non-FPP controllers. That only ever widens
2097 // the window, which is the safe direction.
2098 889 int window = INFO_JITTER_BASE + (int)m_remoteSystems.size();
2099
2/2
✓ Branch 0 taken 768 times.
✓ Branch 1 taken 121 times.
889 if (window > INFO_JITTER_MAX) {
2100 768 window = INFO_JITTER_MAX;
2101 }
2102
1/2
✓ Branch 0 taken 889 times.
✗ Branch 1 not taken.
889 time_t when = now + (FPPrand() % window);
2103
1/2
✓ Branch 0 taken 889 times.
✗ Branch 1 not taken.
889 stampSiblings([&](MultiSyncSystem& other) {
2104 890 other.infoNextFetch = when;
2105 890 other.infoFetchedVersion = other.version;
2106 890 });
2107 889 continue;
2108 889 }
2109
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 if (now < sys.infoNextFetch) {
2110 2 continue;
2111 }
2112 // Cap only what is actually sent. Scheduling above is free and has
2113 // to keep running even while the cap is reached, or a large fleet
2114 // would never get its jittered times assigned. Anything held back
2115 // here is simply picked up by the next sweep.
2116
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if ((int)toFetch.size() + m_infoFetchesInFlight >= INFO_MAX_IN_FLIGHT) {
2117 ✗ continue;
2118 }
2119
2120
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 stampSiblings([&](MultiSyncSystem& other) {
2121 3 other.infoFetchPending = true;
2122 3 other.infoFetchedVersion = other.version;
2123 3 });
2124
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 toFetch.push_back(address);
2125
4/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 891 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 891 times.
1785 }
2126 2613 }
2127
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2613 times.
2616 for (auto& address : toFetch) {
2128
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 FetchSystemInfo(address);
2129 }
2130 2613 }
2131
2132 64 void MultiSync::InvalidateSystemInfo(const std::string& address) {
2133
1/2
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
64 ForEachRemoteMatching(address, [](MultiSyncSystem& sys) {
2134 // Due, but unscheduled: the next sweep gives it a jittered time. Never
2135 // fetch straight from here -- this runs for every peer that heard the
2136 // same discover ping, which is exactly the pile-on being avoided.
2137 64 sys.infoNextFetch = 0;
2138 64 });
2139 64 }
2140
2141 // One device, one identity.
2142 //
2143 // A box with several addresses gets an entry per address, and a real UUID only
2144 // ever reaches the entry that was actually probed for it -- UpdateSystem()
2145 // matches on the address. The others keep whatever they were given, which
2146 // since MAC-derived identities exist means a stand-in rather than nothing. Two
2147 // different identities for one device is worse than none: the UI keys a row on
2148 // the UUID and draws the device twice, and the statistics count it twice, which
2149 // defeats the deduplication the identity exists to provide.
2150 //
2151 // So once any entry knows a real UUID, every other entry for that device adopts
2152 // it. Guarded the way the UI guards its own hostname fallback: a hostname
2153 // claimed by more than one real UUID is not identifying a device at all -- two
2154 // controllers left on the default name -- so in that case nothing is touched.
2155 //
2156 // This is only about filling in for entries that have no identity of their own.
2157 // A real UUID is never overwritten by another.
2158 // Records identities one device reported for OTHER addresses. Some vendors
2159 // describe their neighbours but not themselves, so this is the only place those
2160 // devices' identities ever come from.
2161 //
2162 // The hints are kept, not applied once and dropped. Discovery reaches
2163 // addresses in whatever order it reaches them, so the neighbour describing a
2164 // device is very often probed BEFORE that device is found -- applying only to
2165 // what exists right now would silently lose exactly the identities that arrive
2166 // early. UpdateSystem() consults the map for every system it touches, so the
2167 // two orders behave the same.
2168 //
2169 // A hint never creates a system: a name in somebody's peer list is not evidence
2170 // that a device is on this network, and a system discovery has not found has no
2171 // business appearing in the list.
2172 44 void MultiSync::ApplyPeerUUIDs(const std::map<std::string, std::string>& peerUUIDs) {
2173
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 1 times.
44 if (peerUUIDs.empty()) {
2174 43 return;
2175 }
2176
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
2177
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 for (const auto& [addr, uuid] : peerUUIDs) {
2178
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 if (!addr.empty() && !uuid.empty()) {
2179 // One device's reply decides how many entries it lists, so a
2180 // device (or anything answering as one) could grow this for good.
2181 // Far more than any network's worth of devices.
2182
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
3 if (m_peerUUIDHints.size() >= 4096 && !m_peerUUIDHints.contains(addr)) {
2183 ✗ continue;
2184 }
2185
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 m_peerUUIDHints[addr] = uuid;
2186 }
2187 }
2188
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 for (auto& sys : m_remoteSystems) {
2189
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 ApplyUUIDHint(sys);
2190 }
2191 1 }
2192
2193 // Precedence matches MultiSyncSystem::update(): a device's own UUID always
2194 // wins, and a MAC-derived stand-in may only replace another stand-in.
2195 //
2196 // That a hint OVERRIDES an ARP-derived stand-in is deliberate, and it matters:
2197 // the two are not the same value. An ESP32 running on Ethernet uses base+3 as
2198 // its interface address, so ARP sees a MAC three higher than the one the device
2199 // reports for itself (measured on WLED and on two Baldricks; Genius, which
2200 // reports its interface address rather than the base, matches ARP exactly).
2201 // Both are stable identities for the device -- but they are DIFFERENT stable
2202 // identities, and letting a device carry whichever one its observer happened to
2203 // obtain would split it in two, which is the failure this whole path exists to
2204 // remove. So the vendor-reported value wins everywhere it is available, and
2205 // ARP stays the fallback for devices no detector could identify at all.
2206 4858 void MultiSync::ApplyUUIDHint(MultiSyncSystem& sys) {
2207
12/18
✓ Branch 0 taken 516 times.
✓ Branch 1 taken 4342 times.
✓ Branch 2 taken 516 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 516 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 308 times.
✓ Branch 7 taken 208 times.
✓ Branch 8 taken 516 times.
✓ Branch 9 taken 4342 times.
✓ Branch 10 taken 516 times.
✓ Branch 11 taken 4342 times.
✓ Branch 12 taken 308 times.
✓ Branch 13 taken 4550 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
5890 if (!sys.uuid.empty() && !startsWith(sys.uuid, MAC_UUID_PREFIX)) {
2208 308 return;
2209 }
2210
1/2
✓ Branch 0 taken 4550 times.
✗ Branch 1 not taken.
4550 auto it = m_peerUUIDHints.find(sys.address);
2211
5/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4549 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 4549 times.
4550 if (it != m_peerUUIDHints.end() && !it->second.empty()) {
2212
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 sys.uuid = it->second;
2213 }
2214 }
2215
2216 9089 void MultiSync::ReconcileDeviceIdentity(const std::string& hostname, FPPMode fppMode) {
2217
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9089 times.
9089 if (hostname.empty()) {
2218 8782 return;
2219 }
2220 5165 auto isStandIn = [](const std::string& u) {
2221
10/16
✓ Branch 0 taken 825 times.
✓ Branch 1 taken 4340 times.
✓ Branch 2 taken 825 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 825 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 207 times.
✓ Branch 7 taken 618 times.
✓ Branch 8 taken 825 times.
✓ Branch 9 taken 4340 times.
✓ Branch 10 taken 825 times.
✓ Branch 11 taken 4340 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
6815 return u.empty() || startsWith(u, MAC_UUID_PREFIX);
2222 };
2223
2224 9089 std::string real;
2225
2/2
✓ Branch 0 taken 2716529 times.
✓ Branch 1 taken 9087 times.
2725616 for (auto& sys : m_remoteSystems) {
2226
9/10
✓ Branch 0 taken 4876 times.
✓ Branch 1 taken 2711653 times.
✓ Branch 2 taken 4857 times.
✓ Branch 3 taken 19 times.
✓ Branch 4 taken 4857 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4546 times.
✓ Branch 7 taken 311 times.
✓ Branch 8 taken 2716218 times.
✓ Branch 9 taken 311 times.
2716529 if (sys.hostname != hostname || sys.fppMode != fppMode || isStandIn(sys.uuid)) {
2227 2716218 continue;
2228 }
2229
2/2
✓ Branch 0 taken 309 times.
✓ Branch 1 taken 2 times.
311 if (real.empty()) {
2230
1/2
✓ Branch 0 taken 309 times.
✗ Branch 1 not taken.
309 real = sys.uuid;
2231
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 } else if (real != sys.uuid) {
2232 2 return; // hostname shared by more than one device
2233 }
2234 }
2235
2/2
✓ Branch 0 taken 8780 times.
✓ Branch 1 taken 307 times.
9087 if (real.empty()) {
2236 8780 return;
2237 }
2238
2/2
✓ Branch 0 taken 1194 times.
✓ Branch 1 taken 307 times.
1501 for (auto& sys : m_remoteSystems) {
2239
8/10
✓ Branch 0 taken 308 times.
✓ Branch 1 taken 886 times.
✓ Branch 2 taken 308 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 308 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 307 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 1193 times.
1194 if (sys.hostname == hostname && sys.fppMode == fppMode && isStandIn(sys.uuid)) {
2240
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 sys.uuid = real;
2241 }
2242 }
2243
2/2
✓ Branch 0 taken 307 times.
✓ Branch 1 taken 8782 times.
9089 }
2244
2245 67 void MultiSync::ForEachRemoteMatching(const std::string& address,
2246 const std::function<void(MultiSyncSystem&)>& apply) {
2247
1/2
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
67 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
2248 67 std::string uuid;
2249
1/2
✓ Branch 0 taken 73 times.
✗ Branch 1 not taken.
73 for (auto& sys : m_remoteSystems) {
2250
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 6 times.
73 if (sys.address == address) {
2251
1/2
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
67 uuid = sys.uuid;
2252 67 break;
2253 }
2254 }
2255
2/4
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 67 times.
67 if (uuid == "Unknown") {
2256 ✗ uuid.clear();
2257 }
2258
2/2
✓ Branch 0 taken 93 times.
✓ Branch 1 taken 67 times.
160 for (auto& sys : m_remoteSystems) {
2259
7/8
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 67 times.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 12 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 14 times.
✓ Branch 6 taken 67 times.
✓ Branch 7 taken 26 times.
93 if (sys.address == address || (!uuid.empty() && sys.uuid == uuid)) {
2260
1/2
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
67 apply(sys);
2261 }
2262 }
2263 67 }
2264
2265 3 void MultiSync::FetchSystemInfo(const std::string& address) {
2266
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string url = buildHttpURL(address, "/api/system/info");
2267 3 ++m_infoFetchesInFlight;
2268 // `address` is captured by value on purpose: m_remoteSystems is a vector of
2269 // values that UpdateSystem() push_back()s into, so any pointer or reference
2270 // to an entry can be dangling by the time this callback runs. Everything
2271 // below re-finds the entries by address instead.
2272
5/10
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
9 CurlManager::INSTANCE.addGet(url, [this, address](int rc, const std::string& resp) {
2273 2 --m_infoFetchesInFlight;
2274
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value v;
2275
6/8
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1 times.
2 if (rc != 200 || !LoadJsonFromString(resp, v, JsonRoot::Object)) {
2276
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_SYNC, "Could not fetch system info from %s (rc: %d)\n", address.c_str(), rc);
2277 1 time_t retryAt = time(nullptr) + INFO_RETRY_INTERVAL;
2278
1/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 ForEachRemoteMatching(address, [retryAt](MultiSyncSystem& sys) {
2279 1 sys.infoFetchPending = false;
2280 // A zero here means InvalidateSystemInfo() ran while this fetch
2281 // was in flight -- the remote restarted mid-request, so what we
2282 // just read may already be wrong. Leave it zero and let the
2283 // sweep schedule a fresh attempt.
2284
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (sys.infoNextFetch != 0) {
2285 1 sys.infoNextFetch = retryAt;
2286 }
2287 1 });
2288 1 return;
2289 }
2290
2291 1 MultiSyncSystemInfo info;
2292 1 info.valid = true;
2293
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.platform = ReplyString(v, "Platform");
2294
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.variant = ReplyString(v, "Variant");
2295
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.subPlatform = ReplyString(v, "SubPlatform");
2296
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.osVersion = ReplyString(v, "OSVersion");
2297
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.osRelease = ReplyString(v, "OSRelease");
2298
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.kernel = ReplyString(v, "Kernel");
2299
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.hostDescription = ReplyString(v, "HostDescription");
2300
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.backgroundColor = ReplyString(v, "backgroundColor");
2301
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.branch = ReplyString(v, "Branch");
2302
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.localGitVersion = ReplyString(v, "LocalGitVersion");
2303
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.remoteGitVersion = ReplyString(v, "RemoteGitVersion");
2304
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 info.upgradeSource = ReplyString(v, "UpgradeSource");
2305
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
2 if (JsonHas(v, "channelInputsEnabled")) {
2306 ✗ info.channelInputsEnabled = ReplyBool(v, "channelInputsEnabled", false) ? 1 : 0;
2307 }
2308
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.
2 if (JsonHas(v, "channelOutputsEnabled")) {
2309
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 info.channelOutputsEnabled = ReplyBool(v, "channelOutputsEnabled", false) ? 1 : 0;
2310 }
2311
9/22
✓ 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.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 1 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 1 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
3 if (JsonHas(v, "IPs") && v["IPs"].isArray()) {
2312
8/14
✓ 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.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2 times.
✓ Branch 13 taken 1 times.
3 for (const auto& ip : v["IPs"]) {
2313
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 if (ip.isString()) {
2314
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 info.ips.push_back(ip.asString());
2315 }
2316 }
2317 }
2318
2319
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 ForEachRemoteMatching(address, [&info](MultiSyncSystem& sys) {
2320 1 sys.systemInfo = info;
2321 1 });
2322
2323 // Chain the cape fetch rather than issuing both at once: it reuses the
2324 // connection just established and halves the peak request count.
2325 // infoFetchPending stays set until it finishes so the sweep above does
2326 // not re-queue this remote in between.
2327
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 FetchCapeInfo(address);
2328
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 });
2329 3 }
2330
2331 1 void MultiSync::FetchCapeInfo(const std::string& address) {
2332
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string url = buildHttpURL(address, "/api/cape");
2333 1 ++m_infoFetchesInFlight;
2334
5/10
✓ 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.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
3 CurlManager::INSTANCE.addGet(url, [this, address](int rc, const std::string& resp) {
2335 1 --m_infoFetchesInFlight;
2336 1 MultiSyncCapeInfo cape;
2337
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Json::Value v;
2338
4/8
✓ 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.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
1 if (rc == 200 && LoadJsonFromString(resp, v, JsonRoot::Object)) {
2339
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 cape = MultiSyncCapeInfo::fromCapeJSON(v);
2340 ✗ } else if (rc == 404) {
2341 // GetCapeInfo() answers 404 with {"id": "No Cape!"} when nothing is
2342 // installed. Cache that: it is an answer, not a failure. (An FPP
2343 // old enough to predate the route would 404 the same way and be
2344 // recorded as having no cape, which is what the UI shows for it
2345 // anyway.)
2346 ✗ cape.valid = true;
2347 ✗ cape.present = false;
2348 } else {
2349 // Anything else (connection failure, or an FPP old enough not to
2350 // have /api/cape at all) leaves capeInfo unset and untrusted.
2351 ✗ LogDebug(VB_SYNC, "Could not fetch cape info from %s (rc: %d)\n", address.c_str(), rc);
2352 }
2353
2354 // End of the chain, so this is where the next refresh is scheduled --
2355 // the system info the caller already stored is good either way, so a
2356 // cape that could not be read doesn't earn the shorter retry interval.
2357 1 time_t nextAt = time(nullptr) + INFO_REFRESH_INTERVAL;
2358
1/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 ForEachRemoteMatching(address, [&cape, nextAt](MultiSyncSystem& sys) {
2359
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (cape.valid) {
2360 1 sys.capeInfo = cape;
2361 }
2362 1 sys.infoFetchPending = false;
2363 // Zero means the remote sent a discover ping while this fetch was
2364 // in flight, i.e. it restarted mid-request and what we just stored
2365 // may already be stale. Don't paper over that with a 30 minute
2366 // interval; leave it due so the sweep reschedules.
2367
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (sys.infoNextFetch != 0) {
2368 1 sys.infoNextFetch = nextAt;
2369 }
2370 1 });
2371 1 });
2372 1 }
2373
2374 // How many HTTP probes of unresponsive remotes may be outstanding at once.
2375 // Small on purpose: these run against boxes that are quite possibly down, so
2376 // most of them sit there burning the connect timeout, and the player doing the
2377 // probing may be a single-core BeagleBone.
2378 #define HTTP_PING_MAX_IN_FLIGHT 4
2379
2380 // Starts queued probes until the cap is reached or the queue runs dry. Called
2381 // from PeriodicPing() on the main loop and again from each completion, which may
2382 // be on another thread -- see m_httpProbeLock.
2383 489 void MultiSync::PumpHTTPPings() {
2384 while (true) {
2385 489 std::string address;
2386 {
2387
1/2
✓ Branch 0 taken 489 times.
✗ Branch 1 not taken.
489 std::unique_lock<std::mutex> lock(m_httpProbeLock);
2388
6/6
✓ Branch 0 taken 445 times.
✓ Branch 1 taken 44 times.
✓ Branch 2 taken 288 times.
✓ Branch 3 taken 157 times.
✓ Branch 4 taken 332 times.
✓ Branch 5 taken 157 times.
489 if (m_httpPingQueue.empty() || m_httpPingsInFlight >= HTTP_PING_MAX_IN_FLIGHT) {
2389 664 return;
2390 }
2391
1/2
✓ Branch 0 taken 157 times.
✗ Branch 1 not taken.
157 address = m_httpPingQueue.front();
2392 157 m_httpPingQueue.pop_front();
2393 // Claimed here, under the same lock as the cap test, so a second
2394 // pumper cannot read a stale count and overshoot.
2395 157 ++m_httpPingsInFlight;
2396
2/2
✓ Branch 0 taken 157 times.
✓ Branch 1 taken 332 times.
489 }
2397
1/2
✓ Branch 0 taken 157 times.
✗ Branch 1 not taken.
157 StartHTTPPing(address);
2398
2/2
✓ Branch 0 taken 157 times.
✓ Branch 1 taken 332 times.
646 }
2399 }
2400
2401 // Probe one address now, outside the queue. MDNSManager calls this the moment
2402 // it resolves a host, and that probe must not be deferred behind a sweep -- but
2403 // it still has to be counted, or the cap above is measured against a number that
2404 // does not describe reality. (It is the entry point every caller outside this
2405 // file uses, which is why the accounting lives here rather than in the pump.)
2406 26 void MultiSync::PingSingleRemoteViaHTTP(const std::string& address) {
2407 {
2408
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 std::unique_lock<std::mutex> lock(m_httpProbeLock);
2409 26 ++m_httpPingsInFlight;
2410 26 }
2411 26 StartHTTPPing(address);
2412 26 }
2413
2414 // Asks whatever is at `address` for its front page and hands the answer to the
2415 // controller detectors. Both halves are asynchronous, so this returns as soon
2416 // as the request is queued. The caller must already have claimed an in-flight
2417 // slot; whichever path below finishes the probe releases it.
2418 183 void MultiSync::StartHTTPPing(const std::string& address) {
2419
7/14
✓ Branch 0 taken 183 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 183 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 183 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 183 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 183 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 183 times.
✗ Branch 13 not taken.
915 CurlManager::INSTANCE.addGet(buildHttpURL(address), [this, address](int rc, const std::string& resp) {
2420 // The address may be queued for a re-ping again from here on: the
2421 // answer (or the failure) is in.
2422 {
2423
1/2
✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
179 std::unique_lock<std::mutex> lock(m_httpProbeLock);
2424
1/2
✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
179 s_httpPingPending.erase(address);
2425 179 }
2426 // rc == 0 is a transfer that never got an HTTP response at all; any
2427 // status code means something answered, and the old blocking probe
2428 // likewise fed a non-200 body to the detectors rather than dropping it.
2429
5/6
✓ Branch 0 taken 152 times.
✓ Branch 1 taken 27 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 152 times.
✓ Branch 4 taken 27 times.
✓ Branch 5 taken 152 times.
179 if (rc == 0 || resp.empty()) {
2430 27 FinishHTTPPing();
2431 27 return;
2432 }
2433
3/6
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 152 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 152 times.
✗ Branch 5 not taken.
152 NetworkController::DetectControllerViaHTML(address, resp, [this, address](NetworkController* nc) {
2434
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 148 times.
152 if (nc) {
2435 4 UpdateSystem(nc->typeId, nc->majorVersion, nc->minorVersion,
2436 4 nc->systemMode, nc->ip, nc->hostname, nc->version,
2437 4 nc->typeStr, nc->ranges, nc->uuid, false, nc->sendingMultiSync);
2438 4 ApplyPeerUUIDs(nc->peerUUIDs);
2439
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 delete nc;
2440 } else {
2441
4/10
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 148 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 148 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 148 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
1480 UpdateSystem(kSysTypeUnknown, 0, 0, UNKNOWN_MODE, address,
2442
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 address, "Unknown", "Unknown", "0-0", "Unknown", false, false);
2443 }
2444 152 FinishHTTPPing();
2445 152 });
2446 });
2447 183 }
2448
2449 // Releases the in-flight slot and starts whatever is next. Kept separate
2450 // because the probe can end at either of two points above.
2451 179 void MultiSync::FinishHTTPPing() {
2452 {
2453
1/2
✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
179 std::unique_lock<std::mutex> lock(m_httpProbeLock);
2454 179 --m_httpPingsInFlight;
2455 179 }
2456 179 PumpHTTPPings();
2457 179 }
2458
2459 // Answering a discover ping: one broadcast ping plus a unicast one to the
2460 // asker. This used to sleep up to 6ms on the main loop per discover ping
2461 // (so the remotes' answers are spread out) and answer every one, so a stream
2462 // of them stalled output and turned every packet into several. The answer
2463 // now goes out from a one-shot timer a few ms later, and every discover ping
2464 // that arrives while one is pending joins it: one broadcast, plus a unicast to
2465 // each distinct asker (bounded).
2466 static std::mutex s_discoverReplyLock;
2467 static std::set<std::string> s_discoverReplyTargets;
2468 static bool s_discoverReplyArmed = false;
2469
2470 70 void MultiSync::QueueDiscoverReply(const std::string& address, const std::string& srcIp) {
2471
1/2
✓ Branch 0 taken 70 times.
✗ Branch 1 not taken.
70 std::lock_guard<std::mutex> lock(s_discoverReplyLock);
2472
2/2
✓ Branch 0 taken 140 times.
✓ Branch 1 taken 70 times.
210 for (const std::string* a : { &address, &srcIp }) {
2473
3/6
✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 140 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 140 times.
✗ Branch 5 not taken.
140 if (!a->empty() && s_discoverReplyTargets.size() < 64) {
2474
1/2
✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
140 s_discoverReplyTargets.insert(*a);
2475 }
2476 }
2477
2/2
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 15 times.
70 if (s_discoverReplyArmed) {
2478 55 return;
2479 }
2480 15 s_discoverReplyArmed = true;
2481 // a slight random delay so all the remotes don't answer at the same time
2482
4/8
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 15 times.
✗ Branch 7 not taken.
45 Timers::INSTANCE.addTimer("MultiSyncDiscoverReply", GetTimeMS() + 1 + FPPrand() % 6, [this]() {
2483 15 std::set<std::string> targets;
2484 {
2485
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 std::lock_guard<std::mutex> lock(s_discoverReplyLock);
2486 15 targets.swap(s_discoverReplyTargets);
2487 15 s_discoverReplyArmed = false;
2488 15 }
2489
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 Ping();
2490
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 15 times.
51 for (const auto& t : targets) {
2491
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 PingSingleRemote(t.c_str(), 0);
2492 }
2493 15 });
2494
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 55 times.
70 }
2495
2496 74 void MultiSync::PingSingleRemote(const char* address, int discover) {
2497 74 MultiSyncSystem sys;
2498
2499
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 sys.address = address;
2500
2501
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 PingSingleRemote(sys, discover);
2502 74 }
2503
2504 74 void MultiSync::PingSingleRemote(MultiSyncSystem& sys, int discover) {
2505 char outBuf[512];
2506 74 memset(outBuf, 0, sizeof(outBuf));
2507 int len;
2508 {
2509
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
2510
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 74 times.
74 if (m_localSystems.empty()) {
2511 ✗ return;
2512 }
2513
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 len = CreatePingPacket(m_localSystems[0], outBuf, discover);
2514
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 }
2515
1/2
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
74 SendUnicastPacket(sys.address, outBuf, len);
2516 }
2517 4330 int MultiSync::CreatePingPacket(MultiSyncSystem& sysInfo, char* outBuf, int discover) {
2518 4330 ControlPkt* cpkt = (ControlPkt*)outBuf;
2519 4330 InitControlPacket(cpkt);
2520
2521 4330 cpkt->pktType = CTRL_PKT_PING;
2522 4330 cpkt->extraDataLen = PING_V4_LEN;
2523
2524 4330 unsigned char* ed = (unsigned char*)(outBuf + 7);
2525 4330 memset(ed, 0, cpkt->extraDataLen);
2526
2527 4330 ed[0] = 4; // ping version 4
2528
2/2
✓ Branch 0 taken 1840 times.
✓ Branch 1 taken 2490 times.
4330 ed[1] = discover > 0 ? 1 : 0; // 0 = ping, 1 = discover
2529 4330 ed[2] = sysInfo.type;
2530 4330 ed[3] = (sysInfo.majorVersion & 0xFF00) >> 8;
2531 4330 ed[4] = (sysInfo.majorVersion & 0x00FF);
2532 4330 ed[5] = (sysInfo.minorVersion & 0xFF00) >> 8;
2533 4330 ed[6] = (sysInfo.minorVersion & 0x00FF);
2534 4330 ed[7] = sysInfo.fppMode;
2535
2/2
✓ Branch 0 taken 106 times.
✓ Branch 1 taken 4224 times.
4330 if (sysInfo.sendingMultiSync) {
2536 106 ed[7] |= 0x04;
2537 }
2538 4330 ed[8] = sysInfo.ipa;
2539 4330 ed[9] = sysInfo.ipb;
2540 4330 ed[10] = sysInfo.ipc;
2541 4330 ed[11] = sysInfo.ipd;
2542
2543 4330 strncpy((char*)(ed + 12), sysInfo.hostname.c_str(), 64);
2544 4330 strncpy((char*)(ed + 77), sysInfo.version.c_str(), 40);
2545 4330 strncpy((char*)(ed + 118), sysInfo.model.c_str(), 40);
2546 4330 strncpy((char*)(ed + 159), sysInfo.ranges.c_str(), 120);
2547 // v4: appended after the whole v3 payload, which receivers that predate
2548 // it never read past. "Unknown" is getSetting()'s answer for no UUID.
2549
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4330 times.
4330 if (sysInfo.uuid != "Unknown") {
2550 ✗ strncpy((char*)(ed + PING_V3_LEN), sysInfo.uuid.c_str(), PING_UUID_MAX);
2551 }
2552 4330 return sizeof(ControlPkt) + cpkt->extraDataLen;
2553 }
2554
2555 // The most a sender may put after the ControlPkt header. Receivers read each
2556 // datagram into a MAX_MS_RCV_BUFSIZE buffer and drop anything that arrives
2557 // truncated, so a larger packet is sent but never acted on. Every sender
2558 // checks against this one limit and refuses (with an error) rather than send
2559 // something no remote will accept.
2560 static constexpr size_t MAX_MS_PAYLOAD = MAX_MS_RCV_BUFSIZE - sizeof(ControlPkt);
2561
2562 // The sync senders below write filename into a SyncPkt within a 2048-byte
2563 // outBuf; a filename that wouldn't fit in a packet must be rejected before the
2564 // strcpy rather than silently truncated or overflowed. sizeof(SyncPkt)
2565 // already counts the filename's terminating NUL.
2566 7536 static bool FitsInSyncPacket(const std::string& filename) {
2567 7536 return (sizeof(SyncPkt) + filename.length()) <= MAX_MS_PAYLOAD;
2568 }
2569
2570 33 void MultiSync::SendSeqOpenPacket(const std::string& filename) {
2571
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (filename.empty()) {
2572 ✗ return;
2573 }
2574
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (!FitsInSyncPacket(filename)) {
2575 ✗ LogErr(VB_SYNC, "ERROR: SendSeqOpenPacket filename '%s' is too long to send\n", filename.c_str());
2576 ✗ return;
2577 }
2578
2579
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (m_controlSock < 0) {
2580 ✗ LogErr(VB_SYNC, "ERROR: Tried to send start packet but sync socket is not open.\n");
2581 ✗ return;
2582 }
2583
2584
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 LogDebug(VB_SYNC, "SendSeqOpenPacket('%s')\n", filename.c_str());
2585
2/4
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 33 times.
33 for (auto a : getPluginsCopy()) {
2586 ✗ a->SendSeqOpenPacket(filename);
2587 33 }
2588 33 __atomic_store_n(&m_lastFrame, -1, __ATOMIC_RELAXED);
2589 33 __atomic_store_n(&m_lastFrameSent, -1, __ATOMIC_RELAXED);
2590
2591 char outBuf[2048];
2592 33 bzero(outBuf, sizeof(outBuf));
2593
2594 33 ControlPkt* cpkt = (ControlPkt*)outBuf;
2595 33 SyncPkt* spkt = (SyncPkt*)(outBuf + sizeof(ControlPkt));
2596
2597
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 InitControlPacket(cpkt);
2598
2599 33 cpkt->pktType = CTRL_PKT_SYNC;
2600 33 cpkt->extraDataLen = sizeof(SyncPkt) + filename.length();
2601
2602 33 spkt->pktType = SYNC_PKT_OPEN;
2603 33 spkt->fileType = SYNC_FILE_SEQ;
2604 33 spkt->frameNumber = 0;
2605 33 spkt->secondsElapsed = 0;
2606 33 strcpy(spkt->filename, filename.c_str());
2607
2608
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 SendControlPacket(outBuf, sizeof(ControlPkt) + sizeof(SyncPkt) + filename.length());
2609 }
2610
2611 32 void MultiSync::SendSeqSyncStartPacket(const std::string& filename) {
2612
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (filename.empty()) {
2613 ✗ return;
2614 }
2615
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (!FitsInSyncPacket(filename)) {
2616 ✗ LogErr(VB_SYNC, "ERROR: SendSeqSyncStartPacket filename '%s' is too long to send\n", filename.c_str());
2617 ✗ return;
2618 }
2619
2620
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (m_controlSock < 0) {
2621 ✗ LogErr(VB_SYNC, "ERROR: Tried to send start packet but sync socket is not open.\n");
2622 ✗ return;
2623 }
2624
2625
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 LogDebug(VB_SYNC, "SendSeqSyncStartPacket('%s')\n", filename.c_str());
2626
2/4
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 32 times.
32 for (auto a : getPluginsCopy()) {
2627 ✗ a->SendSeqSyncStartPacket(filename);
2628 32 }
2629 32 __atomic_store_n(&m_lastFrame, -1, __ATOMIC_RELAXED);
2630 32 __atomic_store_n(&m_lastFrameSent, -1, __ATOMIC_RELAXED);
2631
2632 char outBuf[2048];
2633 32 bzero(outBuf, sizeof(outBuf));
2634
2635 32 ControlPkt* cpkt = (ControlPkt*)outBuf;
2636 32 SyncPkt* spkt = (SyncPkt*)(outBuf + sizeof(ControlPkt));
2637
2638
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 InitControlPacket(cpkt);
2639
2640 32 cpkt->pktType = CTRL_PKT_SYNC;
2641 32 cpkt->extraDataLen = sizeof(SyncPkt) + filename.length();
2642
2643 32 spkt->pktType = SYNC_PKT_START;
2644 32 spkt->fileType = SYNC_FILE_SEQ;
2645 32 spkt->frameNumber = 0;
2646 32 spkt->secondsElapsed = 0;
2647 32 strcpy(spkt->filename, filename.c_str());
2648
2649
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 SendControlPacket(outBuf, sizeof(ControlPkt) + sizeof(SyncPkt) + filename.length());
2650 }
2651
2652 /*
2653 *
2654 */
2655 227 void MultiSync::SendSeqSyncStopPacket(const std::string& filename) {
2656
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 227 times.
227 if (filename.empty()) {
2657 ✗ return;
2658 }
2659
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 227 times.
227 if (!FitsInSyncPacket(filename)) {
2660 ✗ LogErr(VB_SYNC, "ERROR: SendSeqSyncStopPacket filename '%s' is too long to send\n", filename.c_str());
2661 ✗ return;
2662 }
2663
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 227 times.
227 if (m_controlSock < 0) {
2664 ✗ LogErr(VB_SYNC, "ERROR: Tried to send stop packet but sync socket is not open.\n");
2665 ✗ return;
2666 }
2667
1/2
✓ Branch 0 taken 227 times.
✗ Branch 1 not taken.
227 LogDebug(VB_SYNC, "SendSeqSyncStopPacket(%s)\n", filename.c_str());
2668
2669
2/4
✓ Branch 0 taken 227 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 227 times.
227 for (auto a : getPluginsCopy()) {
2670 ✗ a->SendSeqSyncStopPacket(filename);
2671 227 }
2672
2673 char outBuf[2048];
2674 227 bzero(outBuf, sizeof(outBuf));
2675
2676 227 ControlPkt* cpkt = (ControlPkt*)outBuf;
2677 227 SyncPkt* spkt = (SyncPkt*)(outBuf + sizeof(ControlPkt));
2678
2679
1/2
✓ Branch 0 taken 227 times.
✗ Branch 1 not taken.
227 InitControlPacket(cpkt);
2680
2681 227 cpkt->pktType = CTRL_PKT_SYNC;
2682 227 cpkt->extraDataLen = sizeof(SyncPkt) + filename.length();
2683
2684 227 spkt->pktType = SYNC_PKT_STOP;
2685 227 spkt->fileType = SYNC_FILE_SEQ;
2686 227 spkt->frameNumber = 0;
2687 227 spkt->secondsElapsed = 0;
2688 227 strcpy(spkt->filename, filename.c_str());
2689
2690
1/2
✓ Branch 0 taken 227 times.
✗ Branch 1 not taken.
227 SendControlPacket(outBuf, sizeof(ControlPkt) + sizeof(SyncPkt) + filename.length());
2691
2692 227 __atomic_store_n(&m_lastFrame, -1, __ATOMIC_RELAXED);
2693 227 __atomic_store_n(&m_lastFrameSent, -1, __ATOMIC_RELAXED);
2694 }
2695
2696 /*
2697 *
2698 */
2699 4827 void MultiSync::SendSeqSyncPacket(const std::string& filename, int frames, float seconds) {
2700
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4827 times.
4827 if (filename.empty()) {
2701 4153 return;
2702 }
2703
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4827 times.
4827 if (!FitsInSyncPacket(filename)) {
2704 ✗ LogErr(VB_SYNC, "ERROR: SendSeqSyncPacket filename '%s' is too long to send\n", filename.c_str());
2705 ✗ return;
2706 }
2707
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4827 times.
4827 if (m_controlSock < 0) {
2708 ✗ LogErr(VB_SYNC, "ERROR: Tried to send sync packet but sync socket is not open.\n");
2709 ✗ return;
2710 }
2711
3/4
✓ Branch 0 taken 4827 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 543 times.
✓ Branch 3 taken 4827 times.
5370 for (auto a : getPluginsCopy()) {
2712
1/2
✓ Branch 0 taken 543 times.
✗ Branch 1 not taken.
543 a->SendSeqSyncPacket(filename, frames, seconds);
2713 4827 }
2714 // The output thread sends these while the sequence's open, start and
2715 // stop -- on the playlist's thread -- reset them, so every access is a
2716 // relaxed atomic one (the members are plain ints in a plugin-facing
2717 // header).
2718 4827 __atomic_store_n(&m_lastFrame, frames, __ATOMIC_RELAXED);
2719 4827 int diff = frames - __atomic_load_n(&m_lastFrameSent, __ATOMIC_RELAXED);
2720
2/2
✓ Branch 0 taken 3948 times.
✓ Branch 1 taken 879 times.
4827 if (frames > 32) {
2721 // after 32 frames, we send every 10
2722 // that's either twice a second (50ms sequences) or 4 times (25ms)
2723
2/2
✓ Branch 0 taken 3517 times.
✓ Branch 1 taken 431 times.
3948 if (diff < 10) {
2724 3517 return;
2725 }
2726
4/4
✓ Branch 0 taken 874 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 636 times.
✓ Branch 3 taken 238 times.
879 } else if (frames && diff < 4) {
2727 // under 32 frames, we send every 4
2728 636 return;
2729 }
2730 674 __atomic_store_n(&m_lastFrameSent, frames, __ATOMIC_RELAXED);
2731
2732
1/2
✓ Branch 0 taken 674 times.
✗ Branch 1 not taken.
674 LogDebug(VB_SYNC, "SendSeqSyncPacket( '%s', %d, %.2f)\n",
2733 filename.c_str(), frames, seconds);
2734 char outBuf[2048];
2735 674 bzero(outBuf, sizeof(outBuf));
2736
2737 674 ControlPkt* cpkt = (ControlPkt*)outBuf;
2738 674 SyncPkt* spkt = (SyncPkt*)(outBuf + sizeof(ControlPkt));
2739
2740
1/2
✓ Branch 0 taken 674 times.
✗ Branch 1 not taken.
674 InitControlPacket(cpkt);
2741
2742 674 cpkt->pktType = CTRL_PKT_SYNC;
2743 674 cpkt->extraDataLen = sizeof(SyncPkt) + filename.length();
2744
2745 674 spkt->pktType = SYNC_PKT_SYNC;
2746 674 spkt->fileType = SYNC_FILE_SEQ;
2747 674 spkt->frameNumber = frames;
2748 674 spkt->secondsElapsed = seconds;
2749 674 strcpy(spkt->filename, filename.c_str());
2750
2751
1/2
✓ Branch 0 taken 674 times.
✗ Branch 1 not taken.
674 SendControlPacket(outBuf, sizeof(ControlPkt) + sizeof(SyncPkt) + filename.length());
2752 }
2753
2754 8 void MultiSync::SendMediaOpenPacket(const std::string& filename) {
2755
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (filename.empty()) {
2756 ✗ return;
2757 }
2758
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (!FitsInSyncPacket(filename)) {
2759 ✗ LogErr(VB_SYNC, "ERROR: SendMediaOpenPacket filename '%s' is too long to send\n", filename.c_str());
2760 ✗ return;
2761 }
2762
2763
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m_controlSock < 0) {
2764 ✗ LogErr(VB_SYNC, "ERROR: Tried to send start packet but sync socket is not open.\n");
2765 ✗ return;
2766 }
2767
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogDebug(VB_SYNC, "SendMediaOpenPacket('%s')\n", filename.c_str());
2768
2769
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 for (auto a : getPluginsCopy()) {
2770 ✗ a->SendMediaOpenPacket(filename);
2771 8 }
2772
2773 8 m_lastMediaHalfSecond = 0;
2774
2775 char outBuf[2048];
2776 8 bzero(outBuf, sizeof(outBuf));
2777
2778 8 ControlPkt* cpkt = (ControlPkt*)outBuf;
2779 8 SyncPkt* spkt = (SyncPkt*)(outBuf + sizeof(ControlPkt));
2780
2781
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 InitControlPacket(cpkt);
2782
2783 8 cpkt->pktType = CTRL_PKT_SYNC;
2784 8 cpkt->extraDataLen = sizeof(SyncPkt) + filename.length();
2785
2786 8 spkt->pktType = SYNC_PKT_OPEN;
2787 8 spkt->fileType = SYNC_FILE_MEDIA;
2788 8 spkt->frameNumber = 0;
2789 8 spkt->secondsElapsed = 0;
2790 8 strcpy(spkt->filename, filename.c_str());
2791
2792
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 SendControlPacket(outBuf, sizeof(ControlPkt) + sizeof(SyncPkt) + filename.length());
2793 }
2794 8 void MultiSync::SendMediaSyncStartPacket(const std::string& filename) {
2795
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (filename.empty()) {
2796 ✗ return;
2797 }
2798
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (!FitsInSyncPacket(filename)) {
2799 ✗ LogErr(VB_SYNC, "ERROR: SendMediaSyncStartPacket filename '%s' is too long to send\n", filename.c_str());
2800 ✗ return;
2801 }
2802
2803
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m_controlSock < 0) {
2804 ✗ LogErr(VB_SYNC, "ERROR: Tried to send start packet but sync socket is not open.\n");
2805 ✗ return;
2806 }
2807
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogDebug(VB_SYNC, "SendMediaSyncStartPacket('%s')\n", filename.c_str());
2808
2809
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 for (auto a : getPluginsCopy()) {
2810 ✗ a->SendMediaSyncStartPacket(filename);
2811 8 }
2812
2813 8 m_lastMediaHalfSecond = 0;
2814
2815 char outBuf[2048];
2816 8 bzero(outBuf, sizeof(outBuf));
2817
2818 8 ControlPkt* cpkt = (ControlPkt*)outBuf;
2819 8 SyncPkt* spkt = (SyncPkt*)(outBuf + sizeof(ControlPkt));
2820
2821
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 InitControlPacket(cpkt);
2822
2823 8 cpkt->pktType = CTRL_PKT_SYNC;
2824 8 cpkt->extraDataLen = sizeof(SyncPkt) + filename.length();
2825
2826 8 spkt->pktType = SYNC_PKT_START;
2827 8 spkt->fileType = SYNC_FILE_MEDIA;
2828 8 spkt->frameNumber = 0;
2829 8 spkt->secondsElapsed = 0;
2830 8 strcpy(spkt->filename, filename.c_str());
2831
2832
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 SendControlPacket(outBuf, sizeof(ControlPkt) + sizeof(SyncPkt) + filename.length());
2833 }
2834
2835 /*
2836 *
2837 */
2838 8 void MultiSync::SendMediaSyncStopPacket(const std::string& filename) {
2839
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (filename.empty()) {
2840 ✗ return;
2841 }
2842
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (!FitsInSyncPacket(filename)) {
2843 ✗ LogErr(VB_SYNC, "ERROR: SendMediaSyncStopPacket filename '%s' is too long to send\n", filename.c_str());
2844 ✗ return;
2845 }
2846
2847
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m_controlSock < 0) {
2848 ✗ LogErr(VB_SYNC, "ERROR: Tried to send stop packet but sync socket is not open.\n");
2849 ✗ return;
2850 }
2851
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogDebug(VB_SYNC, "SendMediaSyncStopPacket(%s)\n", filename.c_str());
2852
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 for (auto a : getPluginsCopy()) {
2853 ✗ a->SendMediaSyncStopPacket(filename);
2854 8 }
2855
2856 char outBuf[2048];
2857 8 bzero(outBuf, sizeof(outBuf));
2858
2859 8 ControlPkt* cpkt = (ControlPkt*)outBuf;
2860 8 SyncPkt* spkt = (SyncPkt*)(outBuf + sizeof(ControlPkt));
2861
2862
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 InitControlPacket(cpkt);
2863
2864 8 cpkt->pktType = CTRL_PKT_SYNC;
2865 8 cpkt->extraDataLen = sizeof(SyncPkt) + filename.length();
2866
2867 8 spkt->pktType = SYNC_PKT_STOP;
2868 8 spkt->fileType = SYNC_FILE_MEDIA;
2869 8 spkt->frameNumber = 0;
2870 8 spkt->secondsElapsed = 0;
2871 8 strcpy(spkt->filename, filename.c_str());
2872
2873
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 SendControlPacket(outBuf, sizeof(ControlPkt) + sizeof(SyncPkt) + filename.length());
2874 }
2875
2876 /*
2877 *
2878 */
2879 2393 void MultiSync::SendMediaSyncPacket(const std::string& filename, float seconds) {
2880
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2393 times.
2393 if (filename.empty()) {
2881 2354 return;
2882 }
2883
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2393 times.
2393 if (!FitsInSyncPacket(filename)) {
2884 ✗ LogErr(VB_SYNC, "ERROR: SendMediaSyncPacket filename '%s' is too long to send\n", filename.c_str());
2885 ✗ return;
2886 }
2887
2888
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2393 times.
2393 if (m_controlSock < 0) {
2889 ✗ LogErr(VB_SYNC, "ERROR: Tried to send sync packet but sync socket is not open.\n");
2890 ✗ return;
2891 }
2892
2893
2/4
✓ Branch 0 taken 2393 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2393 times.
2393 for (auto a : getPluginsCopy()) {
2894 ✗ a->SendMediaSyncPacket(filename, seconds);
2895 2393 }
2896
2897 2393 int curTS = (seconds * 2.0f);
2898
2/2
✓ Branch 0 taken 2354 times.
✓ Branch 1 taken 39 times.
2393 if (m_lastMediaHalfSecond == curTS) {
2899 // not time to send
2900 2354 return;
2901 }
2902 39 m_lastMediaHalfSecond = curTS;
2903
2904
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 LogExcess(VB_SYNC, "SendMediaSyncPacket( '%s', %.2f)\n",
2905 filename.c_str(), seconds);
2906
2907 char outBuf[2048];
2908 39 bzero(outBuf, sizeof(outBuf));
2909
2910 39 ControlPkt* cpkt = (ControlPkt*)outBuf;
2911 39 SyncPkt* spkt = (SyncPkt*)(outBuf + sizeof(ControlPkt));
2912
2913
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 InitControlPacket(cpkt);
2914
2915 39 cpkt->pktType = CTRL_PKT_SYNC;
2916 39 cpkt->extraDataLen = sizeof(SyncPkt) + filename.length();
2917
2918 39 spkt->pktType = SYNC_PKT_SYNC;
2919 39 spkt->fileType = SYNC_FILE_MEDIA;
2920 39 spkt->frameNumber = 0;
2921 39 spkt->secondsElapsed = seconds;
2922 39 strcpy(spkt->filename, filename.c_str());
2923
2924
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 SendControlPacket(outBuf, sizeof(ControlPkt) + sizeof(SyncPkt) + filename.length());
2925 }
2926
2927 2 void MultiSync::SendPluginData(const std::string& name, const uint8_t* data, int len) {
2928
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (name.empty()) {
2929 1 return;
2930 }
2931
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 for (auto a : getPluginsCopy()) {
2932 ✗ a->SendPluginData(name, data, len);
2933 2 }
2934
2935
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogDebug(VB_SYNC, "SendPluginData('%s')\n", name.c_str());
2936
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (m_controlSock < 0) {
2937 ✗ LogErr(VB_SYNC, "ERROR: Tried to send plugin data packet but control socket is not open.\n");
2938 ✗ return;
2939 }
2940
2941 char outBuf[2048];
2942 2 bzero(outBuf, sizeof(outBuf));
2943
2944 2 ControlPkt* cpkt = (ControlPkt*)outBuf;
2945 2 CommandPkt* epkt = (CommandPkt*)(outBuf + sizeof(ControlPkt));
2946
2947
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 InitControlPacket(cpkt);
2948 2 int nlen = strlen(name.c_str()) + 1; // add the null
2949
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 if (len < 0 || ((size_t)nlen + (size_t)len) > MAX_MS_PAYLOAD) {
2950
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_SYNC, "ERROR: Plugin data packet for '%s' (%d bytes) is too large to send\n",
2951 name.c_str(), len);
2952 1 return;
2953 }
2954 1 cpkt->pktType = CTRL_PKT_PLUGIN;
2955 1 cpkt->extraDataLen = len + nlen;
2956
2957 1 strcpy(epkt->command, name.c_str());
2958 1 memcpy(&epkt->command[nlen], data, len);
2959
2960
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 SendControlPacket(outBuf, sizeof(ControlPkt) + len + nlen);
2961 }
2962
2963 /*
2964 *
2965 */
2966 67 void MultiSync::SendBlankingDataPacket(void) {
2967
1/2
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
67 LogDebug(VB_SYNC, "SendBlankingDataPacket()\n");
2968
2969
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 67 times.
67 if (m_controlSock < 0) {
2970 ✗ LogErr(VB_SYNC, "ERROR: Tried to send blanking data packet but control socket is not open.\n");
2971 ✗ return;
2972 }
2973
2/4
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 67 times.
67 for (auto a : getPluginsCopy()) {
2974 ✗ a->SendBlankingDataPacket();
2975 67 }
2976 char outBuf[2048];
2977 67 bzero(outBuf, sizeof(outBuf));
2978
2979 67 ControlPkt* cpkt = (ControlPkt*)outBuf;
2980
2981
1/2
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
67 InitControlPacket(cpkt);
2982
2983 67 cpkt->pktType = CTRL_PKT_BLANK;
2984 67 cpkt->extraDataLen = 0;
2985
2986
1/2
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
67 SendControlPacket(outBuf, sizeof(ControlPkt));
2987 }
2988 9796 std::vector<MultiSyncPlugin*> MultiSync::getPluginsCopy() {
2989
1/2
✓ Branch 0 taken 9796 times.
✗ Branch 1 not taken.
9796 std::unique_lock<std::mutex> lock(m_pluginsLock);
2990
1/2
✓ Branch 0 taken 9796 times.
✗ Branch 1 not taken.
19592 return m_plugins;
2991 9796 }
2992 24000 void MultiSync::addMultiSyncPlugin(MultiSyncPlugin* p) {
2993
1/2
✓ Branch 0 taken 24000 times.
✗ Branch 1 not taken.
24000 std::unique_lock<std::mutex> lock(m_pluginsLock);
2994
1/2
✓ Branch 0 taken 24000 times.
✗ Branch 1 not taken.
24000 m_plugins.push_back(p);
2995 24000 }
2996 24000 void MultiSync::removeMultiSyncPlugin(MultiSyncPlugin* p) {
2997
1/2
✓ Branch 0 taken 24000 times.
✗ Branch 1 not taken.
24000 std::unique_lock<std::mutex> lock(m_pluginsLock);
2998
1/2
✓ Branch 0 taken 24000 times.
✗ Branch 1 not taken.
24000 auto a = std::find(m_plugins.begin(), m_plugins.end(), p);
2999
1/2
✓ Branch 0 taken 24000 times.
✗ Branch 1 not taken.
24000 if (a != m_plugins.end()) {
3000
1/2
✓ Branch 0 taken 24000 times.
✗ Branch 1 not taken.
24000 m_plugins.erase(a);
3001 }
3002 24000 }
3003
3004 /*
3005 *
3006 */
3007 2325 void MultiSync::ShutdownSync(void) {
3008
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 LogDebug(VB_SYNC, "ShutdownSync()\n");
3009
3010 // Idempotent, like WLEDAudioSync::Cleanup(): fppd's shutdown calls this and
3011 // ~MultiSync() calls it again at static-destruction time, by which point the
3012 // global SettingsConfig may already be gone -- locking its destroyed mutex
3013 // would throw out of a noexcept destructor. Unregistering is not a
3014 // barrier: listeners are called from a copy with no settings lock held, so
3015 // one may still be running ReloadSyncDestinations() on the settings-reload
3016 // thread, or be about to start. Close the gate and wait for those to
3017 // finish before the sockets below are closed.
3018
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 if (!m_settingsListenersRemoved.exchange(true)) {
3019 13950 for (const char* s : { "MultiSyncRemotes", "MultiSyncExtraRemotes",
3020
2/2
✓ Branch 0 taken 11625 times.
✓ Branch 1 taken 2325 times.
16275 "MultiSyncBroadcast", "MultiSyncMulticast", "MultiSyncUnicast" }) {
3021
3/6
✓ Branch 0 taken 11625 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11625 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11625 times.
✗ Branch 5 not taken.
46500 unregisterSettingsListener("MultiSync", s);
3022 }
3023
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::unique_lock<std::mutex> lock(s_listenerQuiesceLock);
3024 2325 s_listenersShutdown = true;
3025
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
4650 s_listenerQuiesceCV.wait(lock, []() { return s_listenersInFlight == 0; });
3026 2325 }
3027
3028 {
3029
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::unique_lock<std::mutex> pluginLock(m_pluginsLock);
3030
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 auto plugins = m_plugins;
3031
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 pluginLock.unlock();
3032
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2325 times.
2325 for (auto a : plugins) {
3033 ✗ a->ShutdownSync();
3034 }
3035 2325 }
3036 {
3037
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::unique_lock<std::mutex> pluginLock(m_pluginsLock);
3038 2325 m_plugins.clear();
3039 2325 }
3040
3041
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::unique_lock<std::mutex> lock(m_socketLock);
3042
2/2
✓ Branch 0 taken 1867 times.
✓ Branch 1 taken 458 times.
2325 if (m_broadcastSock >= 0) {
3043
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 close(m_broadcastSock);
3044 1867 m_broadcastSock = -1;
3045 }
3046
3047
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 2293 times.
2325 if (m_controlSock >= 0) {
3048
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 close(m_controlSock);
3049 32 m_controlSock = -1;
3050 }
3051
3052
2/2
✓ Branch 0 taken 1867 times.
✓ Branch 1 taken 458 times.
2325 if (m_twinklyDiscoverySock >= 0) {
3053
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 EPollManager::INSTANCE.removeFileDescriptor(m_twinklyDiscoverySock);
3054
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 close(m_twinklyDiscoverySock);
3055 1867 m_twinklyDiscoverySock = -1;
3056 }
3057
3058
2/2
✓ Branch 0 taken 1867 times.
✓ Branch 1 taken 458 times.
2325 if (m_receiveSock >= 0) {
3059
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 close(m_receiveSock);
3060 1867 m_receiveSock = -1;
3061 }
3062 2325 }
3063
3064 /*
3065 *
3066 */
3067 1887 int MultiSync::OpenBroadcastSocket(void) {
3068
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 LogDebug(VB_SYNC, "OpenBroadcastSocket()\n");
3069
3070 1887 m_broadcastSock = OpenSocket(AF_INET, SOCK_DGRAM, 0);
3071
3072
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (m_broadcastSock < 0) {
3073 ✗ LogErr(VB_SYNC, "Error opening MultiSync broadcast socket\n");
3074 ✗ WarningHolder::AddWarning(42, "MultiSync: could not open broadcast socket");
3075 ✗ return 0;
3076 }
3077
3078 1887 char loopch = 0;
3079
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (setsockopt(m_broadcastSock, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&loopch, sizeof(loopch)) < 0) {
3080 ✗ LogErr(VB_SYNC, "Error setting IP_MULTICAST_LOOP: \n",
3081 FPPstrerror(errno));
3082 ✗ return 0;
3083 }
3084
3085 1887 int broadcast = 1;
3086
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (setsockopt(m_broadcastSock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) {
3087 ✗ LogErr(VB_SYNC, "Error setting SO_BROADCAST: \n", FPPstrerror(errno));
3088 ✗ return 0;
3089 }
3090
3091 1887 return 1;
3092 }
3093
3094 /*
3095 *
3096 */
3097 32 int MultiSync::OpenControlSockets() {
3098
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 LogDebug(VB_SYNC, "OpenControlSockets()\n");
3099
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 std::unique_lock<std::mutex> openLock(s_controlSockOpenLock);
3100
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (m_controlSock >= 0) {
3101 ✗ return 1;
3102 }
3103
3104 // Set the socket up fully before publishing it, so a sender never sees a
3105 // half-configured one, and a failed setsockopt() doesn't leave it behind
3106 // as if the open had succeeded.
3107 32 int sock = OpenSocket(AF_INET, SOCK_DGRAM, 0);
3108
3109
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (sock < 0) {
3110 ✗ LogErr(VB_SYNC, "Error opening MultiSync socket\n");
3111 ✗ WarningHolder::AddWarning(42, "MultiSync: could not open control socket");
3112 ✗ return 0;
3113 }
3114
3115 32 char loopch = 0;
3116
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&loopch, sizeof(loopch)) < 0) {
3117 ✗ LogErr(VB_SYNC, "Error setting IP_MULTICAST_LOOP: %s\n",
3118 FPPstrerror(errno));
3119 ✗ close(sock);
3120 ✗ return 0;
3121 }
3122
3123 {
3124
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 std::unique_lock<std::mutex> lock(m_socketLock);
3125 32 m_controlSock = sock;
3126 32 }
3127 // The destination reload resolves hostnames, so don't make other threads
3128 // wanting the socket wait on it; they can use the socket already.
3129
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 openLock.unlock();
3130
3131
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 ReloadSyncDestinations();
3132
3133
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 FillInInterfaces();
3134
3135 32 return 1;
3136 32 }
3137
3138 39 void MultiSync::ReloadSyncDestinations() {
3139 // Same lock UpdateUnicastDestinations() uses, for the same reason: the
3140 // resolve step below is slow and runs without m_socketLock, so two
3141 // overlapping reloads could otherwise finish out of order and leave the
3142 // older result in place. Holding it also keeps UpdateUnicastDestinations()
3143 // from reading m_destAddr for its dedupe set mid-swap. Nothing in this
3144 // function may take m_systemsLock while it is held; it is released before
3145 // the UpdateUnicastDestinations() call at the end, which snapshots the
3146 // remote list under m_systemsLock and then takes this lock itself.
3147
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 std::unique_lock<std::mutex> updateLock(m_unicastUpdateLock);
3148
3149 39 std::set<std::string> remotes;
3150
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 AddRemotesFromSetting("MultiSyncRemotes", remotes);
3151
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 AddRemotesFromSetting("MultiSyncExtraRemotes", remotes);
3152
3153 // Assign rather than |=: this runs again on every settings change, so a
3154 // method the user just turned off has to go back to false.
3155
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 bool sendBroadcast = getSettingInt("MultiSyncBroadcast") != 0;
3156
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 bool sendMulticast = getSettingInt("MultiSyncMulticast") != 0;
3157
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 bool sendUnicast = getSettingInt("MultiSyncUnicast") != 0;
3158
10/12
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 31 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 3 times.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✓ Branch 11 taken 37 times.
39 if (remotes.empty() && !sendBroadcast && !sendMulticast && !sendUnicast && m_multiSyncEnabled) {
3159 // No explicit remotes or send method configured; default to multicast.
3160 // A list that names no host (only quotes or commas, which is how the
3161 // old UI saved an empty one) counts as no remotes.
3162 2 sendMulticast = true;
3163 }
3164
3165 // Resolve into a local list first: getaddrinfo() below is an unbounded
3166 // network lookup and must not run while m_socketLock is held, since that
3167 // lock is taken by the send path on the output thread every frame.
3168 39 std::vector<struct sockaddr_in> newAddrs;
3169
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 39 times.
80 for (auto& s : remotes) {
3170
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 LogDebug(VB_SYNC, "Setting up Remote Sync for %s\n", s.c_str());
3171 struct sockaddr_in newRemote;
3172 41 memset(&newRemote, 0, sizeof(newRemote));
3173
3174 41 newRemote.sin_family = AF_INET;
3175 41 newRemote.sin_port = htons(FPP_CTRL_PORT);
3176
3177 // A letter (or a space) means this is a hostname and has to be resolved;
3178 // anything else is a dotted-quad to parse directly. The test used to
3179 // read `... == s.end()`, i.e. "contains no letters", which is backwards:
3180 // hostnames took the inet_addr() path, came back INADDR_NONE, and were
3181 // then installed as a destination of 255.255.255.255 -- so every entry
3182 // that wasn't already an IP address quietly broadcast its sync packets.
3183
4/6
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 364 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 364 times.
406 bool isHostname = std::find_if(s.begin(), s.end(), [](char c) { return (isalpha(c) || (c == ' ')); }) != s.end();
3184 41 bool valid = true;
3185
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 40 times.
41 if (isHostname) {
3186 // Reentrant and cached, unlike gethostbyname(), which shares a
3187 // single static hostent across the process.
3188 1 uint32_t resolved = 0;
3189
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (!ResolveHostToIPv4(s, resolved)) {
3190 ✗ LogErr(VB_SYNC,
3191 "Error looking up Remote hostname: %s\n",
3192 s.c_str());
3193 ✗ valid = false;
3194 } else {
3195 1 newRemote.sin_addr.s_addr = resolved;
3196 }
3197 } else {
3198 40 newRemote.sin_addr.s_addr = inet_addr(s.c_str());
3199
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 40 times.
40 if (newRemote.sin_addr.s_addr == INADDR_NONE) {
3200 ✗ LogErr(VB_SYNC, "Error parsing Remote IP address: %s\n", s.c_str());
3201 ✗ valid = false;
3202 }
3203 }
3204
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 if (valid) {
3205
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 newAddrs.push_back(newRemote);
3206 }
3207 }
3208
3209 39 m_sendBroadcast = sendBroadcast;
3210 39 m_sendMulticast = sendMulticast;
3211 39 m_sendUnicast = sendUnicast;
3212
3213 {
3214 // Swap the whole list in at once. Each mmsghdr points at its own
3215 // element of m_destAddr, so the two vectors must be rebuilt together
3216 // and never observed half-updated by SendControlPacketViaMsgs().
3217
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 std::unique_lock<std::mutex> lock(m_socketLock);
3218 39 m_destAddr = std::move(newAddrs);
3219 39 m_destMsgs.clear();
3220
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 m_destMsgs.reserve(m_destAddr.size());
3221
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 39 times.
80 for (size_t x = 0; x < m_destAddr.size(); x++) {
3222 struct mmsghdr msg;
3223 41 memset(&msg, 0, sizeof(msg));
3224
3225 41 msg.msg_hdr.msg_name = &m_destAddr[x];
3226 41 msg.msg_hdr.msg_namelen = sizeof(sockaddr_in);
3227 41 msg.msg_hdr.msg_iov = &m_destIovec;
3228 41 msg.msg_hdr.msg_iovlen = 1;
3229 41 msg.msg_len = 0;
3230
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 m_destMsgs.push_back(msg);
3231 }
3232
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 LogDebug(VB_SYNC, "%d Remote Sync systems configured\n",
3233 (int)m_destAddr.size());
3234
3235
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 37 times.
39 if (!sendUnicast) {
3236 // "Send to ALL KNOWN remotes" is off, so drop that list rather than
3237 // leaving a stale copy behind for the next time it is turned on.
3238 2 m_unicastDestMsgs.clear();
3239 2 m_unicastDestAddr.clear();
3240 }
3241 39 }
3242
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 updateLock.unlock();
3243
3244 // Rebuild the "all known remotes" list too: it dedupes itself against
3245 // m_destAddr, which just changed. Must run with both locks released --
3246 // UpdateUnicastDestinations() snapshots the remote list under m_systemsLock
3247 // and then takes m_unicastUpdateLock and m_socketLock itself.
3248
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 2 times.
39 if (sendUnicast) {
3249
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 UpdateUnicastDestinations();
3250 }
3251 39 }
3252
3253 // Malformed control packets come from whatever else is on the MultiSync port
3254 // (another program, an incompatible version), as fast as it sends them, and
3255 // each was logged with a hex dump: a line or more per packet. Log the first
3256 // few in each minute, then say how many more were ignored.
3257 static std::mutex badPacketLock;
3258 static long long badPacketWindowMS = 0;
3259 static int badPacketsLogged = 0;
3260 static int badPacketsSuppressed = 0;
3261 static constexpr int BAD_PACKETS_LOGGED_PER_MINUTE = 5;
3262
3263 1211 static bool ShouldLogBadPacket() {
3264
1/2
✓ Branch 0 taken 1211 times.
✗ Branch 1 not taken.
1211 std::lock_guard<std::mutex> l(badPacketLock);
3265
1/2
✓ Branch 0 taken 1211 times.
✗ Branch 1 not taken.
1211 long long now = GetMonotonicTimeMS();
3266
3/4
✓ Branch 0 taken 1207 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1207 times.
1211 if (!badPacketWindowMS || now - badPacketWindowMS >= 60000) {
3267
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (badPacketsSuppressed) {
3268 ✗ LogWarn(VB_SYNC, "%d more malformed MultiSync packets ignored in the last minute\n", badPacketsSuppressed);
3269 }
3270
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 badPacketWindowMS = now ? now : 1;
3271 4 badPacketsLogged = 0;
3272 4 badPacketsSuppressed = 0;
3273 }
3274
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 1191 times.
1211 if (badPacketsLogged < BAD_PACKETS_LOGGED_PER_MINUTE) {
3275 20 badPacketsLogged++;
3276 20 return true;
3277 }
3278 1191 badPacketsSuppressed++;
3279 1191 return false;
3280 1211 }
3281
3282 // Undelivered sync packets since the last log line about them, and when that
3283 // was (GetMonotonicTimeMS()). Guarded by m_socketLock; file-static rather
3284 // than members because MultiSync's layout is plugin ABI. A remote that is
3285 // gone stays gone: logging every failed packet wrote a line per sync packet
3286 // (several a second) to the SD card for the whole show.
3287 static int undeliveredSinceLog = 0;
3288 static long long undeliveredLoggedAt = 0;
3289 static constexpr long long UNDELIVERED_LOG_INTERVAL_MS = 60000;
3290
3291 // The same for the broadcast and multicast sends (ENOBUFS on Wi-Fi, an
3292 // interface that is down or firewalled): logged when it starts, then once a
3293 // minute with a count, rather than for every sync packet on every interface.
3294 // Callers hold m_socketLock.
3295 static int sendFailuresSinceLog = 0;
3296 static long long sendFailureLoggedAt = 0;
3297
3298 182 static void LogSendFailure(int err) {
3299 182 sendFailuresSinceLog++;
3300 182 long long now = GetMonotonicTimeMS();
3301
3/4
✓ Branch 0 taken 179 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 179 times.
182 if (!sendFailureLoggedAt || now - sendFailureLoggedAt >= UNDELIVERED_LOG_INTERVAL_MS) {
3302
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 LogErr(VB_SYNC, "Error: Unable to send packet: %s (%d failed sends%s)\n", FPPstrerror(err),
3303 sendFailuresSinceLog, sendFailureLoggedAt ? " in the last minute" : "");
3304 3 sendFailuresSinceLog = 0;
3305
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 sendFailureLoggedAt = now ? now : 1;
3306 }
3307 182 }
3308
3309 // How long one sync send may keep retrying a destination whose socket buffer is
3310 // full, before skipping it. These sends run on the channel output thread ahead
3311 // of the frame's channel data, and under m_socketLock, which the main loop's
3312 // media sync sends also take -- so the bound covers both. See
3313 // SendControlPacketViaMsgs() for why a dropped sync packet is the cheap side.
3314 static constexpr long long SEND_BUDGET_MS = 2;
3315
3316 // One datagram, never blocking for longer than the budget left since startTime.
3317 // A blocking sendto() here once waited for good: Wi-Fi stopped transmitting
3318 // mid-show, the multicast socket's buffer filled, and the channel output thread
3319 // slept in sendto() holding m_socketLock while the main loop queued up behind
3320 // it -- all pixel output froze until a power cycle (issue #3072).
3321 // Returns 0, or the errno of the failed send.
3322 7194 static int SendDatagramBounded(int sock, const void* buf, int len, const struct sockaddr_in& addr,
3323 long long startTime) {
3324 while (true) {
3325
2/2
✓ Branch 0 taken 6153 times.
✓ Branch 1 taken 1041 times.
7194 if (sendto(sock, buf, len, MSG_DONTWAIT, (const struct sockaddr*)&addr, sizeof(addr)) >= 0) {
3326 6153 return 0;
3327 }
3328 1041 int err = errno;
3329
4/6
✓ Branch 0 taken 175 times.
✓ Branch 1 taken 866 times.
✓ Branch 2 taken 175 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 175 times.
✗ Branch 5 not taken.
1041 bool transient = err == EAGAIN || err == EWOULDBLOCK || err == ENOBUFS;
3330
5/6
✓ Branch 0 taken 1041 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 182 times.
✓ Branch 3 taken 859 times.
✓ Branch 4 taken 182 times.
✓ Branch 5 taken 859 times.
1041 if (!transient || (GetMonotonicTimeMS() - startTime) >= SEND_BUDGET_MS) {
3331 182 return err;
3332 }
3333
1/2
✓ Branch 0 taken 859 times.
✗ Branch 1 not taken.
859 std::this_thread::sleep_for(std::chrono::microseconds(250));
3334 859 }
3335 }
3336
3337 2172 void MultiSync::SendControlPacketViaMsgs(std::vector<struct mmsghdr>& msgs, struct iovec& iovec, void* outBuf, int len) {
3338 // The msgs vector (and the sockaddrs its entries point at) may be rebuilt
3339 // concurrently by UpdateUnicastDestinations(), so read its size, point the
3340 // shared iovec at our buffer, and send all while holding m_socketLock.
3341
1/2
✓ Branch 0 taken 2172 times.
✗ Branch 1 not taken.
2172 std::unique_lock<std::mutex> lock(m_socketLock);
3342 2172 int msgCount = msgs.size();
3343
2/2
✓ Branch 0 taken 1425 times.
✓ Branch 1 taken 747 times.
2172 if (msgCount == 0) {
3344 1425 return;
3345 }
3346 747 iovec.iov_base = outBuf;
3347 747 iovec.iov_len = len;
3348
3349 // sendmmsg stops at the first message it cannot send and reports how many
3350 // it did send, so each pass simply resumes at the destination that stopped
3351 // it.
3352 //
3353 // This runs on the channel output thread ahead of the frame's channel data,
3354 // so the whole call is bounded well inside a frame. The bound matters more
3355 // than it looks: a remote that has dropped off the LAN leaves its neighbour
3356 // entry unresolved, and the kernel then answers sends to it with EAGAIN
3357 // (not EHOSTUNREACH) for as long as it is gone - measured here at ~97% of
3358 // sends. Retrying that for long turns one dead remote into a per-frame
3359 // stall for every controller, which is exactly what the budget prevents.
3360 // A skipped sync packet is cheap by comparison: the next frame sends another.
3361 747 int outputCount = 0;
3362 747 int undelivered = 0;
3363 // monotonic: on the wall clock a step back during the budget kept this
3364 // loop retrying, on the channel output thread, for the size of the step
3365
1/2
✓ Branch 0 taken 747 times.
✗ Branch 1 not taken.
747 long long startTime = GetMonotonicTimeMS();
3366
2/2
✓ Branch 0 taken 779 times.
✓ Branch 1 taken 747 times.
1526 while (outputCount < msgCount) {
3367
1/2
✓ Branch 0 taken 779 times.
✗ Branch 1 not taken.
779 int sent = sendmmsg(m_controlSock, &msgs[outputCount], msgCount - outputCount, MSG_DONTWAIT);
3368
2/2
✓ Branch 0 taken 728 times.
✓ Branch 1 taken 51 times.
779 if (sent > 0) {
3369 728 outputCount += sent;
3370 728 continue;
3371 }
3372
4/8
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 51 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 51 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 51 times.
51 bool transient = (sent == 0) || errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOBUFS;
3373
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 51 times.
51 if (transient && (GetMonotonicTimeMS() - startTime) < SEND_BUDGET_MS) {
3374 // Genuine buffer pressure clears in well under the budget.
3375 ✗ std::this_thread::sleep_for(std::chrono::microseconds(250));
3376 ✗ continue;
3377 }
3378 // Either a hard error for this destination, or it is still refusing the
3379 // packet after the budget. Step past it rather than retrying or
3380 // bailing out entirely, so the remaining healthy remotes still get this
3381 // frame's packet - one unreachable remote must not desync the rest.
3382 51 ++outputCount;
3383 51 ++undelivered;
3384 }
3385
2/2
✓ Branch 0 taken 51 times.
✓ Branch 1 taken 696 times.
747 if (undelivered) {
3386 51 int err = errno;
3387 51 undeliveredSinceLog += undelivered;
3388
1/2
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
51 long long now = GetMonotonicTimeMS();
3389
3/4
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 48 times.
51 if (!undeliveredLoggedAt || now - undeliveredLoggedAt >= UNDELIVERED_LOG_INTERVAL_MS) {
3390
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 LogErr(VB_SYNC, "Error: Unable to send multisync packet: %s (%d/%d sent; %d undelivered %s)\n",
3391 FPPstrerror(err), msgCount - undelivered, msgCount, undeliveredSinceLog,
3392 undeliveredLoggedAt ? "in the last minute" : "");
3393 3 undeliveredSinceLog = 0;
3394
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 undeliveredLoggedAt = now ? now : 1;
3395 }
3396 }
3397
2/2
✓ Branch 0 taken 747 times.
✓ Branch 1 taken 1425 times.
2172 }
3398
3399 // A broadcast is looped back to this host's own sockets (multicast is not:
3400 // IP_MULTICAST_LOOP is off). An FPP command sent to all hosts is already run
3401 // locally by its sender, and plugin data is meant for the other hosts, so the
3402 // copy that comes back must not be dispatched a second time. Remember what
3403 // was just sent and drop the matching echo. Only those two types: other
3404 // local senders (scripts, tools, tests on this box) still get through.
3405 static std::mutex s_ownSendsLock;
3406 static std::deque<std::pair<size_t, long long>> s_ownSends; // hash, monotonic ms
3407
3408 8724 static bool IsEchoCandidate(const void* buf, int len) {
3409
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8724 times.
8724 if (len < (int)sizeof(ControlPkt)) {
3410 ✗ return false;
3411 }
3412 8724 uint8_t type = ((const ControlPkt*)buf)->pktType;
3413
3/4
✓ Branch 0 taken 8712 times.
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8712 times.
8724 return type == CTRL_PKT_FPPCOMMAND || type == CTRL_PKT_PLUGIN;
3414 }
3415 18 static size_t HashPacket(const void* buf, int len) {
3416 18 return std::hash<std::string_view>{}(std::string_view((const char*)buf, len));
3417 }
3418 9 static void RememberOwnSend(const void* buf, int len) {
3419
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 long long now = GetMonotonicTimeMS();
3420
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::lock_guard<std::mutex> lock(s_ownSendsLock);
3421
5/8
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 9 times.
9 while (!s_ownSends.empty() && (s_ownSends.size() >= 64 || now - s_ownSends.front().second > 2000)) {
3422 ✗ s_ownSends.pop_front();
3423 }
3424
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 s_ownSends.emplace_back(HashPacket(buf, len), now);
3425 9 }
3426 9 static bool IsOwnEcho(const void* buf, int len) {
3427 9 size_t h = HashPacket(buf, len);
3428
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 long long now = GetMonotonicTimeMS();
3429
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::lock_guard<std::mutex> lock(s_ownSendsLock);
3430
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 for (auto it = s_ownSends.begin(); it != s_ownSends.end(); ++it) {
3431
3/6
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
9 if (it->first == h && now - it->second <= 2000) {
3432
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 s_ownSends.erase(it);
3433 9 return true;
3434 }
3435 }
3436 ✗ return false;
3437 9 }
3438
3439 1102 void MultiSync::SendControlPacket(void* outBuf, int len) {
3440
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1102 times.
1102 if (WillLog(LOG_EXCESSIVE, VB_SYNC)) {
3441 ✗ LogExcess(VB_SYNC, "SendControlPacket()\n");
3442 ✗ HexDump("Sending Control packet with contents:", outBuf, len, VB_SYNC);
3443 }
3444
3445 // Statically-configured unicast remotes (MultiSyncRemotes/ExtraRemotes).
3446 1102 SendControlPacketViaMsgs(m_destMsgs, m_destIovec, outBuf, len);
3447
3448 // All known FPP remotes in Remote mode (MultiSyncUnicast setting).
3449
2/2
✓ Branch 0 taken 1070 times.
✓ Branch 1 taken 32 times.
1102 if (m_sendUnicast) {
3450 1070 SendControlPacketViaMsgs(m_unicastDestMsgs, m_unicastDestIovec, outBuf, len);
3451 }
3452
2/2
✓ Branch 0 taken 348 times.
✓ Branch 1 taken 754 times.
1102 if (m_sendMulticast) {
3453 348 SendMulticastPacket(outBuf, len);
3454 }
3455
2/2
✓ Branch 0 taken 320 times.
✓ Branch 1 taken 782 times.
1102 if (m_sendBroadcast) {
3456 320 SendBroadcastPacket(outBuf, len);
3457 }
3458 1102 }
3459
3460 37 void MultiSync::UpdateUnicastDestinations() {
3461 // Snapshot the addresses to resolve while holding m_systemsLock only
3462 // briefly, then release it before the rebuild. The rebuild takes
3463 // m_unicastUpdateLock, which must never be acquired while m_systemsLock is
3464 // held (see the overload below), so the snapshot and the rebuild cannot
3465 // share a lock scope.
3466 37 std::vector<std::string> addrsToResolve;
3467 {
3468
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
3469
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 37 times.
39 for (auto& sys : m_remoteSystems) {
3470
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (sys.supportsUnicast) {
3471
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 addrsToResolve.push_back(sys.address);
3472 }
3473 }
3474 37 }
3475
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 UpdateUnicastDestinations(addrsToResolve);
3476 37 }
3477
3478 39 void MultiSync::UpdateUnicastDestinations(const std::vector<std::string>& addrsToResolve) {
3479 // Serialize rebuilds against each other -- DNS resolution below happens
3480 // without any other lock held, so without this, two overlapping calls could
3481 // interleave and the slower resolution could finish last and clobber a
3482 // newer result. Callers must not hold m_systemsLock here: ReloadSyncDestinations()
3483 // holds this lock and its tail reaches m_systemsLock through the no-arg
3484 // overload, so taking the two in the other order would deadlock.
3485
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 std::unique_lock<std::mutex> updateLock(m_unicastUpdateLock);
3486
3487 // getaddrinfo() (via GetIPForHost()) is an unbounded, potentially slow
3488 // DNS/mDNS lookup, which is why the caller resolves from a snapshot rather
3489 // than iterating m_remoteSystems here: resolving under m_systemsLock used to
3490 // stall every other m_systemsLock caller (notably the frequently-polled
3491 // GetSystems()) for as long as the lookup took. GetIPForHost() itself is
3492 // reentrant/thread-safe, so it's fine to call without any MultiSync lock held.
3493 39 std::vector<struct sockaddr_in> newAddrs;
3494
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 39 times.
42 for (auto& address : addrsToResolve) {
3495 struct sockaddr_in addr;
3496 3 memset(&addr, 0, sizeof(addr));
3497 3 addr.sin_family = AF_INET;
3498 3 addr.sin_port = htons(FPP_CTRL_PORT);
3499
3500
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string ipAd = address;
3501
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (GetIPForHost(ipAd)) {
3502 3 addr.sin_addr.s_addr = inet_addr(ipAd.c_str());
3503 } else {
3504 ✗ addr.sin_addr.s_addr = inet_addr(address.c_str());
3505 }
3506 // Skip anything that didn't resolve to a usable IPv4 address
3507 // (e.g. an IPv6-only peer); sockaddr_in can't represent it.
3508
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (addr.sin_addr.s_addr == INADDR_NONE || addr.sin_addr.s_addr == 0) {
3509 ✗ continue;
3510 }
3511
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 newAddrs.push_back(addr);
3512
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 }
3513
3514 // Build the new sockaddr list, then swap it into place under m_socketLock.
3515 // m_socketLock is the innermost lock here and is never held while acquiring
3516 // m_systemsLock, matching the m_systemsLock -> m_socketLock order used by Ping().
3517
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 std::unique_lock<std::mutex> lock(m_socketLock);
3518 // Skip any address already covered by the statically-configured remote list
3519 // (m_destAddr, rebuilt by ReloadSyncDestinations(), which holds the same
3520 // m_unicastUpdateLock we hold here while it swaps) so
3521 // a remote that is both individually selected for unicast AND picked up by
3522 // "all known remotes" only receives each packet once. The same set also
3523 // dedupes the all-known list against itself (e.g. a remote known under both
3524 // a hostname and an IP).
3525 39 std::set<uint32_t> seen;
3526
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 39 times.
81 for (auto& s : m_destAddr) {
3527
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 seen.insert(s.sin_addr.s_addr);
3528 }
3529 39 m_unicastDestAddr.clear();
3530
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 m_unicastDestAddr.reserve(newAddrs.size());
3531
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 39 times.
42 for (auto& addr : newAddrs) {
3532
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (seen.insert(addr.sin_addr.s_addr).second) {
3533
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 m_unicastDestAddr.push_back(addr);
3534 }
3535 }
3536 39 m_unicastDestMsgs.clear();
3537
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 m_unicastDestMsgs.reserve(m_unicastDestAddr.size());
3538
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 39 times.
42 for (size_t x = 0; x < m_unicastDestAddr.size(); x++) {
3539 struct mmsghdr msg;
3540 3 memset(&msg, 0, sizeof(msg));
3541 3 msg.msg_hdr.msg_name = &m_unicastDestAddr[x];
3542 3 msg.msg_hdr.msg_namelen = sizeof(sockaddr_in);
3543 3 msg.msg_hdr.msg_iov = &m_unicastDestIovec;
3544 3 msg.msg_hdr.msg_iovlen = 1;
3545 3 msg.msg_len = 0;
3546
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 m_unicastDestMsgs.push_back(msg);
3547 }
3548
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 LogDebug(VB_SYNC, "%d unicast MultiSync destinations (all known remotes)\n",
3549 (int)m_unicastDestMsgs.size());
3550 39 }
3551 4187 void MultiSync::SendBroadcastPacket(void* outBuf, int len) {
3552
2/4
✓ Branch 0 taken 4187 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4187 times.
4187 if (WillLog(LOG_EXCESSIVE, VB_SYNC)) {
3553 ✗ HexDump("Sending Broadcast packet with contents:", outBuf, len, VB_SYNC);
3554 }
3555
3556 4187 bool echoes = IsEchoCandidate(outBuf, len);
3557
1/2
✓ Branch 0 taken 4187 times.
✗ Branch 1 not taken.
4187 std::unique_lock<std::mutex> lock(m_socketLock);
3558
1/2
✓ Branch 0 taken 4187 times.
✗ Branch 1 not taken.
4187 long long startTime = GetMonotonicTimeMS();
3559
2/2
✓ Branch 0 taken 4861 times.
✓ Branch 1 taken 4187 times.
9048 for (auto& a : m_interfaces) {
3560 struct sockaddr_in bda;
3561 4861 memset((void*)&bda, 0, sizeof(struct sockaddr_in));
3562 4861 bda.sin_family = AF_INET;
3563 4861 bda.sin_port = htons(FPP_CTRL_PORT);
3564 4861 bda.sin_addr.s_addr = a.second.broadcastAddress;
3565
3566 // One datagram per interface, and each comes back to us: remember
3567 // each one, or the second interface's echo runs the command again.
3568
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 4852 times.
4861 if (echoes) {
3569
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 RememberOwnSend(outBuf, len);
3570 }
3571
3572
3/4
✓ Branch 0 taken 4861 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✓ Branch 3 taken 4829 times.
4861 if (int err = SendDatagramBounded(m_broadcastSock, outBuf, len, bda, startTime))
3573
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 LogSendFailure(err);
3574 }
3575 4187 }
3576 737 void MultiSync::SendMulticastPacket(void* outBuf, int len) {
3577
1/2
✓ Branch 0 taken 737 times.
✗ Branch 1 not taken.
737 std::unique_lock<std::mutex> lock(m_socketLock);
3578
1/2
✓ Branch 0 taken 737 times.
✗ Branch 1 not taken.
737 long long startTime = GetMonotonicTimeMS();
3579
2/2
✓ Branch 0 taken 1474 times.
✓ Branch 1 taken 737 times.
2211 for (auto& a : m_interfaces) {
3580 struct sockaddr_in bda;
3581 1474 memset((void*)&bda, 0, sizeof(struct sockaddr_in));
3582 1474 bda.sin_family = AF_INET;
3583 1474 bda.sin_port = htons(FPP_CTRL_PORT);
3584 1474 bda.sin_addr.s_addr = MULTISYNC_MULTICAST_ADD;
3585
3586
2/2
✓ Branch 0 taken 220 times.
✓ Branch 1 taken 1254 times.
1474 if (a.second.multicastSocket == -1) {
3587 // create the socket
3588 220 a.second.multicastSocket = OpenSocket(AF_INET, SOCK_DGRAM, 0);
3589
3590
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 220 times.
220 if (a.second.multicastSocket < 0) {
3591 ✗ LogErr(VB_SYNC, "Error opening Multicast socket for %s\n", a.second.interfaceName.c_str());
3592 } else {
3593 220 char loopch = 0;
3594
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 220 times.
220 if (setsockopt(a.second.multicastSocket, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&loopch, sizeof(loopch)) < 0) {
3595 ✗ LogErr(VB_SYNC, "Error setting IP_MULTICAST_LOOP for %s: %s\n", a.second.interfaceName.c_str(), FPPstrerror(errno));
3596 }
3597 #ifdef PLATFORM_OSX
3598 int idx = if_nametoindex(a.second.interfaceName.c_str());
3599 if (setsockopt(a.second.multicastSocket, IPPROTO_IP, IP_BOUND_IF, &idx, sizeof(idx)) < 0) {
3600 LogErr(VB_SYNC, "Error setting IP_MULTICAST Device for %s: %s\n", a.second.interfaceName.c_str(), FPPstrerror(errno));
3601 }
3602 #else
3603
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 219 times.
220 if (setsockopt(a.second.multicastSocket, SOL_SOCKET, SO_BINDTODEVICE, a.second.interfaceName.c_str(), a.second.interfaceName.size()) < 0) {
3604
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogErr(VB_SYNC, "Error setting IP_MULTICAST Device for %s: %s\n", a.second.interfaceName.c_str(), FPPstrerror(errno));
3605 }
3606 #endif
3607 }
3608 }
3609
3610
1/2
✓ Branch 0 taken 1474 times.
✗ Branch 1 not taken.
1474 if (a.second.multicastSocket >= 0) {
3611
3/4
✓ Branch 0 taken 1474 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150 times.
✓ Branch 3 taken 1324 times.
1474 if (int err = SendDatagramBounded(a.second.multicastSocket, outBuf, len, bda, startTime))
3612
1/2
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
150 LogSendFailure(err);
3613 }
3614 }
3615 737 }
3616 75 void MultiSync::SendUnicastPacket(const std::string& address, void* outBuf, int len) {
3617
2/4
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 75 times.
75 if (WillLog(LOG_EXCESSIVE, VB_SYNC)) {
3618 ✗ LogExcess(VB_SYNC, "SendControlPacket()\n");
3619 ✗ HexDump("Sending Control packet with contents:", outBuf, len, VB_SYNC);
3620 }
3621 struct sockaddr_in dest_addr;
3622 75 memset(&dest_addr, 0, sizeof(dest_addr));
3623 75 dest_addr.sin_family = AF_INET;
3624
3625
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 std::string ipAd = address;
3626
3/4
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 67 times.
✓ Branch 3 taken 8 times.
75 if (GetIPForHost(ipAd)) {
3627 67 dest_addr.sin_addr.s_addr = inet_addr(ipAd.c_str());
3628 } else {
3629 8 dest_addr.sin_addr.s_addr = inet_addr(address.c_str());
3630 }
3631 75 dest_addr.sin_port = htons(FPP_CTRL_PORT);
3632
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 std::unique_lock<std::mutex> lock(m_socketLock);
3633
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 68 times.
75 if (m_controlSock >= 0) {
3634
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 sendto(m_controlSock, outBuf, len, MSG_DONTWAIT, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
3635 } else {
3636 68 int uSock = OpenSocket(AF_INET, SOCK_DGRAM, 0);
3637
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 68 times.
68 if (uSock < 0) {
3638 ✗ LogErr(VB_SYNC, "Error opening Unicast socket\n");
3639 ✗ return;
3640 }
3641
3642
1/2
✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
68 sendto(uSock, outBuf, len, MSG_DONTWAIT, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
3643
1/2
✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
68 close(uSock);
3644 }
3645
2/4
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75 times.
✗ Branch 3 not taken.
75 }
3646
3647 3334 bool MultiSync::FillInInterfaces() {
3648 struct ifaddrs *interfaces, *tmp;
3649 3334 getifaddrs(&interfaces);
3650 3334 tmp = interfaces;
3651
3652 3334 bool change = false;
3653 // An interface can hold its 169.254 fallback and a DHCP/static address at
3654 // the same time. Only one is kept per interface, so prefer the routable
3655 // one, whatever order getifaddrs() lists them in.
3656 3334 std::set<std::string> haveRoutable;
3657
3658
1/2
✓ Branch 0 taken 3334 times.
✗ Branch 1 not taken.
3334 std::unique_lock<std::mutex> lock(m_socketLock);
3659 3334 std::map<std::string, std::string> before;
3660
2/2
✓ Branch 0 taken 2597 times.
✓ Branch 1 taken 3334 times.
5931 for (auto& [name, info] : m_interfaces) {
3661
2/4
✓ Branch 0 taken 2597 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2597 times.
✗ Branch 3 not taken.
2597 before[name] = info.interfaceAddress;
3662 }
3663
2/2
✓ Branch 0 taken 18037 times.
✓ Branch 1 taken 3334 times.
21371 while (tmp) {
3664
3/4
✓ Branch 0 taken 18037 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6993 times.
✓ Branch 3 taken 11044 times.
18037 if (tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_INET) {
3665 // Check if interface is UP and RUNNING before adding it
3666
1/2
✓ Branch 0 taken 6993 times.
✗ Branch 1 not taken.
6993 if (isSupportedForMultisync("", tmp->ifa_name) &&
3667
5/6
✓ Branch 0 taken 3655 times.
✓ Branch 1 taken 3338 times.
✓ Branch 2 taken 3655 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3655 times.
✓ Branch 5 taken 3338 times.
10648 (tmp->ifa_flags & IFF_UP) &&
3668
1/2
✓ Branch 0 taken 3655 times.
✗ Branch 1 not taken.
3655 (tmp->ifa_flags & IFF_RUNNING)) {
3669 // skip the usb* interfaces as we won't support multisync on those
3670 #ifdef PLATFORM_OSX
3671 struct sockaddr_in* ba = (struct sockaddr_in*)(tmp->ifa_dstaddr);
3672 #else
3673 3655 struct sockaddr_in* ba = (struct sockaddr_in*)(tmp->ifa_ifu.ifu_broadaddr);
3674 #endif
3675 3655 struct sockaddr_in* sa = (struct sockaddr_in*)(tmp->ifa_addr);
3676 3655 char abuf[INET_ADDRSTRLEN] = {0};
3677 3655 inet_ntop(AF_INET, &sa->sin_addr, abuf, sizeof(abuf));
3678
8/30
✓ Branch 0 taken 3655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3655 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3655 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 3655 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 3655 times.
✓ Branch 16 taken 3655 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 3655 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✓ Branch 21 taken 3655 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
10965 if (IsIPv4LinkLocal(abuf) && haveRoutable.contains(tmp->ifa_name)) {
3679 ✗ tmp = tmp->ifa_next;
3680 ✗ continue;
3681 }
3682
3/6
✓ Branch 0 taken 3655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3655 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3655 times.
✗ Branch 5 not taken.
7310 if (!IsIPv4LinkLocal(abuf)) {
3683
2/4
✓ Branch 0 taken 3655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3655 times.
✗ Branch 3 not taken.
7310 haveRoutable.insert(tmp->ifa_name);
3684 }
3685
3686
2/4
✓ Branch 0 taken 3655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3655 times.
✗ Branch 3 not taken.
7310 NetInterfaceInfo& info = m_interfaces[tmp->ifa_name];
3687
1/2
✓ Branch 0 taken 3655 times.
✗ Branch 1 not taken.
3655 change |= info.interfaceName == "";
3688
1/2
✓ Branch 0 taken 3655 times.
✗ Branch 1 not taken.
3655 info.interfaceName = tmp->ifa_name;
3689
1/2
✓ Branch 0 taken 3655 times.
✗ Branch 1 not taken.
3655 info.interfaceAddress = abuf;
3690 3655 info.address = sa->sin_addr.s_addr;
3691 3655 info.broadcastAddress = ba->sin_addr.s_addr;
3692 }
3693
1/2
✓ Branch 0 taken 11044 times.
✗ Branch 1 not taken.
11044 } else if (tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_INET6) {
3694 // FIXME for ipv6 multisync
3695 }
3696 18037 tmp = tmp->ifa_next;
3697 }
3698 3334 freeifaddrs(interfaces);
3699
2/2
✓ Branch 0 taken 4629 times.
✓ Branch 1 taken 3334 times.
7963 for (auto& [name, info] : m_interfaces) {
3700
1/2
✓ Branch 0 taken 4629 times.
✗ Branch 1 not taken.
4629 auto it = before.find(name);
3701
4/4
✓ Branch 0 taken 2597 times.
✓ Branch 1 taken 2032 times.
✓ Branch 2 taken 144 times.
✓ Branch 3 taken 2453 times.
4629 change |= it == before.end() || it->second != info.interfaceAddress;
3702 }
3703 3334 return change;
3704 3334 }
3705 741 bool MultiSync::RemoveInterface(const std::string& interface) {
3706
1/2
✓ Branch 0 taken 741 times.
✗ Branch 1 not taken.
741 std::unique_lock<std::mutex> lock(m_socketLock);
3707
1/2
✓ Branch 0 taken 741 times.
✗ Branch 1 not taken.
741 auto it = m_interfaces.find(interface);
3708
2/2
✓ Branch 0 taken 223 times.
✓ Branch 1 taken 518 times.
741 if (it != m_interfaces.end()) {
3709
1/2
✓ Branch 0 taken 223 times.
✗ Branch 1 not taken.
223 LogDebug(VB_SYNC, "Removing interface %s - %s\n", it->second.interfaceName.c_str(), it->second.interfaceAddress.c_str());
3710
3711 struct ip_mreq mreq;
3712 223 memset(&mreq, 0, sizeof(mreq));
3713 223 mreq.imr_multiaddr.s_addr = inet_addr(MULTISYNC_MULTICAST_ADDRESS);
3714 223 mreq.imr_interface.s_addr = it->second.address;
3715 223 int rc = 0;
3716
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 200 times.
223 if ((rc = setsockopt(m_receiveSock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq))) < 0) {
3717
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 LogDebug(VB_SYNC, " Did not drop Multicast membership for interface %s - %s\n", it->second.interfaceName.c_str(), it->second.interfaceAddress.c_str());
3718 }
3719
1/2
✓ Branch 0 taken 223 times.
✗ Branch 1 not taken.
223 m_interfaces.erase(it);
3720 223 return true;
3721 }
3722 518 return false;
3723 741 }
3724
3725 /*
3726 *
3727 */
3728 5435 void MultiSync::InitControlPacket(ControlPkt* pkt) {
3729 5435 bzero(pkt, sizeof(ControlPkt));
3730
3731 5435 pkt->fppd[0] = 'F';
3732 5435 pkt->fppd[1] = 'P';
3733 5435 pkt->fppd[2] = 'P';
3734 5435 pkt->fppd[3] = 'D';
3735 5435 pkt->pktType = 0;
3736 5435 pkt->extraDataLen = 0;
3737 5435 }
3738
3739 /*
3740 *
3741 */
3742 1888 int MultiSync::OpenReceiveSocket(void) {
3743
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 LogDebug(VB_SYNC, "OpenReceiveSocket()\n");
3744
3745 int UniverseOctet[2];
3746 int i;
3747 char strMulticastGroup[16];
3748
3749 /* set up socket */
3750 1888 m_receiveSock = OpenSocket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
3751
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1888 times.
1888 if (m_receiveSock < 0) {
3752 ✗ LogErr(VB_SYNC, "Error opening Receive socket; %s\n", FPPstrerror(errno));
3753 ✗ WarningHolder::AddWarning(42, "MultiSync: could not open receive socket");
3754 ✗ return 0;
3755 }
3756
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 LogDebug(VB_SYNC, "Receive socket: %d\n", m_receiveSock);
3757
3758 // The default buffer (212992 bytes) holds about 166 datagrams -- each
3759 // costs ~1.3KB of it whatever its size -- so any stall of the main loop
3760 // (an output reload, a burst of pings from a large show) dropped sync and
3761 // ping packets in the kernel. 1MB (the kernel doubles it) holds ~1600.
3762 // A plain SO_RCVBUF is clamped to rmem_max, which only fppinit raises (to
3763 // 8MB): where it hasn't run, the request came back at the default. So
3764 // force it first, as the bridge sockets do (fppd runs as root).
3765 1888 int rcvBuf = 1024 * 1024;
3766 1888 bool forced = false;
3767 #ifdef SO_RCVBUFFORCE
3768 1888 forced = setsockopt(m_receiveSock, SOL_SOCKET, SO_RCVBUFFORCE, &rcvBuf, sizeof(rcvBuf)) == 0;
3769 #endif
3770
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 1888 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1888 times.
1888 if (!forced && setsockopt(m_receiveSock, SOL_SOCKET, SO_RCVBUF, &rcvBuf, sizeof(rcvBuf)) < 0) {
3771 ✗ LogWarn(VB_SYNC, "Error setting SO_RCVBUF on the receive socket; %s\n", FPPstrerror(errno));
3772 }
3773
3774 1888 bzero((char*)&m_receiveSrcAddr, sizeof(m_receiveSrcAddr));
3775 1888 m_receiveSrcAddr.sin_family = AF_INET;
3776 1888 m_receiveSrcAddr.sin_addr.s_addr = htonl(INADDR_ANY);
3777 1888 m_receiveSrcAddr.sin_port = htons(FPP_CTRL_PORT);
3778
3779 1888 int optval = 1;
3780
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1888 times.
1888 if (setsockopt(m_receiveSock, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)) < 0) {
3781 ✗ LogErr(VB_SYNC, "Error turning on SO_REUSEPORT; %s\n", FPPstrerror(errno));
3782 ✗ return 0;
3783 }
3784
3785 // Bind the socket to address/port
3786
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1887 times.
1888 if (bind(m_receiveSock, (struct sockaddr*)&m_receiveSrcAddr, sizeof(m_receiveSrcAddr)) < 0) {
3787
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogErr(VB_SYNC, "Error binding socket; %s\n", FPPstrerror(errno));
3788
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
3 WarningHolder::AddWarning(42, "MultiSync: could not bind receive socket (port in use?)");
3789 1 return 0;
3790 }
3791
3792
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (setsockopt(m_receiveSock, IPPROTO_IP, IP_PKTINFO, &optval, sizeof(optval)) < 0) {
3793 ✗ LogErr(VB_SYNC, "Error calling setsockopt; %s\n", FPPstrerror(errno));
3794 ✗ return 0;
3795 }
3796
3797
3/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37 times.
✓ Branch 3 taken 1850 times.
1887 if (getFPPmode() == REMOTE_MODE) {
3798
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 int remoteOffsetInt = getSettingInt("remoteOffset");
3799
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 36 times.
37 if (remoteOffsetInt)
3800 1 m_remoteOffset = (float)remoteOffsetInt * -0.001;
3801 else
3802 36 m_remoteOffset = 0.0;
3803
3804
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 LogDebug(VB_SYNC, "Using remoteOffset of %.3f\n", m_remoteOffset);
3805 }
3806
3807 1887 memset(rcvMsgs, 0, sizeof(rcvMsgs));
3808
2/2
✓ Branch 0 taken 22644 times.
✓ Branch 1 taken 1887 times.
24531 for (int i = 0; i < MAX_MS_RCV_MSG; i++) {
3809 22644 rcvIovecs[i].iov_base = rcvBuffers[i];
3810 22644 rcvIovecs[i].iov_len = MAX_MS_RCV_BUFSIZE;
3811 22644 rcvMsgs[i].msg_hdr.msg_iov = &rcvIovecs[i];
3812 22644 rcvMsgs[i].msg_hdr.msg_iovlen = 1;
3813 22644 rcvMsgs[i].msg_hdr.msg_name = &rcvSrcAddr[i];
3814 22644 rcvMsgs[i].msg_hdr.msg_namelen = sizeof(struct sockaddr_storage);
3815 22644 rcvMsgs[i].msg_hdr.msg_control = &rcvCmbuf[i];
3816 22644 rcvMsgs[i].msg_hdr.msg_controllen = 0x100;
3817 }
3818
3819
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 setupMulticastReceive(false);
3820 1887 return 1;
3821 }
3822 15007 bool MultiSync::isSupportedForMultisync(const char* address, const char* intface) {
3823
7/8
✓ Branch 0 taken 8014 times.
✓ Branch 1 taken 6993 times.
✓ Branch 2 taken 8009 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 3980 times.
✓ Branch 5 taken 4029 times.
✓ Branch 6 taken 3980 times.
✗ Branch 7 not taken.
15007 if (strlen(address) > 3 && address[0] == '1' && address[1] == '2' && address[2] == '7') {
3824 // the entire 127.* subnet is localhost
3825 3980 return false;
3826 }
3827
5/8
✓ Branch 0 taken 11027 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7685 times.
✓ Branch 3 taken 3342 times.
✓ Branch 4 taken 7685 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 7685 times.
11027 if (!strncmp(intface, "usb", 3) || !strcmp(intface, "lo") || !strncmp(intface, "tether", 6) || !strncmp(intface, "SoftAp", 6)) {
3828 3342 return false;
3829 }
3830 7685 return true;
3831 }
3832
3833 7013 void MultiSync::setupMulticastReceive(bool cycle) {
3834
1/2
✓ Branch 0 taken 7013 times.
✗ Branch 1 not taken.
7013 LogDebug(VB_SYNC, "setupMulticastReceive()\n");
3835 // loop through all the interfaces and subscribe to the group
3836
1/2
✓ Branch 0 taken 7013 times.
✗ Branch 1 not taken.
7013 std::unique_lock<std::mutex> lock(m_socketLock);
3837
2/2
✓ Branch 0 taken 7798 times.
✓ Branch 1 taken 7013 times.
14811 for (auto& a : m_interfaces) {
3838
1/2
✓ Branch 0 taken 7798 times.
✗ Branch 1 not taken.
7798 LogDebug(VB_SYNC, " Adding interface %s - %s\n", a.second.interfaceName.c_str(), a.second.interfaceAddress.c_str());
3839 struct ip_mreq mreq;
3840 7798 memset(&mreq, 0, sizeof(mreq));
3841 7798 mreq.imr_multiaddr.s_addr = inet_addr(MULTISYNC_MULTICAST_ADDRESS);
3842 7798 mreq.imr_interface.s_addr = a.second.address;
3843 7798 int rc = 0;
3844
2/2
✓ Branch 0 taken 3519 times.
✓ Branch 1 taken 4279 times.
7798 if (cycle) {
3845 3519 setsockopt(m_receiveSock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
3846 }
3847
2/2
✓ Branch 0 taken 2938 times.
✓ Branch 1 taken 4860 times.
7798 if ((rc = setsockopt(m_receiveSock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq))) < 0) {
3848
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2938 times.
2938 if (m_broadcastSock < 0) {
3849 // first time through, log as warning, otherwise error is likely due to already being subscribed
3850 ✗ LogWarn(VB_SYNC, " Could not setup Multicast Group for interface %s rc: %d\n", a.second.interfaceName.c_str(), rc);
3851 } else {
3852
1/2
✓ Branch 0 taken 2938 times.
✗ Branch 1 not taken.
2938 LogDebug(VB_SYNC, " Could not setup Multicast Group for interface %s rc: %d\n", a.second.interfaceName.c_str(), rc);
3853 }
3854 }
3855 }
3856 7013 }
3857
3858 // True if a later message in this recvmmsg batch supersedes message i (same
3859 // sync file type and sync packet type), so only the newest need be acted on.
3860 // Entries are one per received message, with a null buffer for messages that
3861 // were not received or are too short to hold a ControlPkt, so the indexes line
3862 // up with the msg indexes the caller uses. Only packets long enough to hold
3863 // the SyncPkt that follows the header can be compared.
3864 19613 static bool shouldSkipPacket(int i, int num, const std::vector<std::pair<unsigned char*, int>>& rcvBuffers) {
3865 19613 constexpr int MIN_SYNC_LEN = (int)(sizeof(ControlPkt) + sizeof(SyncPkt));
3866
5/6
✓ Branch 0 taken 19613 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5813 times.
✓ Branch 3 taken 13800 times.
✓ Branch 4 taken 5813 times.
✓ Branch 5 taken 13800 times.
19613 if (rcvBuffers[i].first == nullptr || rcvBuffers[i].second < MIN_SYNC_LEN) {
3867 5813 return false;
3868 }
3869 13800 ControlPkt* pkt = (ControlPkt*)(rcvBuffers[i].first);
3870
2/2
✓ Branch 0 taken 9875 times.
✓ Branch 1 taken 3925 times.
13800 if (pkt->pktType != CTRL_PKT_SYNC) {
3871 9875 return false;
3872 }
3873 3925 SyncPkt* spkt = (SyncPkt*)(((char*)pkt) + sizeof(ControlPkt));
3874
2/2
✓ Branch 0 taken 975 times.
✓ Branch 1 taken 3374 times.
4349 for (int x = i + 1; x < num; x++) {
3875
5/6
✓ Branch 0 taken 975 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 934 times.
✓ Branch 4 taken 41 times.
✓ Branch 5 taken 934 times.
975 if (rcvBuffers[x].first == nullptr || rcvBuffers[x].second < MIN_SYNC_LEN) {
3876 41 continue;
3877 }
3878 934 ControlPkt* npkt = (ControlPkt*)(rcvBuffers[x].first);
3879
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 890 times.
934 if (npkt->pktType != CTRL_PKT_SYNC) {
3880 44 continue;
3881 }
3882 890 SyncPkt* snpkt = (SyncPkt*)(((char*)npkt) + sizeof(ControlPkt));
3883
4/4
✓ Branch 0 taken 633 times.
✓ Branch 1 taken 257 times.
✓ Branch 2 taken 551 times.
✓ Branch 3 taken 82 times.
890 if (spkt->fileType == snpkt->fileType && spkt->pktType == snpkt->pktType) {
3884 551 return true;
3885 }
3886 }
3887 3374 return false;
3888 }
3889
3890 /*
3891 *
3892 */
3893 5625 void MultiSync::ProcessControlPacket(bool pingOnly) {
3894 5625 LogExcess(VB_SYNC, "ProcessControlPacket()\n");
3895
3896 ControlPkt* pkt;
3897
3898 5625 int msgcnt = recvmmsg(m_receiveSock, rcvMsgs, MAX_MS_RCV_MSG, MSG_DONTWAIT, nullptr);
3899 // Batches read per wakeup: the socket is level-triggered, so the rest is
3900 // read on the next pass. Draining until empty let a flood of packets keep
3901 // the main loop in here for as long as it lasted.
3902 5625 int batches = 1;
3903
2/2
✓ Branch 0 taken 6765 times.
✓ Branch 1 taken 5625 times.
12390 while (msgcnt > 0) {
3904 // One entry per received message, buffer and length, so that the
3905 // indexes shouldSkipPacket() walks are the same msg indexes used below.
3906 // Messages that failed or are too short for a ControlPkt get a null
3907 // placeholder rather than being left out of the vector.
3908 6765 std::vector<std::pair<unsigned char*, int>> v;
3909
1/2
✓ Branch 0 taken 6765 times.
✗ Branch 1 not taken.
6765 v.reserve(msgcnt);
3910
2/2
✓ Branch 0 taken 19743 times.
✓ Branch 1 taken 6765 times.
26508 for (int msg = 0; msg < msgcnt; msg++) {
3911 19743 int len = rcvMsgs[msg].msg_len;
3912
2/2
✓ Branch 0 taken 130 times.
✓ Branch 1 taken 19613 times.
19743 if (len < (int)sizeof(ControlPkt)) {
3913
1/2
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
130 v.emplace_back(nullptr, 0);
3914 130 continue;
3915 }
3916 19613 unsigned char* inBuf = rcvBuffers[msg];
3917 19613 inBuf[len] = 0;
3918
1/2
✓ Branch 0 taken 19613 times.
✗ Branch 1 not taken.
19613 v.emplace_back(inBuf, len);
3919 }
3920
1/2
✓ Branch 0 taken 6765 times.
✗ Branch 1 not taken.
6765 LogExcess(VB_SYNC, "ProcessControlPacket msgcnt: %d\n", msgcnt);
3921
2/2
✓ Branch 0 taken 19743 times.
✓ Branch 1 taken 6765 times.
26508 for (int msg = 0; msg < msgcnt; msg++) {
3922 19743 int len = rcvMsgs[msg].msg_len;
3923
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 19687 times.
19743 if (len <= 0) {
3924 // an empty datagram: recvmmsg() itself succeeded (a failure
3925 // ends the batch), so there is no error to report
3926 1596 continue;
3927 }
3928 19687 unsigned char* inBuf = rcvBuffers[msg];
3929
3930
2/2
✓ Branch 0 taken 74 times.
✓ Branch 1 taken 19613 times.
19687 if (len < sizeof(ControlPkt)) {
3931
3/4
✓ Branch 0 taken 74 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 72 times.
74 if (ShouldLogBadPacket()) {
3932
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SYNC, "Error: Received control packet too short\n");
3933
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 HexDump("Received data:", (void*)inBuf, len, VB_SYNC);
3934 }
3935 74 continue;
3936 }
3937
2/2
✓ Branch 0 taken 551 times.
✓ Branch 1 taken 19062 times.
19613 if (shouldSkipPacket(msg, msgcnt, v)) {
3938
1/2
✓ Branch 0 taken 551 times.
✗ Branch 1 not taken.
551 LogExcess(VB_SYNC, "Skipping sync packet %d/%d\n", msg, msgcnt);
3939 551 continue;
3940 }
3941
3942 char tmpIP[INET_ADDRSTRLEN];
3943 19062 inet_ntop(rcvSrcAddr[msg].ss_family, &(((struct sockaddr_in*)&rcvSrcAddr[msg])->sin_addr), tmpIP, INET_ADDRSTRLEN);
3944
1/2
✓ Branch 0 taken 19062 times.
✗ Branch 1 not taken.
19062 std::string sourceIP(tmpIP);
3945 19062 std::string hostname;
3946
3947 // Both system lists have to be read under m_systemsLock: UpdateSystem()
3948 // runs on the HTTP/mDNS probe threads and can push_back onto
3949 // m_remoteSystems, reallocating it out from under this loop. The
3950 // stats map work below then runs under m_statsLock alone -- the two
3951 // locks are never held at the same time.
3952 19062 bool isLocal = false;
3953 {
3954
1/2
✓ Branch 0 taken 19062 times.
✗ Branch 1 not taken.
19062 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
3955
2/2
✓ Branch 0 taken 19422 times.
✓ Branch 1 taken 19062 times.
38484 for (auto& sys : m_localSystems) {
3956
2/2
✓ Branch 0 taken 4537 times.
✓ Branch 1 taken 14885 times.
19422 if (sys.address == sourceIP) {
3957 4537 isLocal = true;
3958 }
3959 }
3960
2/2
✓ Branch 0 taken 14525 times.
✓ Branch 1 taken 4537 times.
19062 if (!isLocal) {
3961
2/2
✓ Branch 0 taken 2710403 times.
✓ Branch 1 taken 14525 times.
2724928 for (auto& sys : m_remoteSystems) {
3962
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2710402 times.
2710403 if (sys.address == sourceIP)
3963
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 hostname = sys.hostname;
3964 }
3965 }
3966 19062 }
3967
3968 19062 pkt = (ControlPkt*)inBuf;
3969
3970
2/2
✓ Branch 0 taken 18956 times.
✓ Branch 1 taken 106 times.
19062 if ((pkt->fppd[0] != 'F') ||
3971
1/2
✓ Branch 0 taken 18956 times.
✗ Branch 1 not taken.
18956 (pkt->fppd[1] != 'P') ||
3972
1/2
✓ Branch 0 taken 18956 times.
✗ Branch 1 not taken.
18956 (pkt->fppd[2] != 'P') ||
3973
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18956 times.
18956 (pkt->fppd[3] != 'D')) {
3974
3/4
✓ Branch 0 taken 106 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 100 times.
106 if (ShouldLogBadPacket()) {
3975
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogErr(VB_SYNC, "Error: Invalid Received Control Packet, missing 'FPPD' header\n");
3976
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 HexDump("Received data:", (void*)inBuf, len, VB_SYNC);
3977 }
3978 106 continue;
3979 }
3980
3981
2/2
✓ Branch 0 taken 800 times.
✓ Branch 1 taken 18156 times.
18956 if (len != (sizeof(ControlPkt) + pkt->extraDataLen)) {
3982
3/4
✓ Branch 0 taken 800 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 790 times.
800 if (ShouldLogBadPacket()) {
3983
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 LogErr(VB_SYNC, "Error: Expected %d data bytes, received %d\n",
3984 pkt->extraDataLen, len - sizeof(ControlPkt));
3985
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 HexDump("Received data:", (void*)inBuf, len, VB_SYNC);
3986 }
3987 800 continue;
3988 }
3989
3990
2/4
✓ Branch 0 taken 18156 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 18156 times.
18156 if (WillLog(LOG_EXCESSIVE, VB_SYNC)) {
3991 ✗ HexDump("Received MultiSync packet with contents:", (void*)inBuf, len, VB_SYNC);
3992 }
3993
3994 // Per-sender stats, created only for a well-formed packet (any
3995 // datagram from a new, possibly spoofed, source used to add one),
3996 // and only up to MAX_SYNC_STATS senders. Entries are never freed
3997 // (their pointers are used after m_statsLock is released), so a
3998 // sender past the cap simply isn't tracked.
3999 static constexpr size_t MAX_SYNC_STATS = 1024;
4000
3/6
✓ Branch 0 taken 18156 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18156 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18156 times.
✗ Branch 5 not taken.
54468 MultiSyncStats tempStats("", "");
4001 18156 MultiSyncStats* stats = &tempStats;
4002
2/2
✓ Branch 0 taken 13619 times.
✓ Branch 1 taken 4537 times.
18156 if (!isLocal) {
4003
1/2
✓ Branch 0 taken 13619 times.
✗ Branch 1 not taken.
13619 std::unique_lock<std::recursive_mutex> slock(m_statsLock);
4004
1/2
✓ Branch 0 taken 13619 times.
✗ Branch 1 not taken.
13619 auto a = m_syncStats.find(sourceIP);
4005
2/2
✓ Branch 0 taken 12425 times.
✓ Branch 1 taken 1194 times.
13619 if (a != m_syncStats.end()) {
4006 12425 stats = (MultiSyncStats*)a->second;
4007
2/2
✓ Branch 0 taken 1118 times.
✓ Branch 1 taken 76 times.
1194 } else if (m_syncStats.size() < MAX_SYNC_STATS) {
4008
5/12
✓ Branch 0 taken 1118 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1118 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1118 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1118 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1118 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
1118 stats = new MultiSyncStats(sourceIP, hostname);
4009
1/2
✓ Branch 0 taken 1118 times.
✗ Branch 1 not taken.
1118 m_syncStats[sourceIP] = stats;
4010 }
4011 13619 stats->lastReceiveTime = time(NULL);
4012 13619 }
4013
4014
8/10
✓ Branch 0 taken 4537 times.
✓ Branch 1 taken 13619 times.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 4528 times.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✓ Branch 9 taken 18147 times.
18156 if (isLocal && IsEchoCandidate(inBuf, len) && IsOwnEcho(inBuf, len)) {
4015 9 continue; // our own broadcast, looped back (see IsOwnEcho)
4016 }
4017
4018
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 18147 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
18147 if (!pingOnly || pkt->pktType == CTRL_PKT_PING) {
4019
7/7
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 8788 times.
✓ Branch 2 taken 92 times.
✓ Branch 3 taken 9045 times.
✓ Branch 4 taken 28 times.
✓ Branch 5 taken 59 times.
✓ Branch 6 taken 104 times.
18147 switch (pkt->pktType) {
4020 31 case CTRL_PKT_CMD:
4021
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 ProcessCommandPacket(pkt, len, stats);
4022 31 break;
4023 8788 case CTRL_PKT_SYNC:
4024
3/4
✓ Branch 0 taken 8788 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2220 times.
✓ Branch 3 taken 6568 times.
8788 if (getFPPmode() == REMOTE_MODE) {
4025
1/2
✓ Branch 0 taken 2220 times.
✗ Branch 1 not taken.
2220 ProcessSyncPacket(pkt, len, stats);
4026 }
4027 8788 break;
4028 92 case CTRL_PKT_BLANK:
4029
3/4
✓ Branch 0 taken 92 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 83 times.
92 if (getFPPmode() == REMOTE_MODE) {
4030
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
9 for (auto a : getPluginsCopy()) {
4031 ✗ a->ReceivedBlankingDataPacket();
4032 9 }
4033 9 BumpStat(stats->pktBlank);
4034
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 sequence->SendBlankingData();
4035 }
4036 92 break;
4037 9045 case CTRL_PKT_PING: {
4038 9045 struct sockaddr_in* inAddr = (struct sockaddr_in*)(&rcvSrcAddr[msg]);
4039 9045 char ipbuf[INET_ADDRSTRLEN] = {0};
4040 9045 inet_ntop(AF_INET, &inAddr->sin_addr, ipbuf, sizeof(ipbuf));
4041
1/2
✓ Branch 0 taken 9045 times.
✗ Branch 1 not taken.
9045 std::string ip = ipbuf;
4042
1/2
✓ Branch 0 taken 9045 times.
✗ Branch 1 not taken.
9045 ProcessPingPacket(pkt, len, sourceIP, stats, ip);
4043 9045 break;
4044 9045 }
4045 28 case CTRL_PKT_PLUGIN:
4046
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 ProcessPluginPacket(pkt, len, stats);
4047 28 break;
4048 59 case CTRL_PKT_FPPCOMMAND:
4049
1/2
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
59 ProcessFPPCommandPacket(pkt, len, stats);
4050 59 break;
4051 }
4052 }
4053
6/6
✓ Branch 0 taken 18147 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 18147 times.
✓ Branch 3 taken 915 times.
✓ Branch 4 taken 18147 times.
✓ Branch 5 taken 915 times.
19986 }
4054
3/4
✓ Branch 0 taken 6755 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 6755 times.
✗ Branch 3 not taken.
6765 msgcnt = ++batches > 64 ? 0 : recvmmsg(m_receiveSock, rcvMsgs, MAX_MS_RCV_MSG, MSG_DONTWAIT, nullptr);
4055 6765 }
4056 5625 }
4057
4058 48 void MultiSync::OpenSyncedSequence(const std::string& filename) {
4059 48 LogDebug(VB_SYNC, "OpenSyncedSequence(%s)\n", filename.c_str());
4060
4061
2/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 48 times.
48 for (auto a : getPluginsCopy()) {
4062 ✗ a->ReceivedSeqOpenPacket(filename);
4063 48 }
4064 48 ResetMasterPosition();
4065 48 sequence->OpenSequenceFile(filename);
4066 48 }
4067
4068 43 void MultiSync::StartSyncedSequence(const std::string& filename) {
4069 43 LogDebug(VB_SYNC, "StartSyncedSequence(%s)\n", filename.c_str());
4070
4071 43 ResetMasterPosition();
4072
2/4
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 43 times.
43 for (auto a : getPluginsCopy()) {
4073 ✗ a->ReceivedSeqSyncStartPacket(filename);
4074 43 }
4075
8/20
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 43 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 43 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 43 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 43 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 43 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 43 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
129 if (!sequence->IsSequenceRunning(filename) && !sequence->IsSequenceRunning("fallback.fseq")) {
4076 43 sequence->StartSequence(filename, 0);
4077 }
4078 43 }
4079
4080 29 void MultiSync::StopSyncedSequence(const std::string& filename) {
4081 29 LogDebug(VB_SYNC, "StopSyncedSequence(%s)\n", filename.c_str());
4082
2/4
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 29 times.
29 for (auto a : getPluginsCopy()) {
4083 ✗ a->ReceivedSeqSyncStopPacket(filename);
4084 29 }
4085 29 sequence->CloseIfOpen(filename);
4086 29 }
4087 ✗ void MultiSync::SyncStopAll() {
4088 ✗ LogDebug(VB_SYNC, "SyncStopAll()\n");
4089 ✗ sequence->CloseSequenceFile();
4090 ✗ if (!mediaOutput) {
4091 ✗ return;
4092 }
4093 ✗ CloseMediaOutput();
4094 }
4095
4096 ✗ void MultiSync::SyncPlaylistToMS(uint64_t ms, const std::string& pl, bool sendSyncPackets) {
4097 ✗ SyncPlaylistToMS(ms, -1, pl, sendSyncPackets);
4098 ✗ }
4099
4100 2 void MultiSync::SyncPlaylistToMS(uint64_t ms, int pos, const std::string& pl, bool sendSyncPackets) {
4101
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (Player::INSTANCE.GetPlaylistName() != pl) {
4102
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (pl == "") {
4103 ✗ return;
4104 }
4105
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 Player::INSTANCE.Load(pl);
4106 }
4107
4108 2 int desiredpos = pos;
4109
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (pos <= -1) {
4110 ✗ desiredpos = Player::INSTANCE.FindPosForMS(ms, pos == -2);
4111 }
4112
4113 2 float seconds = ms;
4114 2 seconds /= 1000;
4115
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (desiredpos >= 0) {
4116
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (m_controlSock < 0 && sendSyncPackets) {
4117 ✗ OpenControlSockets();
4118 }
4119
4120 2 std::string seq, med;
4121
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Player::INSTANCE.GetFilenamesForPos(desiredpos, seq, med);
4122
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (seq != "") {
4123 ✗ if (!sequence->IsSequenceRunning(seq)) {
4124 ✗ if (sequence->IsSequenceRunning()) {
4125 ✗ if (sendSyncPackets)
4126 ✗ SendSeqSyncStopPacket(sequence->GetSeqFilenameCopy());
4127 ✗ sequence->CloseSequenceFile();
4128 }
4129 ✗ ResetMasterPosition();
4130 ✗ sequence->OpenSequenceFile(seq.c_str());
4131 ✗ if (sendSyncPackets)
4132 ✗ SendSeqOpenPacket(seq);
4133 ✗ int frame = ms / sequence->GetSeqStepTime();
4134 ✗ sequence->StartSequence(seq.c_str(), frame);
4135 ✗ if (sendSyncPackets)
4136 ✗ SendSeqSyncStartPacket(seq);
4137 } else {
4138 ✗ int frame = ms / sequence->GetSeqStepTime();
4139 ✗ SyncSyncedSequence(seq.c_str(), frame, seconds);
4140 ✗ if (sendSyncPackets)
4141 ✗ SendSeqSyncPacket(seq, frame, seconds);
4142 }
4143 }
4144
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (med != "") {
4145 ✗ if (mediaOutput && !MatchesRunningMediaFilename(med.c_str())) {
4146 ✗ StopSyncedMedia(med.c_str());
4147 ✗ if (sendSyncPackets)
4148 ✗ SendMediaSyncStopPacket(med);
4149 }
4150 ✗ if (!mediaOutput) {
4151 ✗ OpenSyncedMedia(med.c_str());
4152 ✗ if (sendSyncPackets)
4153 ✗ SendMediaOpenPacket(med);
4154 ✗ StartSyncedMedia(med.c_str());
4155 ✗ if (sendSyncPackets)
4156 ✗ SendMediaSyncStartPacket(med);
4157 }
4158 ✗ UpdateMasterMediaPosition(med.c_str(), seconds);
4159 ✗ if (sendSyncPackets)
4160 ✗ SendMediaSyncPacket(med, seconds);
4161 }
4162 2 }
4163 }
4164
4165 /*
4166 *
4167 */
4168 858 void MultiSync::SyncSyncedSequence(const std::string& filename, int frameNumber, float secondsElapsed) {
4169 858 LogExcess(VB_SYNC, "SyncSyncedSequence('%s', %d, %.2f)\n",
4170 filename.c_str(), frameNumber, secondsElapsed);
4171
4172
2/4
✓ Branch 0 taken 858 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 858 times.
858 for (auto a : getPluginsCopy()) {
4173 ✗ a->ReceivedSeqSyncPacket(filename, frameNumber, secondsElapsed);
4174 858 }
4175
12/20
✓ Branch 0 taken 858 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209 times.
✓ Branch 3 taken 649 times.
✓ Branch 4 taken 209 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 209 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 209 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 209 times.
✓ Branch 11 taken 649 times.
✓ Branch 12 taken 209 times.
✓ Branch 13 taken 649 times.
✓ Branch 14 taken 209 times.
✓ Branch 15 taken 649 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
1276 if (!sequence->IsSequenceRunning(filename) && !sequence->IsSequenceRunning("fallback.fseq")) {
4176 209 sequence->StartSequence(filename, frameNumber);
4177 }
4178
2/2
✓ Branch 0 taken 652 times.
✓ Branch 1 taken 206 times.
858 if (sequence->IsSequenceRunning(filename)) {
4179 652 double framePosition = frameNumber;
4180
2/2
✓ Branch 0 taken 650 times.
✓ Branch 1 taken 2 times.
652 if (secondsElapsed > 0.0001f) {
4181 // recalculate the frame number based on the seconds, which carry
4182 // remoteOffset: kept whole, the lights would move by the offset
4183 // rounded to a frame (+30 ms at 20 fps moved them 50 ms).
4184 650 framePosition = secondsElapsed * 1000.0 / sequence->GetSeqStepTime();
4185 650 frameNumber = std::round(framePosition);
4186 }
4187 652 UpdateMasterPosition(frameNumber, framePosition);
4188 }
4189 858 }
4190
4191 22 void MultiSync::OpenSyncedMedia(const std::string& filename) {
4192 22 LogDebug(VB_SYNC, "OpenSyncedMedia(%s)\n", filename.c_str());
4193
4194
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (mediaOutput) {
4195 ✗ LogDebug(VB_SYNC, "Start media %s received while playing media %s\n",
4196 filename.c_str(), mediaOutput->m_mediaFilename.c_str());
4197
4198 ✗ CloseMediaOutput();
4199 }
4200
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 22 times.
22 for (auto a : getPluginsCopy()) {
4201 ✗ a->ReceivedMediaOpenPacket(filename);
4202 22 }
4203
4204 22 m_lastSyncedMediaPacketMS = (uint64_t)GetTimeMS();
4205 22 OpenMediaOutput(filename);
4206 22 }
4207
4208 21 void MultiSync::StartSyncedMedia(const std::string& filename, float secondsElapsed) {
4209 21 LogDebug(VB_SYNC, "StartSyncedMedia(%s, %.2f)\n", filename.c_str(), secondsElapsed);
4210
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
21 for (auto a : getPluginsCopy()) {
4211 ✗ a->ReceivedMediaSyncStartPacket(filename);
4212 21 }
4213
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 17 times.
21 int msTime = (secondsElapsed > 0.0f) ? (int)(secondsElapsed * 1000.0f) : 0;
4214 21 m_lastSyncedMediaPacketMS = (uint64_t)GetTimeMS();
4215 21 StartMediaOutput(filename, msTime);
4216 21 }
4217
4218 /*
4219 *
4220 */
4221 20 void MultiSync::StopSyncedMedia(const std::string& filename) {
4222 20 LogDebug(VB_SYNC, "StopSyncedMedia(%s)\n", filename.c_str());
4223
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 20 times.
20 for (auto a : getPluginsCopy()) {
4224 ✗ a->ReceivedMediaSyncStopPacket(filename);
4225 20 }
4226
4227 20 m_lastSyncedMediaPacketMS = 0;
4228
4229
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 16 times.
20 if (!mediaOutput) {
4230 4 return;
4231 }
4232
4233
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 if (MatchesRunningMediaFilename(filename)) {
4234 16 LogDebug(VB_SYNC, "Stopping synced media: %s\n", mediaOutput->m_mediaFilename.c_str());
4235 16 CloseMediaOutput();
4236 }
4237 }
4238
4239 /*
4240 * Close a synced media output that has run to its end while the player has
4241 * gone away (issue #2727: a wedged-decoder restart on the player left both
4242 * remotes holding an EOS'd pipeline forever -- slot still active, /dev/video10
4243 * and dmabufs still held -- while the UI happily reported "playing").
4244 *
4245 * The trigger is deliberately NOT "sync packets went quiet". A remote's media
4246 * free-runs on its own clock, so a network glitch mid-clip is harmless: the
4247 * clip keeps playing and UpdateMasterMediaPosition nudges it back when packets
4248 * resume. Tearing that down on packet silence would turn a glitch into a
4249 * visible outage. The unrecoverable state is a clip that has ENDED with the
4250 * player gone -- nothing but a sync packet can ever move it forward again.
4251 *
4252 * The normal end-of-clip-to-next-clip gap on a remote is ~13ms, so the default
4253 * grace of 10s is ~750x the real-world gap; a glitch straddling a clip boundary
4254 * costs at most the last frame going away early, and the next START packet
4255 * opens a fresh output as usual.
4256 */
4257 7771 void MultiSync::CheckSyncedMediaIdleTimeout() {
4258 7771 uint64_t last = m_lastSyncedMediaPacketMS;
4259
2/2
✓ Branch 0 taken 7525 times.
✓ Branch 1 taken 246 times.
7771 if (last == 0) {
4260 7771 return; // no synced media open
4261 }
4262 // 0 disables the watchdog
4263
1/2
✓ Branch 0 taken 246 times.
✗ Branch 1 not taken.
246 int timeoutSec = getSettingInt("RemoteSyncedMediaIdleTimeout", 10);
4264
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 246 times.
246 if (timeoutSec <= 0) {
4265 ✗ return;
4266 }
4267
2/4
✓ Branch 0 taken 246 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 246 times.
✗ Branch 3 not taken.
246 if (((uint64_t)GetTimeMS() - last) < (uint64_t)(timeoutSec * 1000)) {
4268 246 return;
4269 }
4270
4271 ✗ std::string filename;
4272 {
4273 ✗ std::unique_lock<std::mutex> lock(mediaOutputLock);
4274 ✗ if (!mediaOutput) {
4275 ✗ m_lastSyncedMediaPacketMS = 0;
4276 ✗ return;
4277 }
4278 ✗ if (mediaOutput->IsPlaying()) {
4279 ✗ return; // still playing -- free-running through a sync gap is fine
4280 }
4281 ✗ filename = mediaOutput->m_mediaFilename;
4282 ✗ }
4283
4284 ✗ LogWarn(VB_SYNC, "Synced media '%s' finished and no sync packet received in %d seconds "
4285 "- player appears to have gone away, closing media output\n",
4286 filename.c_str(), timeoutSec);
4287 ✗ m_lastSyncedMediaPacketMS = 0;
4288 ✗ CloseMediaOutput();
4289 ✗ }
4290
4291 /*
4292 *
4293 */
4294 1114 void MultiSync::SyncSyncedMedia(const std::string& filename, int frameNumber, float secondsElapsed) {
4295 1114 LogExcess(VB_SYNC, "SyncSyncedMedia('%s', %d, %.2f)\n",
4296 filename.c_str(), frameNumber, secondsElapsed);
4297
2/4
✓ Branch 0 taken 1114 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1114 times.
1114 for (auto a : getPluginsCopy()) {
4298 ✗ a->ReceivedMediaSyncPacket(filename, secondsElapsed);
4299 1114 }
4300
4301 1114 m_lastSyncedMediaPacketMS = (uint64_t)GetTimeMS();
4302 1114 UpdateMasterMediaPosition(filename, secondsElapsed);
4303 1114 }
4304
4305 /*
4306 *
4307 */
4308 // A file name from a sync packet, as a name relative to the media folder it is
4309 // looked up in, or false if it can't be one. Masters send what they play: a
4310 // name, a path below the folder, or -- for a random media item -- the full
4311 // path on the master. That full path is cut down to what follows its
4312 // music/videos/sequences folder, which also makes it work on a remote whose
4313 // media directory is elsewhere. Anything else absolute, and any "..", is
4314 // refused: the name is network input, and a remote opened whatever existing
4315 // file it named.
4316 2217 static bool SyncFilenameToMediaRelative(std::string& name, uint8_t fileType) {
4317
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2217 times.
2217 if (name.empty()) {
4318 ✗ return false;
4319 }
4320
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 2188 times.
2217 if (name[0] == '/') {
4321 static const char* const seqDirs[] = { "/sequences/", nullptr };
4322 static const char* const mediaDirs[] = { "/music/", "/videos/", nullptr };
4323 29 bool found = false;
4324
5/6
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 58 times.
✓ Branch 3 taken 16 times.
✓ Branch 4 taken 45 times.
✓ Branch 5 taken 13 times.
74 for (const char* const* d = (fileType == SYNC_FILE_SEQ) ? seqDirs : mediaDirs; *d && !found; d++) {
4325 45 size_t pos = name.find(*d);
4326
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 32 times.
45 if (pos != std::string::npos) {
4327
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 name = name.substr(pos + strlen(*d));
4328 13 found = true;
4329 }
4330 }
4331
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 13 times.
29 if (!found) {
4332 16 return false;
4333 }
4334 }
4335 2201 size_t start = 0;
4336
2/2
✓ Branch 0 taken 2201 times.
✓ Branch 1 taken 2185 times.
4386 while (start <= name.size()) {
4337 2201 size_t end = name.find('/', start);
4338
2/2
✓ Branch 0 taken 2185 times.
✓ Branch 1 taken 16 times.
2201 if (end == std::string::npos) {
4339 2185 end = name.size();
4340 }
4341
5/6
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2185 times.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✓ Branch 5 taken 2185 times.
2201 if (name.compare(start, end - start, "..") == 0 && end - start == 2) {
4342 16 return false;
4343 }
4344 2185 start = end + 1;
4345 }
4346
2/4
✓ Branch 0 taken 2185 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2185 times.
✗ Branch 3 not taken.
2185 return !name.empty() && name.back() != '/';
4347 }
4348
4349 2220 void MultiSync::ProcessSyncPacket(ControlPkt* pkt, int len, MultiSyncStats* stats) {
4350
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2217 times.
2220 if (pkt->extraDataLen < sizeof(SyncPkt)) {
4351
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (ShouldLogBadPacket()) {
4352 ✗ LogErr(VB_SYNC, "Error: Invalid length of received sync packet\n");
4353 ✗ HexDump("Received data:", (void*)pkt, len, VB_SYNC);
4354 }
4355 3 BumpStat(stats->pktError);
4356 35 return;
4357 }
4358
4359 {
4360 // Read by GetSyncStats() on an API thread; m_statsLock is the lock the
4361 // two sides share. Do not reach for m_systemsLock while holding it.
4362
1/2
✓ Branch 0 taken 2217 times.
✗ Branch 1 not taken.
2217 std::unique_lock<std::recursive_mutex> slock(m_statsLock);
4363
1/2
✓ Branch 0 taken 2217 times.
✗ Branch 1 not taken.
2217 m_syncMaster = stats->sourceIP;
4364 2217 }
4365
4366 2217 SyncPkt* spkt = (SyncPkt*)(((char*)pkt) + sizeof(ControlPkt));
4367
4368 // filename is a flexible char[1]: read it no further than the packet goes,
4369 // terminated or not.
4370 2217 size_t nameMax = pkt->extraDataLen - offsetof(SyncPkt, filename);
4371
1/2
✓ Branch 0 taken 2217 times.
✗ Branch 1 not taken.
3185 std::string filename(spkt->filename, strnlen(spkt->filename, nameMax));
4372 2217 float secondsIn = spkt->secondsElapsed;
4373
4374
2/4
✓ Branch 0 taken 2217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2217 times.
✗ Branch 3 not taken.
2217 LogDebug(VB_SYNC, "ProcessSyncPacket() filename: %s type: %d filetype: %d frameNumber: %d secondsElapsed: %0.2f\n",
4375 TruncateForLog(filename).c_str(), spkt->pktType, spkt->fileType, spkt->frameNumber, secondsIn);
4376
4377
3/4
✓ Branch 0 taken 2217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✓ Branch 3 taken 2185 times.
2217 if (!SyncFilenameToMediaRelative(filename, spkt->fileType)) {
4378 // A master sends one of these several times a second; say it once.
4379
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
32 static std::string s_lastRefused;
4380
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 25 times.
32 if (filename != s_lastRefused) {
4381
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 s_lastRefused = filename;
4382
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 LogWarn(VB_SYNC, "Ignoring sync packets from %s for '%s': not a name in the media folders\n",
4383 stats->sourceIP.c_str(), TruncateForLog(filename).c_str());
4384 }
4385 32 BumpStat(stats->pktError);
4386 32 return;
4387 }
4388 // A NaN passes every "< 0" test and the int conversions downstream are
4389 // undefined for it, as for anything past INT_MAX milliseconds.
4390
6/6
✓ Branch 0 taken 2177 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 2171 times.
✓ Branch 4 taken 14 times.
✓ Branch 5 taken 2171 times.
2185 if (!std::isfinite(secondsIn) || secondsIn < 0.0f) {
4391 14 secondsIn = 0.0f;
4392
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 2126 times.
2171 } else if (secondsIn > 86400.0f) {
4393 45 secondsIn = 86400.0f;
4394 }
4395 // The same bound for the frame number, at the fastest frame rate (1 ms):
4396 // it is unsigned on the wire and an int from here on, so 2^31 and up went
4397 // negative, and the frameNumber - 1 downstream overflowed at INT_MIN.
4398 2185 uint32_t wireFrame = spkt->frameNumber;
4399 2185 const int frameNumber = (int)std::min<uint32_t>(wireFrame, 86400u * 1000u);
4400
4401 2185 float secondsElapsed = 0.0;
4402
4403
2/2
✓ Branch 0 taken 983 times.
✓ Branch 1 taken 1202 times.
2185 if (spkt->fileType == SYNC_FILE_SEQ) {
4404
5/5
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 43 times.
✓ Branch 2 taken 29 times.
✓ Branch 3 taken 858 times.
✓ Branch 4 taken 5 times.
983 switch (spkt->pktType) {
4405 48 case SYNC_PKT_OPEN:
4406
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 OpenSyncedSequence(filename);
4407 48 BumpStat(stats->pktSyncSeqOpen);
4408 48 break;
4409 43 case SYNC_PKT_START:
4410
1/2
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
43 StartSyncedSequence(filename);
4411 43 BumpStat(stats->pktSyncSeqStart);
4412 43 break;
4413 29 case SYNC_PKT_STOP:
4414
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 StopSyncedSequence(filename);
4415 29 BumpStat(stats->pktSyncSeqStop);
4416 29 break;
4417 858 case SYNC_PKT_SYNC:
4418 858 secondsElapsed = secondsIn - m_remoteOffset;
4419
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 858 times.
858 if (secondsElapsed < 0)
4420 ✗ secondsElapsed = 0.0;
4421
4422
1/2
✓ Branch 0 taken 858 times.
✗ Branch 1 not taken.
858 SyncSyncedSequence(filename,
4423 frameNumber, secondsElapsed);
4424 858 BumpStat(stats->pktSyncSeqSync);
4425 858 break;
4426 }
4427
2/2
✓ Branch 0 taken 1180 times.
✓ Branch 1 taken 22 times.
1202 } else if (spkt->fileType == SYNC_FILE_MEDIA) {
4428
5/5
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 21 times.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 1114 times.
✓ Branch 4 taken 3 times.
1180 switch (spkt->pktType) {
4429 22 case SYNC_PKT_OPEN:
4430
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 OpenSyncedMedia(filename);
4431 22 BumpStat(stats->pktSyncMedOpen);
4432 22 break;
4433 21 case SYNC_PKT_START:
4434 21 secondsElapsed = secondsIn - m_remoteOffset;
4435
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (secondsElapsed < 0)
4436 ✗ secondsElapsed = 0.0;
4437
4438
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 StartSyncedMedia(filename, secondsElapsed);
4439 21 BumpStat(stats->pktSyncMedStart);
4440 21 break;
4441 20 case SYNC_PKT_STOP:
4442
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 StopSyncedMedia(filename);
4443 20 BumpStat(stats->pktSyncMedStop);
4444 20 break;
4445 1114 case SYNC_PKT_SYNC:
4446 1114 secondsElapsed = secondsIn - m_remoteOffset;
4447
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1114 times.
1114 if (secondsElapsed < 0)
4448 ✗ secondsElapsed = 0.0;
4449
4450
1/2
✓ Branch 0 taken 1114 times.
✗ Branch 1 not taken.
1114 SyncSyncedMedia(filename,
4451 frameNumber, secondsElapsed);
4452 1114 BumpStat(stats->pktSyncMedSync);
4453 1114 break;
4454 }
4455 }
4456
2/2
✓ Branch 0 taken 2185 times.
✓ Branch 1 taken 32 times.
2217 }
4457
4458 /*
4459 *
4460 */
4461 31 void MultiSync::ProcessCommandPacket(ControlPkt* pkt, int len, MultiSyncStats* stats) {
4462
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 LogDebug(VB_SYNC, "ProcessCommandPacket()\n");
4463
4464
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 25 times.
31 if (pkt->extraDataLen < sizeof(CommandPkt)) {
4465
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
6 if (ShouldLogBadPacket()) {
4466
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SYNC, "Error: Invalid length of received command packet\n");
4467
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 HexDump("Received data:", (void*)pkt, len, VB_SYNC);
4468 }
4469 6 BumpStat(stats->pktError);
4470 6 return;
4471 }
4472
4473 25 BumpStat(stats->pktCommand);
4474
4475 25 CommandPkt* cpkt = (CommandPkt*)(((char*)pkt) + sizeof(ControlPkt));
4476
4477 // CommandPkt is a flexible char[1] with no length field of its own, so the
4478 // command text is only as terminated as the sender chose to make it. Bound
4479 // the read by the packet's own extraDataLen before handing it to the log
4480 // (which is remote-supplied and unbounded, hence TruncateForLog too).
4481
3/6
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
50 LogDebug(VB_COMMAND, "Legacy command \"%s\" received from remote host %s (%s)\n",
4482 TruncateForLog(std::string(cpkt->command, strnlen(cpkt->command, pkt->extraDataLen))).c_str(),
4483 stats->hostname.c_str(), stats->sourceIP.c_str());
4484
4485 char response[1500];
4486
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 char* r2 = ProcessCommand(cpkt->command, response);
4487
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
25 if (r2) {
4488 ✗ free(r2);
4489 }
4490 }
4491
4492 /*
4493 *
4494 */
4495 9045 void MultiSync::ProcessPingPacket(ControlPkt* pkt, int len, const std::string& srcIp, MultiSyncStats* stats, const std::string& incomingIp) {
4496
1/2
✓ Branch 0 taken 9045 times.
✗ Branch 1 not taken.
9045 LogDebug(VB_SYNC, "ProcessPingPacket()\n");
4497
4498
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 9003 times.
9045 if (pkt->extraDataLen < 169) { // v1 packet length
4499
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 42 times.
42 if (ShouldLogBadPacket()) {
4500 ✗ LogErr(VB_SYNC, "ERROR: Invalid length of received Ping packet\n");
4501 ✗ HexDump("Received data:", (void*)pkt, len, VB_SYNC);
4502 }
4503 42 BumpStat(stats->pktError);
4504 156 return;
4505 }
4506
4507 9003 unsigned char* extraData = (unsigned char*)(((char*)pkt) + sizeof(ControlPkt));
4508
4509 9003 unsigned char pingVersion = extraData[0];
4510
4511
4/4
✓ Branch 0 taken 72 times.
✓ Branch 1 taken 8931 times.
✓ Branch 2 taken 66 times.
✓ Branch 3 taken 6 times.
9003 if ((pingVersion == 1) && (pkt->extraDataLen > 169)) {
4512
2/4
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 66 times.
66 if (ShouldLogBadPacket()) {
4513 ✗ LogErr(VB_SYNC, "ERROR: Ping v1 packet too long: %d\n", pkt->extraDataLen);
4514 ✗ HexDump("Received data:", (void*)pkt, len, VB_SYNC);
4515 }
4516 66 BumpStat(stats->pktError);
4517 66 return;
4518 }
4519
4/4
✓ Branch 0 taken 72 times.
✓ Branch 1 taken 8865 times.
✓ Branch 2 taken 48 times.
✓ Branch 3 taken 24 times.
8937 if ((pingVersion == 2) && (pkt->extraDataLen > 214)) {
4520
2/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 48 times.
48 if (ShouldLogBadPacket()) {
4521 ✗ LogErr(VB_SYNC, "ERROR: Ping v2 packet too long %d\n", pkt->extraDataLen);
4522 ✗ HexDump("Received data:", (void*)pkt, len, VB_SYNC);
4523 }
4524 48 BumpStat(stats->pktError);
4525 48 return;
4526 }
4527 8889 int discover = extraData[1];
4528
4529 8889 MultiSyncSystemType type = (MultiSyncSystemType)extraData[2];
4530
4531 8889 unsigned int majorVersion = ((unsigned int)extraData[3] << 8) | extraData[4];
4532 8889 unsigned int minorVersion = ((unsigned int)extraData[5] << 8) | extraData[6];
4533
4534 8889 FPPMode systemMode = (FPPMode)extraData[7];
4535
4536 8889 bool isInstance = true;
4537 8889 std::string address;
4538
5/8
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 8881 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
8889 if (extraData[8] == 0 && extraData[9] == 0 && extraData[10] == 0 && extraData[11] == 0) {
4539
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 times.
8 if (discover) {
4540 // No ip address in packet, this is a ping/discovery packet
4541 // from something (xLights?) that is just trying to
4542 // get a list of FPP instances, we won't record this
4543 7 isInstance = false;
4544
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 address = "0.0.0.0";
4545
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 } else if (!incomingIp.empty()) {
4546
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 address = incomingIp;
4547 } else {
4548 ✗ isInstance = false;
4549 ✗ address = "0.0.0.0";
4550 }
4551 } else {
4552 char addrStr[16];
4553 8881 memset(addrStr, 0, sizeof(addrStr));
4554 8881 snprintf(addrStr, 16, "%d.%d.%d.%d", extraData[8], extraData[9],
4555 8881 extraData[10], extraData[11]);
4556
1/2
✓ Branch 0 taken 8881 times.
✗ Branch 1 not taken.
8881 address = addrStr;
4557 }
4558
4559 char tmpStr[128];
4560 // Bounded copy of a packet string field. The packet tail is not guaranteed
4561 // to be NUL-terminated and may be longer than tmpStr, so clamp to both the
4562 // field's own size and the destination buffer to avoid a stack overflow /
4563 // over-read.
4564 44155 auto copyField = [&](int fieldOffset) -> std::string {
4565 44155 int avail = (int)pkt->extraDataLen - fieldOffset;
4566
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44155 times.
44155 if (avail < 0) {
4567 ✗ avail = 0;
4568 }
4569 44155 int toCopy = std::min(avail, (int)(sizeof(tmpStr) - 1));
4570 44155 memset(tmpStr, 0, sizeof(tmpStr));
4571 44155 memcpy(tmpStr, (char*)(extraData + fieldOffset), toCopy);
4572
1/2
✓ Branch 0 taken 44155 times.
✗ Branch 1 not taken.
88310 return std::string(tmpStr);
4573 8889 };
4574
1/2
✓ Branch 0 taken 8889 times.
✗ Branch 1 not taken.
8889 std::string hostname = copyField(12);
4575
1/2
✓ Branch 0 taken 8889 times.
✗ Branch 1 not taken.
8889 std::string version = copyField(77);
4576
1/2
✓ Branch 0 taken 8889 times.
✗ Branch 1 not taken.
8889 std::string typeStr = copyField(118);
4577 // End of v1 packet fields
4578
4579 8889 std::string ranges;
4580
2/2
✓ Branch 0 taken 8853 times.
✓ Branch 1 taken 36 times.
8889 if ((pkt->extraDataLen) > 169) {
4581
1/2
✓ Branch 0 taken 8853 times.
✗ Branch 1 not taken.
8853 ranges = copyField(166 - 7);
4582 }
4583
4584 8889 bool isLocal = false;
4585 {
4586 // Only m_interfaces needs m_socketLock; released before UpdateSystem()
4587 // below so it isn't held across a call that can re-enter m_socketLock
4588 // via UpdateUnicastDestinations() (m_socketLock is non-recursive).
4589
1/2
✓ Branch 0 taken 8889 times.
✗ Branch 1 not taken.
8889 std::unique_lock<std::mutex> lock(m_socketLock);
4590
2/2
✓ Branch 0 taken 9711 times.
✓ Branch 1 taken 8889 times.
18600 for (auto& a : m_interfaces) {
4591
2/2
✓ Branch 0 taken 4237 times.
✓ Branch 1 taken 5474 times.
9711 if (address == a.second.interfaceAddress) {
4592 4237 isLocal = true;
4593 }
4594 }
4595 8889 }
4596
4597 // v4: the sender's SystemUUID, so every address a device pings from can be
4598 // tied to the one device without an HTTP round trip. Gated on length
4599 // rather than version, so anything that appends further still carries it.
4600 8889 std::string pktUUID;
4601
2/2
✓ Branch 0 taken 8635 times.
✓ Branch 1 taken 254 times.
8889 if (pkt->extraDataLen >= PING_V4_LEN) {
4602
2/4
✓ Branch 0 taken 8635 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8635 times.
✗ Branch 3 not taken.
8635 pktUUID = copyField(PING_V3_LEN).substr(0, PING_UUID_MAX);
4603
3/4
✓ Branch 0 taken 8635 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 8628 times.
13735 if (std::any_of(pktUUID.begin(), pktUUID.end(), [](unsigned char c) { return !isgraph(c); })) {
4604 7 pktUUID.clear();
4605 }
4606 }
4607
4608
2/2
✓ Branch 0 taken 8882 times.
✓ Branch 1 taken 7 times.
8889 if (isInstance) {
4609
9/14
✓ Branch 0 taken 4237 times.
✓ Branch 1 taken 4645 times.
✓ Branch 2 taken 4237 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4559 times.
✓ Branch 5 taken 86 times.
✓ Branch 6 taken 4559 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 86 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4559 times.
✓ Branch 11 taken 4323 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
13441 std::string uuid = isLocal ? getSetting("SystemUUID") : (pktUUID.empty() ? "Unknown" : pktUUID);
4610 8882 multiSync->UpdateSystem(type, majorVersion, minorVersion,
4611 systemMode, address, hostname, version,
4612 typeStr, ranges, uuid, true,
4613
1/2
✓ Branch 0 taken 8882 times.
✗ Branch 1 not taken.
8882 systemMode & 0x04 ? true : false);
4614
4615 // A discover ping is what an instance sends as its fppd starts (fppd.cpp
4616 // calls Discover() during startup), and a restart is exactly when most
4617 // of what we cache about a peer changes -- channel inputs/outputs, host
4618 // description, background color, anything else that needed a restart to
4619 // take effect. Clearing the fetch timestamp makes the next sweep re-ask
4620 // within INFO_SCAN_INTERVAL instead of serving up to
4621 // INFO_REFRESH_INTERVAL of stale detail. The cached values themselves
4622 // are left in place so the UI doesn't blank out mid-refetch.
4623 //
4624 // This cannot amplify into a fetch storm: a discover ping is only ever
4625 // answered with plain (non-discover) pings, so one restart invalidates
4626 // one system on each peer. xLights' discovery ping is also a discover
4627 // ping, but it carries no address and so never reaches this branch.
4628
4/4
✓ Branch 0 taken 2040 times.
✓ Branch 1 taken 6842 times.
✓ Branch 2 taken 64 times.
✓ Branch 3 taken 1976 times.
8882 if (discover && !isLocal) {
4629
1/2
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
64 multiSync->InvalidateSystemInfo(address);
4630 }
4631 8882 }
4632
2/2
✓ Branch 0 taken 2047 times.
✓ Branch 1 taken 6842 times.
8889 if (discover) {
4633
6/6
✓ Branch 0 taken 71 times.
✓ Branch 1 taken 1976 times.
✓ Branch 2 taken 70 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 70 times.
✓ Branch 5 taken 1977 times.
2047 if ((hostname != m_hostname) && !isLocal) {
4634
1/2
✓ Branch 0 taken 70 times.
✗ Branch 1 not taken.
70 QueueDiscoverReply(address, srcIp);
4635 }
4636 }
4637
4638 8889 BumpStat(stats->pktPing);
4639 8889 }
4640
4641 28 void MultiSync::ProcessPluginPacket(ControlPkt* pkt, int plen, MultiSyncStats* stats) {
4642 28 LogDebug(VB_SYNC, "ProcessPluginPacket()\n");
4643 28 CommandPkt* cpkt = (CommandPkt*)(((char*)pkt) + sizeof(ControlPkt));
4644 28 int len = pkt->extraDataLen;
4645 28 char* pn = &cpkt->command[0];
4646 // The plugin name must be NUL-terminated within the extra data. An
4647 // unterminated name would otherwise run strlen() out to the terminator the
4648 // receive loop writes one byte past the datagram, making nlen larger than
4649 // the data actually present and leaving a negative len for the plugins --
4650 // which they take as a size_t and read far past the buffer.
4651 28 int nlen = (int)strnlen(pn, (size_t)len);
4652
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 4 times.
28 if (nlen >= len) {
4653
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (ShouldLogBadPacket()) {
4654 ✗ LogErr(VB_SYNC, "Error: plugin packet name is not terminated within %d bytes of data\n", len);
4655 }
4656 24 BumpStat(stats->pktError);
4657 24 return;
4658 }
4659 4 nlen++; // include the terminator
4660 4 len -= nlen;
4661 4 uint8_t* data = (uint8_t*)&cpkt->command[nlen];
4662
4663
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
4 for (auto a : getPluginsCopy()) {
4664 ✗ a->ReceivedPluginData(pn, data, len);
4665 4 }
4666
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 PluginManager::INSTANCE.multiSyncData(pn, data, len);
4667
4668 4 BumpStat(stats->pktPlugin);
4669 }
4670
4671 5 static bool MyHostMatches(const std::string& host, const std::string& hostName, std::vector<MultiSyncSystem>& localSystems) {
4672
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::vector<std::string> names = split(host, ',');
4673
3/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 3 times.
5 if (std::find(names.begin(), names.end(), hostName) != names.end()) {
4674 2 return true;
4675 }
4676
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 for (auto& h : names) {
4677
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 for (auto& ls : localSystems) {
4678
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
3 if (h == ls.address || h == ls.hostname) {
4679 ✗ return true;
4680 }
4681 }
4682 }
4683 3 return false;
4684 5 }
4685
4686 59 void MultiSync::ProcessFPPCommandPacket(ControlPkt* pkt, int len, MultiSyncStats* stats) {
4687 // The caller has already checked len == sizeof(ControlPkt) + extraDataLen,
4688 // so the packet's own data ends at `end`. Every field below has to be
4689 // parsed strictly against that bound: a truncated or crafted packet whose
4690 // strings are not terminated inside it would otherwise be read out of the
4691 // stale contents of this reusable receive buffer -- and, for the last row of
4692 // that array, past the end of it -- and the result handed to CommandManager.
4693 59 const char* b = (const char*)pkt;
4694 59 const char* end = b + sizeof(ControlPkt) + pkt->extraDataLen;
4695 59 const char* pos = b + sizeof(ControlPkt);
4696
4697 1815 auto readString = [&pos, end](std::string& out) {
4698
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1803 times.
1815 if (pos >= end) {
4699 12 return false;
4700 }
4701 1803 size_t avail = (size_t)(end - pos);
4702 1803 size_t slen = strnlen(pos, avail);
4703
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 1779 times.
1803 if (slen == avail) {
4704 // ran to the end of the packet without finding a terminator
4705 24 return false;
4706 }
4707 1779 out.assign(pos, slen);
4708 1779 pos += slen + 1;
4709 1779 return true;
4710 59 };
4711
4712
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 53 times.
59 if (pos >= end) {
4713
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (ShouldLogBadPacket()) {
4714 ✗ LogErr(VB_SYNC, "Error: truncated FPP Command packet\n");
4715 }
4716 6 BumpStat(stats->pktError);
4717 42 return;
4718 }
4719 // Unsigned: as a signed char an argument count above 127 reads as negative
4720 // and silently skips the whole argument list.
4721 53 uint8_t numArgs = (uint8_t)*pos++;
4722
4723 53 std::string host;
4724 53 std::string cmd;
4725
8/10
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✓ Branch 3 taken 18 times.
✓ Branch 4 taken 35 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 29 times.
✓ Branch 8 taken 24 times.
✓ Branch 9 taken 29 times.
53 if (!readString(host) || !readString(cmd)) {
4726
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
24 if (ShouldLogBadPacket()) {
4727 ✗ LogErr(VB_SYNC, "Error: truncated FPP Command packet\n");
4728 }
4729 24 BumpStat(stats->pktError);
4730 24 return;
4731 }
4732
4733 29 std::vector<std::string> args;
4734
2/2
✓ Branch 0 taken 1727 times.
✓ Branch 1 taken 17 times.
1744 for (uint8_t x = 0; x < numArgs; x++) {
4735 1727 std::string arg;
4736
3/4
✓ Branch 0 taken 1727 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 1715 times.
1727 if (!readString(arg)) {
4737
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 12 times.
12 if (ShouldLogBadPacket()) {
4738 ✗ LogErr(VB_SYNC, "Error: FPP Command packet claims %d arguments but only holds %d\n",
4739 (int)numArgs, (int)args.size());
4740 }
4741 12 BumpStat(stats->pktError);
4742 12 return;
4743 }
4744
1/2
✓ Branch 0 taken 1715 times.
✗ Branch 1 not taken.
1715 args.push_back(std::move(arg));
4745
2/2
✓ Branch 0 taken 1715 times.
✓ Branch 1 taken 12 times.
1727 }
4746 bool matches;
4747
3/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 4 times.
17 if (host == "") {
4748 13 matches = true;
4749 } else {
4750 // Copy m_localSystems under m_systemsLock and release it before
4751 // evaluating/running -- MyHostMatches only needs a snapshot, and this
4752 // function must not hold m_systemsLock across CommandManager::run() or
4753 // the plugin callbacks below.
4754
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
4755
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::vector<MultiSyncSystem> localSystems = m_localSystems;
4756
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 lock.unlock();
4757
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 matches = MyHostMatches(host, m_hostname, localSystems);
4758 4 }
4759
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 2 times.
17 if (matches) {
4760 15 BumpStat(stats->pktFPPCommand);
4761 // Remote-supplied and unbounded - truncate. (The args are covered by
4762 // CommandManager::run()'s own log line below.)
4763
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 LogDebug(VB_COMMAND, "Command \"%s\" received from remote host %s (%s)\n",
4764 TruncateForLog(cmd).c_str(), stats->hostname.c_str(), stats->sourceIP.c_str());
4765
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 15 times.
15 for (auto a : getPluginsCopy()) {
4766 ✗ a->ReceivedFPPCommandPacket(cmd, args);
4767 15 }
4768
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 CommandManager::INSTANCE.run(cmd, args);
4769 }
4770
6/6
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 36 times.
✓ Branch 4 taken 17 times.
✓ Branch 5 taken 36 times.
101 }
4771
4772 8 void MultiSync::SendFPPCommandPacket(const std::string& host, const std::string& cmd, const std::vector<std::string>& args) {
4773
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m_controlSock < 0) {
4774 ✗ OpenControlSockets();
4775 }
4776
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m_controlSock < 0) {
4777 ✗ OpenControlSockets();
4778 ✗ LogErr(VB_SYNC, "ERROR: Tried to send FPP Command packet but sync socket is not open.\n");
4779 1 return;
4780 }
4781
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogDebug(VB_SYNC, "SendFPPCommandPacket\n");
4782
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 for (auto a : getPluginsCopy()) {
4783 ✗ a->SendFPPCommandPacket(host, cmd, args);
4784 8 }
4785 // The argument count goes out as one byte, so more than 255 would wrap and
4786 // the remote would read a different command than this host runs.
4787
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 if (args.size() > 255) {
4788
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogErr(VB_SYNC, "ERROR: FPP Command \"%s\" has %d arguments, more than a MultiSync packet can carry\n",
4789 TruncateForLog(cmd).c_str(), (int)args.size());
4790 1 return;
4791 }
4792 // Payload: arg count, then host, cmd and each arg NUL-terminated. Sized
4793 // up front so a command too large for one packet is refused as a whole
4794 // rather than sent truncated (or dropped by every receiver) while this
4795 // host runs it in full.
4796 7 size_t payloadLen = 1 + host.length() + 1 + cmd.length() + 1;
4797
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 7 times.
44 for (auto& a : args) {
4798 37 payloadLen += a.length() + 1;
4799 }
4800 7 const bool fits = payloadLen <= MAX_MS_PAYLOAD;
4801
4802 char outBuf[2048];
4803 static_assert(sizeof(ControlPkt) + MAX_MS_PAYLOAD <= sizeof(outBuf), "outBuf must hold the largest packet");
4804 7 bzero(outBuf, sizeof(outBuf));
4805
4806 7 ControlPkt* cpkt = (ControlPkt*)outBuf;
4807
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 InitControlPacket(cpkt);
4808 7 cpkt->pktType = CTRL_PKT_FPPCOMMAND;
4809
4810 7 int pos = sizeof(ControlPkt);
4811
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
7 if (fits) {
4812 42 auto appendField = [&](const std::string& v) {
4813 42 memcpy(&outBuf[pos], v.c_str(), v.length() + 1);
4814 42 pos += v.length() + 1;
4815 48 };
4816 6 outBuf[pos++] = (unsigned char)args.size();
4817 6 appendField(host);
4818 6 appendField(cmd);
4819
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 6 times.
36 for (auto& a : args) {
4820 30 appendField(a);
4821 }
4822 }
4823 7 cpkt->extraDataLen = pos - sizeof(ControlPkt);
4824
4825 // SendFPPCommandPacket can run on API/command threads, so m_localSystems
4826 // must not be read without m_systemsLock; take a snapshot for both
4827 // MyHostMatches() calls below rather than locking per-call.
4828 7 std::vector<MultiSyncSystem> localSystems;
4829 {
4830
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::unique_lock<std::recursive_mutex> lock(m_systemsLock);
4831
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 localSystems = m_localSystems;
4832 7 }
4833
4834
6/8
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 6 times.
7 if (host != "" && host.find(",") == std::string::npos) {
4835
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (MyHostMatches(host, m_hostname, localSystems)) {
4836 // only targetting myself, just run and don't send the packet
4837 ✗ LogDebug(VB_COMMAND, "Command \"%s\" self-targeted, running locally without sending\n", cmd.c_str());
4838 ✗ CommandManager::INSTANCE.run(cmd, args);
4839
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 } else if (!fits) {
4840 ✗ LogErr(VB_SYNC, "ERROR: FPP Command \"%s\" (%d bytes) is too large for a MultiSync packet, not sent\n",
4841 TruncateForLog(cmd).c_str(), (int)payloadLen);
4842 } else {
4843
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 SendUnicastPacket(host, outBuf, pos);
4844 }
4845
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
6 } else if (!fits) {
4846
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogErr(VB_SYNC, "ERROR: FPP Command \"%s\" (%d bytes) is too large for a MultiSync packet, not sent or run\n",
4847 TruncateForLog(cmd).c_str(), (int)payloadLen);
4848 } else {
4849
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 SendControlPacket(outBuf, pos);
4850 // Run it here directly: multicast and unicast don't loop back, and the
4851 // broadcast copies that do are dropped as our own (IsOwnEcho).
4852
3/10
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
5 if (host == "" || MyHostMatches(host, m_hostname, localSystems)) {
4853
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogDebug(VB_COMMAND, "Command \"%s\" broadcast includes this host, running locally too\n", cmd.c_str());
4854
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 CommandManager::INSTANCE.run(cmd, args);
4855 }
4856 }
4857 7 }
4858
4859 19274 MultiSyncStats::MultiSyncStats(std::string ip, std::string host) :
4860 19274 sourceIP(ip),
4861
1/2
✓ Branch 0 taken 19274 times.
✗ Branch 1 not taken.
19274 hostname(host),
4862 19274 pktCommand(0),
4863 19274 pktSyncSeqOpen(0),
4864 19274 pktSyncSeqStart(0),
4865 19274 pktSyncSeqStop(0),
4866 19274 pktSyncSeqSync(0),
4867 19274 pktSyncMedOpen(0),
4868 19274 pktSyncMedStart(0),
4869 19274 pktSyncMedStop(0),
4870 19274 pktSyncMedSync(0),
4871 19274 pktBlank(0),
4872 19274 pktPing(0),
4873 19274 pktPlugin(0),
4874 19274 pktFPPCommand(0),
4875 19274 pktError(0) {
4876 19274 lastReceiveTime = time(NULL);
4877 19274 }
4878
4879 2130 Json::Value MultiSyncStats::toJSON() {
4880
1/2
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
2130 Json::Value result;
4881
4882
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["sourceIP"] = sourceIP;
4883
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["hostname"] = hostname;
4884
4885 char timeStr[34];
4886 2130 memset(timeStr, 0, sizeof(timeStr));
4887 struct tm tm;
4888 2130 localtime_r(&lastReceiveTime, &tm);
4889 2130 snprintf(timeStr, sizeof(timeStr), "%4d-%.2d-%.2d %.2d:%.2d:%.2d",
4890 2130 1900 + tm.tm_year, tm.tm_mon + 1, tm.tm_mday,
4891 tm.tm_hour, tm.tm_min, tm.tm_sec);
4892
4893
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["lastReceiveTime"] = timeStr;
4894
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktCommand"] = __atomic_load_n(&pktCommand, __ATOMIC_RELAXED);
4895
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktSyncSeqOpen"] = __atomic_load_n(&pktSyncSeqOpen, __ATOMIC_RELAXED);
4896
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktSyncSeqStart"] = __atomic_load_n(&pktSyncSeqStart, __ATOMIC_RELAXED);
4897
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktSyncSeqStop"] = __atomic_load_n(&pktSyncSeqStop, __ATOMIC_RELAXED);
4898
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktSyncSeqSync"] = __atomic_load_n(&pktSyncSeqSync, __ATOMIC_RELAXED);
4899
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktSyncMedOpen"] = __atomic_load_n(&pktSyncMedOpen, __ATOMIC_RELAXED);
4900
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktSyncMedStart"] = __atomic_load_n(&pktSyncMedStart, __ATOMIC_RELAXED);
4901
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktSyncMedStop"] = __atomic_load_n(&pktSyncMedStop, __ATOMIC_RELAXED);
4902
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktSyncMedSync"] = __atomic_load_n(&pktSyncMedSync, __ATOMIC_RELAXED);
4903
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktBlank"] = __atomic_load_n(&pktBlank, __ATOMIC_RELAXED);
4904
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktPing"] = __atomic_load_n(&pktPing, __ATOMIC_RELAXED);
4905
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktPlugin"] = __atomic_load_n(&pktPlugin, __ATOMIC_RELAXED);
4906
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktFPPCommand"] = __atomic_load_n(&pktFPPCommand, __ATOMIC_RELAXED);
4907
2/4
✓ Branch 0 taken 2130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2130 times.
✗ Branch 3 not taken.
2130 result["pktError"] = __atomic_load_n(&pktError, __ATOMIC_RELAXED);
4908
4909 2130 return result;
4910 ✗ }
4911