GCC Code Coverage Report


Directory: ./
File: fppd.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 1026 1251 82.0%
Functions: 50 59 84.7%
Branches: 1054 2091 50.4%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the GPL v2 as described in the
10 * included LICENSE.GPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #ifdef PLATFORM_OSX
18 #include <sys/event.h>
19 #define USE_KQUEUE
20 #include "MacOSApp.h"
21 #else
22 #include <sys/epoll.h>
23 #include <sys/prctl.h>
24 #include <sys/sysinfo.h>
25 #include <sys/uio.h>
26 #include <syscall.h>
27 #endif
28
29 #include <Magick++/Image.h>
30 #include <curl/curl.h>
31 #include <magick/magick.h>
32 #include <sys/stat.h>
33 #include <sys/wait.h>
34 #include <atomic>
35 #include <chrono>
36 #include <cstdlib>
37 #include <ctime>
38 #include <errno.h>
39 #include <execinfo.h>
40 #include <fcntl.h>
41 #include <algorithm>
42 #include <filesystem>
43 #include <vector>
44 #include <fstream>
45 #include <functional>
46 #include <getopt.h>
47 #include <iostream>
48 #include <map>
49 #include <set>
50 #include <signal.h>
51 #include <stdio.h>
52 #include <string.h>
53 #include <string>
54 #include <thread>
55 #include <unistd.h>
56 #include <utility>
57
58 #include "common.h"
59 #include "log.h"
60 #include "mqtt.h"
61 #include "settings.h"
62
63 #include "CapeLicenseNotify.h"
64 #include "CurlManager.h"
65 #include "EPollManager.h"
66 #include "Events.h"
67 #include "FileMonitor.h"
68 #include "FPPLocale.h"
69 #include "MultiSync.h"
70 #include "NetworkMonitor.h"
71 #include "OutputMonitor.h"
72 #include "RecurringTasks.h"
73 #include "Player.h"
74 #include "Plugins.h"
75 #include "Scheduler.h"
76 #include "Sequence.h"
77 #include "Timers.h"
78 #include "Variables.h"
79 #include "Warnings.h"
80 #include "command.h"
81 #include "e131bridge.h"
82 #include "effects.h"
83 #include "fppversion.h"
84 #include "gpio.h"
85 #include "httpAPI.h"
86 #include "ping.h"
87 #include "channeloutput/ChannelOutputSetup.h"
88 #include "channeloutput/channeloutputthread.h"
89 #include "channeltester/ChannelTester.h"
90 #include "commands/Commands.h"
91 #include "mediaoutput/AES67Manager.h"
92 #include "mediaoutput/AudioLevelMonitor.h"
93 #include "mediaoutput/OpusRTPManager.h"
94 #include "mediaoutput/RTSPOutputManager.h"
95 #include "mediaoutput/MediaOutputBase.h"
96 #include "mediaoutput/MediaOutputStatus.h"
97 #include "mediaoutput/VideoInputManager.h"
98 #include "mediaoutput/VideoOutputManager.h"
99 #include "mediaoutput/mediaoutput.h"
100 #include "overlays/PixelOverlay.h"
101 #include "playlist/Playlist.h"
102 #include "sensors/Sensors.h"
103 #include "util/GPIOUtils.h"
104 #include "util/TmpFileGPIO.h"
105
106 #include "MDNSManager.h"
107 #include "WLEDAPIResponder.h"
108 #include "WLEDAudioSync.h"
109
110 #include "fppd.h"
111
112 #if defined(PLATFORM_BBB) || defined(PLATFORM_BB64)
113 #include "util/BBBUtils.h"
114 #define PLAT_GPIO_CLASS BBBPinProvider
115 #elif defined(PLATFORM_PI)
116 #include "util/PiGPIOUtils.h"
117 #define PLAT_GPIO_CLASS PiGPIOPinProvider
118 #elif defined(GPIOD_CHIPS) && GPIOD_CHIPS > 0
119 #define PLAT_GPIO_CLASS NoPinCapabilitiesProvider
120 #else
121 #include "util/TmpFileGPIO.h"
122 #define PLAT_GPIO_CLASS TmpFilePinProvider
123 #endif
124
125 pid_t pid, sid;
126 // Cleared by the SIGINT/SIGTERM handler (httpAPI.cpp), which may only touch a
127 // lock-free atomic, and read by the main loop and its watchdog thread.
128 std::atomic_int runMainFPPDLoop{ 1 };
129 static_assert(std::atomic_int::is_always_lock_free, "the signal handler stores to runMainFPPDLoop");
130 volatile bool restartFPPD = 0;
131
132 /* Prototypes for functions below */
133 void MainLoop(void);
134 void PublishStatsBackground(std::string reason);
135 void PublishStatsForce(std::string reason);
136
137 20 static int IsDebuggerPresent() {
138 char buf[1024];
139 20 int debugger_present = 0;
140
141
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 int status_fd = open("/proc/self/status", O_RDONLY);
142
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (status_fd == -1) {
143 ✗ return 0;
144 }
145
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 ssize_t num_read = read(status_fd, buf, sizeof(buf) - 1);
146
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (num_read > 0) {
147 static const char TracerPid[] = "TracerPid:";
148 char* tracer_pid;
149
150 20 buf[num_read] = 0;
151 20 tracer_pid = strstr(buf, TracerPid);
152
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (tracer_pid) {
153 20 debugger_present = !!atoi(tracer_pid + sizeof(TracerPid) - 1);
154 }
155 }
156 20 return debugger_present;
157 }
158
159 // Try to attach gdb to print stack trace (Linux only).
160 // The sole purpose is to improve the very poor stack traces generated by backtrace() on ARM platforms
161 // Sets *reason to a short constant describing why no gdb stack was produced.
162 // Recorded in the crash report because "symbol-only stack, no line numbers" is
163 // otherwise indistinguishable between causes -- gdb missing, gdb timing out,
164 // ptrace refused, or fork() failing under memory pressure all look identical
165 // downstream, and they call for completely different remedies.
166 20 static bool dumpstack_gdb(const char** reason = nullptr) {
167 // The watchdog callers only want the side effect of dumping a stack; only
168 // the crash handler records why it failed.
169 20 const char* ignored = nullptr;
170
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 18 times.
20 if (!reason) {
171 2 reason = &ignored;
172 }
173 20 *reason = "unknown";
174 #ifndef PLATFORM_OSX
175 char pid_buf[30];
176 20 snprintf(pid_buf, sizeof(pid_buf), "%d", getpid());
177 char thread_buf[30];
178 20 snprintf(thread_buf, sizeof(thread_buf), "(LWP %ld)", syscall(__NR_gettid));
179 char name_buf[512];
180 20 ssize_t nLink = readlink("/proc/self/exe", name_buf, sizeof(name_buf) - 1);
181
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 if (nLink > 0 && nLink < (ssize_t)sizeof(name_buf)) {
182 20 name_buf[nLink] = '\0';
183 } else {
184 ✗ name_buf[0] = '\0';
185 }
186
187 constexpr static int kTimeoutSec = 30;
188
189
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 20 times.
20 if (IsDebuggerPresent()) {
190 ✗ *reason = "a debugger is already attached";
191 ✗ return false;
192 }
193 20 (void)remove("/tmp/fppd_crash.log");
194
195 // Allow us to be traced
196 // Note: Does not currently work in WSL: https://github.com/Microsoft/WSL/issues/3053
197 20 int retval = prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
198 20 const bool ptraceSetFailed = (retval != 0);
199
200 // Spawn helper process which will keep running when gdb is attached to main domoticz process
201 20 pid_t intermediate_pid = fork();
202
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
41 if (intermediate_pid == -1) {
203 // Overwhelmingly ENOMEM on a 480MB BeagleBone, or a crash that left the
204 // process unable to fork.
205 ✗ *reason = "fork() for the gdb helper failed";
206 ✗ return false;
207 }
208
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 20 times.
41 if (!intermediate_pid) {
209 // Wathchdog 1: Used to kill sub processes to gdb which may hang
210 21 pid_t timeout_pid1 = fork();
211
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (timeout_pid1 == -1) {
212 ✗ _Exit(1);
213 }
214
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (timeout_pid1 == 0) {
215 ✗ sleep(kTimeoutSec);
216 ✗ _Exit(1);
217 }
218
219 // Wathchdog 2: Give up on gdb, if it still does not finish even after killing its sub processes
220 21 pid_t timeout_pid2 = fork();
221
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (timeout_pid2 == -1) {
222 ✗ kill(timeout_pid1, SIGKILL);
223 ✗ _Exit(1);
224 }
225
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (timeout_pid2 == 0) {
226 ✗ sleep(kTimeoutSec + 10);
227 ✗ _Exit(1);
228 }
229
230 // Worker: Spawns gdb
231 21 pid_t worker_pid = fork();
232
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 43 times.
43 if (worker_pid == -1) {
233 ✗ kill(timeout_pid1, SIGKILL);
234 ✗ kill(timeout_pid2, SIGKILL);
235 ✗ _Exit(1);
236 }
237
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 21 times.
43 if (worker_pid == 0) {
238 22 (void)remove("/tmp/fppd_crash.log");
239
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 int fd = open("/tmp/fppd_crash.log", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
240
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (fd == -1)
241 ✗ _Exit(1);
242
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (dup2(fd, STDOUT_FILENO) == -1)
243 ✗ _Exit(1);
244
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (dup2(fd, STDERR_FILENO) == -1)
245 ✗ _Exit(1);
246 22 execlp("gdb", "gdb", "--batch", "-n", "-ex", "thread apply all bt", "-ex", "info threads", "-ex", "bt", "-ex", "detach", name_buf, pid_buf, NULL);
247
248 // If gdb failed to start, signal back
249
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
22 close(fd);
250 ✗ _Exit(1);
251 }
252
253 21 int result = 1;
254
255 // Wait for all children to die
256
6/6
✓ Branch 0 taken 319 times.
✓ Branch 1 taken 63 times.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 42 times.
✓ Branch 4 taken 21 times.
✓ Branch 5 taken 21 times.
382 while (worker_pid || timeout_pid1 || timeout_pid2) {
257 361 int status = 0;
258 // pid_t exited_pid = wait(&status);
259
1/2
✓ Branch 0 taken 361 times.
✗ Branch 1 not taken.
361 usleep(100000);
260 // printf("pid worker_pid: %d, timeout_pid1: %d, timeout_pid2: %d\n", worker_pid, timeout_pid1, timeout_pid2);
261
7/8
✓ Branch 0 taken 319 times.
✓ Branch 1 taken 42 times.
✓ Branch 2 taken 319 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✓ Branch 5 taken 298 times.
✓ Branch 6 taken 21 times.
✓ Branch 7 taken 340 times.
361 if (worker_pid && waitpid(worker_pid, &status, WNOHANG)) {
262 21 worker_pid = 0;
263 // printf("Status: %x, wifexited: %u, wexitstatus: %u\n", status, WIFEXITED(status), WEXITSTATUS(status));
264 // printf("Sending SIGKILL to timeout_pid1\n");
265
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 if (timeout_pid1)
266 21 kill(timeout_pid1, SIGKILL);
267
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 if (timeout_pid2)
268 21 kill(timeout_pid2, SIGKILL);
269
7/8
✓ Branch 0 taken 319 times.
✓ Branch 1 taken 21 times.
✓ Branch 2 taken 319 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✓ Branch 5 taken 298 times.
✓ Branch 6 taken 21 times.
✓ Branch 7 taken 319 times.
340 } else if (timeout_pid1 && waitpid(timeout_pid1, &status, WNOHANG)) {
270 // Watchdog 1 timed out, attempt to recover by killing all gdb's child processes
271 char tmp[128];
272 21 timeout_pid1 = 0;
273 // printf("Sending SIGKILL to worker_pid's children\n");
274
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (worker_pid) {
275 ✗ snprintf(tmp, sizeof(tmp), "pkill -KILL -P %d", worker_pid);
276 ✗ int ret = system(tmp);
277 }
278
6/8
✓ Branch 0 taken 319 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 319 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✓ Branch 5 taken 298 times.
✓ Branch 6 taken 21 times.
✓ Branch 7 taken 298 times.
319 } else if (timeout_pid2 && waitpid(timeout_pid2, &status, WNOHANG)) {
279 // Watchdog 2 timed out, give up
280 21 timeout_pid2 = 0;
281 // printf("Sending SIGKILL to worker_pid\n");
282
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (worker_pid)
283 ✗ kill(worker_pid, SIGKILL);
284
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (timeout_pid1)
285 ✗ kill(timeout_pid1, SIGKILL);
286 }
287 }
288 21 _Exit(result); // Or some more informative status
289 } else {
290 20 int status = 0;
291
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 pid_t res = waitpid(intermediate_pid, &status, 0);
292
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 if (FileExists("/tmp/fppd_crash.log")) {
293
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 std::string s = GetFileContents("/tmp/fppd_crash.log");
294
3/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✓ Branch 3 taken 1 times.
20 if (s != "") {
295
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 LogErr(VB_ALL, "Stack: \n%s\n", s.c_str());
296 // printf("%s\n", s.c_str());
297 19 return true;
298 }
299 1 *reason = ptraceSetFailed
300
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 ? "gdb produced no output and PR_SET_PTRACER was refused"
301 : "gdb produced no output (not installed, exec failed, or watchdog timeout)";
302
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 19 times.
20 } else {
303 ✗ *reason = ptraceSetFailed
304 ✗ ? "gdb wrote no stack file and PR_SET_PTRACER was refused"
305 : "gdb wrote no stack file (not installed, exec failed, or watchdog timeout)";
306 }
307 }
308 #endif
309 1 return false;
310 }
311
312 36 static const char* safe(const char* in) {
313
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 return in ? in : "<NULL>";
314 }
315
316 // ──────────────────────────────────────────────────────────────────────────
317 // Async-signal-safe crash-report primitives.
318 //
319 // Only write() and pure arithmetic -- no allocation, no locks, no stdio. A
320 // fatal signal that fires while this thread holds the malloc arena lock (a
321 // heap-corruption SIGABRT raised from inside free() always does) turns any
322 // allocating call into a self-deadlock, and that is the same condition most
323 // likely to have made gdb's fork() fail and dropped us onto the fallback path
324 // in the first place. So the machine-readable record is emitted with these
325 // first, and anything that can allocate is attempted only afterwards.
326 // ──────────────────────────────────────────────────────────────────────────
327 1429 static void safeWrite(int fd, const char* str) {
328
2/4
✓ Branch 0 taken 1429 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1429 times.
1429 if (fd < 0 || !str) {
329 ✗ return;
330 }
331 1429 (void)!write(fd, str, strlen(str));
332 }
333
334 138 static void safeWriteHex(int fd, uintptr_t val) {
335 char buf[2 + sizeof(uintptr_t) * 2 + 1];
336 138 char* p = buf + sizeof(buf) - 1;
337 138 *p = '\0';
338
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 136 times.
138 if (val == 0) {
339 2 *--p = '0';
340 }
341
2/2
✓ Branch 0 taken 1275 times.
✓ Branch 1 taken 138 times.
1413 while (val) {
342 1275 *--p = "0123456789abcdef"[val & 0xF];
343 1275 val >>= 4;
344 }
345 138 *--p = 'x';
346 138 *--p = '0';
347
1/2
✓ Branch 0 taken 138 times.
✗ Branch 1 not taken.
138 safeWrite(fd, p);
348 138 }
349
350 90 static void safeWriteReg(int fd, const char* name, uintptr_t val) {
351 90 safeWrite(fd, name);
352 90 safeWrite(fd, "=");
353 90 safeWriteHex(fd, val);
354 90 safeWrite(fd, " ");
355 90 }
356
357 // Registers at the fault, taken from the signal's ucontext.
358 //
359 // This is the one piece of evidence that survives when the unwinder cannot
360 // walk the stack at all. Crashes inside a hand-written-assembly libc routine
361 // (Raspberry Pi preloads libarmmem's memcpy over glibc's) report a single
362 // frame and nothing else, because that code carries no unwind tables -- so the
363 // caller is unrecoverable by backtrace() no matter how fppd itself is
364 // compiled. The link register still holds the return address into the caller,
365 // and the argument registers still hold what was passed, which for a faulting
366 // memcpy is the destination, source and length that caused it.
367 18 static void safeWriteFaultRegisters(int fd, void* ctx) {
368
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 if (!ctx) {
369 ✗ return;
370 }
371 18 safeWrite(fd, "\n=== registers at fault ===\n");
372 // Keyed on __linux__ rather than the FPP platform defines: these are glibc's
373 // ucontext layouts, and every other libc spells them differently.
374 #if defined(__linux__) && defined(__aarch64__)
375 ✗ const ucontext_t* uc = (const ucontext_t*)ctx;
376 ✗ safeWriteReg(fd, "pc", (uintptr_t)uc->uc_mcontext.pc);
377 ✗ safeWriteReg(fd, "sp", (uintptr_t)uc->uc_mcontext.sp);
378 ✗ safeWriteReg(fd, "lr", (uintptr_t)uc->uc_mcontext.regs[30]);
379 ✗ safeWrite(fd, "\n");
380 ✗ for (int i = 0; i < 8; i++) {
381 ✗ char nm[4] = { 'x', (char)('0' + i), '\0', '\0' };
382 ✗ safeWriteReg(fd, nm, (uintptr_t)uc->uc_mcontext.regs[i]);
383 }
384 ✗ safeWrite(fd, "\n");
385 #elif defined(__linux__) && defined(__arm__)
386 ✗ const ucontext_t* uc = (const ucontext_t*)ctx;
387 ✗ safeWriteReg(fd, "pc", (uintptr_t)uc->uc_mcontext.arm_pc);
388 ✗ safeWriteReg(fd, "sp", (uintptr_t)uc->uc_mcontext.arm_sp);
389 ✗ safeWriteReg(fd, "lr", (uintptr_t)uc->uc_mcontext.arm_lr);
390 // Bit 5 (T) says whether pc was running Thumb or ARM code, which is what
391 // decides how the bytes under "code at pc" disassemble: Debian's armhf is
392 // Thumb-2, Raspbian's is ARM.
393 ✗ safeWriteReg(fd, "cpsr", (uintptr_t)uc->uc_mcontext.arm_cpsr);
394 ✗ safeWrite(fd, "\n");
395 ✗ safeWriteReg(fd, "r0", (uintptr_t)uc->uc_mcontext.arm_r0);
396 ✗ safeWriteReg(fd, "r1", (uintptr_t)uc->uc_mcontext.arm_r1);
397 ✗ safeWriteReg(fd, "r2", (uintptr_t)uc->uc_mcontext.arm_r2);
398 ✗ safeWriteReg(fd, "r3", (uintptr_t)uc->uc_mcontext.arm_r3);
399 ✗ safeWrite(fd, "\n");
400 #elif defined(__linux__) && defined(__x86_64__)
401 18 const ucontext_t* uc = (const ucontext_t*)ctx;
402 18 safeWriteReg(fd, "rip", (uintptr_t)uc->uc_mcontext.gregs[REG_RIP]);
403 18 safeWriteReg(fd, "rsp", (uintptr_t)uc->uc_mcontext.gregs[REG_RSP]);
404 18 safeWrite(fd, "\n");
405 18 safeWriteReg(fd, "rdi", (uintptr_t)uc->uc_mcontext.gregs[REG_RDI]);
406 18 safeWriteReg(fd, "rsi", (uintptr_t)uc->uc_mcontext.gregs[REG_RSI]);
407 18 safeWriteReg(fd, "rdx", (uintptr_t)uc->uc_mcontext.gregs[REG_RDX]);
408 18 safeWrite(fd, "\n");
409 #else
410 (void)ctx;
411 safeWrite(fd, "(register dump not implemented for this architecture)\n");
412 #endif
413 }
414
415 // The instruction bytes around the faulting pc, as they were in memory.
416 //
417 // A SIGILL at an instruction that disassembles as perfectly valid in the
418 // packaged library can only mean the bytes the CPU ran were not the packaged
419 // ones -- a library damaged on disk, or overwritten under the running process.
420 // Without the bytes that can only be argued from the stack; with them it is
421 // a comparison. generate_crash_report compares them against the file on disk.
422 //
423 // Read with process_vm_readv rather than by dereferencing pc: a jump to an
424 // unmapped address is itself a common crash, and a fault in here would
425 // re-enter the handler. The syscall returns EFAULT instead, and reading our
426 // own process needs no ptrace permission. The two halves are read separately
427 // so a pc at the start of a mapping still gets the bytes from pc onwards.
428 18 static void safeWriteCodeAtFault(int fd, void* ctx) {
429 #if defined(__linux__) && (defined(__aarch64__) || defined(__arm__) || defined(__x86_64__))
430
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 if (!ctx) {
431 ✗ return;
432 }
433 18 const ucontext_t* uc = (const ucontext_t*)ctx;
434 #if defined(__aarch64__)
435 ✗ uintptr_t pc = (uintptr_t)uc->uc_mcontext.pc;
436 #elif defined(__arm__)
437 ✗ uintptr_t pc = (uintptr_t)uc->uc_mcontext.arm_pc;
438 #else
439 18 uintptr_t pc = (uintptr_t)uc->uc_mcontext.gregs[REG_RIP];
440 #endif
441 18 constexpr size_t kHalf = 16;
442 18 safeWrite(fd, "\n=== code at pc (memory order, pc-16 to pc+16) ===\n");
443
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 18 times.
54 for (int half = 0; half < 2; half++) {
444
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 18 times.
36 uintptr_t addr = half == 0 ? pc - kHalf : pc;
445 unsigned char buf[kHalf];
446 36 struct iovec local = { buf, kHalf };
447 36 struct iovec remote = { (void*)addr, kHalf };
448 36 ssize_t n = process_vm_readv(getpid(), &local, 1, &remote, 1, 0);
449
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 safeWriteHex(fd, addr);
450
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 safeWrite(fd, ":");
451
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if (n != (ssize_t)kHalf) {
452 ✗ safeWrite(fd, " unreadable\n");
453 ✗ continue;
454 }
455
2/2
✓ Branch 0 taken 576 times.
✓ Branch 1 taken 36 times.
612 for (size_t i = 0; i < kHalf; i++) {
456 576 char hex[4] = { ' ', "0123456789abcdef"[buf[i] >> 4], "0123456789abcdef"[buf[i] & 0xF], '\0' };
457
1/2
✓ Branch 0 taken 576 times.
✗ Branch 1 not taken.
576 safeWrite(fd, hex);
458 }
459
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 safeWrite(fd, "\n");
460 }
461 #else
462 (void)fd;
463 (void)ctx;
464 #endif
465 }
466
467 #ifndef PLATFORM_OSX
468 // Appends s at p and returns the new end. No terminator.
469 2336 static char* safeAppend(char* p, const char* s) {
470
2/2
✓ Branch 0 taken 52960 times.
✓ Branch 1 taken 2336 times.
55296 while (*s) {
471 52960 *p++ = *s++;
472 }
473 2336 return p;
474 }
475
476 // Appends the decimal digits of v at p and returns the new end. No terminator.
477 1168 static char* safeAppendDec(char* p, unsigned long v) {
478 char tmp[24];
479 1168 int n = 0;
480 do {
481 1960 tmp[n++] = (char)('0' + v % 10);
482 1960 v /= 10;
483
2/2
✓ Branch 0 taken 792 times.
✓ Branch 1 taken 1168 times.
1960 } while (v);
484
2/2
✓ Branch 0 taken 1960 times.
✓ Branch 1 taken 1168 times.
3128 while (n) {
485 1960 *p++ = tmp[--n];
486 }
487 1168 return p;
488 }
489
490 // A sysfs attribute into buf, trailing whitespace dropped. Only open, read
491 // and close, so the crash handler can call it.
492 1184 static bool safeReadAttr(const char* path, char* buf, size_t len) {
493 1184 int f = open(path, O_RDONLY | O_CLOEXEC);
494
2/2
✓ Branch 0 taken 1136 times.
✓ Branch 1 taken 48 times.
1184 if (f < 0) {
495 1136 return false;
496 }
497 48 ssize_t n = read(f, buf, len - 1);
498 48 close(f);
499
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 48 times.
48 if (n < 0) {
500 ✗ n = 0;
501 }
502
4/6
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✓ Branch 3 taken 48 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 48 times.
96 while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == ' ')) {
503 48 n--;
504 }
505 48 buf[n] = '\0';
506 48 return n > 0;
507 }
508
509 // Every thermal zone's temperature and every cpufreq policy's clock, read at
510 // the fault.
511 //
512 // A SIGILL at a valid instruction, or a pointer with one stray bit set, can be
513 // a part running too hot or clocked too fast for its supply rather than a bug.
514 // The statistics snapshot that rides along in some reports was taken minutes
515 // after fppd started, and the CPU cools within seconds of the load stopping,
516 // so only a reading taken here says how hot the part was when it failed.
517 //
518 // Thermal zones are on-chip sensors on every platform FPP runs on. Values are
519 // written in decimal with no "=0x", because the report side reads every
520 // name=0x... pair after the registers header as a register.
521 18 static void safeWriteThermal(int fd) {
522
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 safeWrite(fd, "\n=== thermal at fault ===\n");
523 char path[96];
524 char val[64];
525 18 bool any = false;
526
2/2
✓ Branch 0 taken 576 times.
✓ Branch 1 taken 18 times.
594 for (unsigned i = 0; i < 32; i++) {
527 576 char* base = safeAppendDec(safeAppend(path, "/sys/class/thermal/thermal_zone"), i);
528 576 *safeAppend(base, "/temp") = '\0';
529 char temp[24];
530
2/4
✓ Branch 0 taken 576 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 576 times.
✗ Branch 3 not taken.
576 if (!safeReadAttr(path, temp, sizeof(temp))) {
531 576 continue;
532 }
533 ✗ any = true;
534 ✗ *safeAppend(base, "/type") = '\0';
535 ✗ safeWrite(fd, "thermal_zone");
536 char num[24];
537 ✗ *safeAppendDec(num, i) = '\0';
538 ✗ safeWrite(fd, num);
539 ✗ safeWrite(fd, " ");
540 ✗ safeWrite(fd, safeReadAttr(path, val, sizeof(val)) ? val : "?");
541 ✗ safeWrite(fd, ": ");
542 // Millidegrees C, possibly negative, written as degrees.
543 ✗ const char* t = temp;
544 ✗ bool neg = *t == '-';
545 ✗ if (neg) {
546 ✗ t++;
547 }
548 ✗ unsigned long mc = 0;
549 ✗ bool digits = *t != '\0';
550 ✗ for (; *t; t++) {
551 ✗ if (*t < '0' || *t > '9') {
552 ✗ digits = false;
553 ✗ break;
554 }
555 ✗ mc = mc * 10 + (unsigned long)(*t - '0');
556 }
557 ✗ if (!digits) {
558 ✗ safeWrite(fd, temp);
559 ✗ safeWrite(fd, " (unparsed)\n");
560 ✗ continue;
561 }
562 ✗ char* p = val;
563 ✗ if (neg) {
564 ✗ *p++ = '-';
565 }
566 ✗ p = safeAppendDec(p, mc / 1000);
567 ✗ *p++ = '.';
568 ✗ unsigned long frac = mc % 1000;
569 ✗ *p++ = (char)('0' + frac / 100);
570 ✗ *p++ = (char)('0' + frac / 10 % 10);
571 ✗ *p++ = (char)('0' + frac % 10);
572 ✗ *safeAppend(p, " C\n") = '\0';
573 ✗ safeWrite(fd, val);
574 }
575
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (!any) {
576
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 safeWrite(fd, "(no thermal zones)\n");
577 }
578
2/2
✓ Branch 0 taken 576 times.
✓ Branch 1 taken 18 times.
594 for (unsigned i = 0; i < 32; i++) {
579 576 char* base = safeAppendDec(safeAppend(path, "/sys/devices/system/cpu/cpufreq/policy"), i);
580 576 *safeAppend(base, "/scaling_cur_freq") = '\0';
581 char freq[24];
582
3/4
✓ Branch 0 taken 576 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 560 times.
✓ Branch 3 taken 16 times.
576 if (!safeReadAttr(path, freq, sizeof(freq))) {
583 560 continue;
584 }
585
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, "cpufreq policy");
586 char num[24];
587 16 *safeAppendDec(num, i) = '\0';
588
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, num);
589
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, ": ");
590
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, freq);
591
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, " kHz");
592 16 *safeAppend(base, "/scaling_max_freq") = '\0';
593
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 if (safeReadAttr(path, val, sizeof(val))) {
594
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, " (max ");
595
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, val);
596
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, ")");
597 }
598 16 *safeAppend(base, "/scaling_governor") = '\0';
599
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 if (safeReadAttr(path, val, sizeof(val))) {
600
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, " ");
601
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, val);
602 }
603
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 safeWrite(fd, "\n");
604 }
605 18 }
606 #else
607 static void safeWriteThermal(int) {}
608 #endif
609
610 // Copy /proc/self/maps verbatim. Without the load address of each module, the
611 // raw frame addresses below are meaningless off-box: the report side needs
612 // (address - module base) to run addr2line against a stashed build.
613 19 static void safeWriteModuleMap(int fd) {
614 #ifndef PLATFORM_OSX
615
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 safeWrite(fd, "\n=== module map (/proc/self/maps) ===\n");
616
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 int mfd = open("/proc/self/maps", O_RDONLY);
617
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (mfd < 0) {
618 ✗ safeWrite(fd, "(unavailable)\n");
619 ✗ return;
620 }
621 char buf[4096];
622 ssize_t n;
623
3/4
✓ Branch 0 taken 361 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 342 times.
✓ Branch 3 taken 19 times.
361 while ((n = read(mfd, buf, sizeof(buf))) > 0) {
624
1/2
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
342 (void)!write(fd, buf, (size_t)n);
625 }
626
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 close(mfd);
627 #else
628 safeWrite(fd, "\n=== module map unavailable on this platform ===\n");
629 #endif
630 }
631
632 // No allocation: the crash handler calls this
633 16 static bool IsManualCrashReport(const char* path) {
634 16 size_t len = strlen(path);
635
3/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 12 times.
16 return len >= 11 && strcmp(path + len - 11, "-manual.zip") == 0;
636 }
637
638 // crashes/fpp-<type>-<version>-<uuid>-<time><suffix>.zip, relative to the media directory
639 17 static void CrashReportName(char* zfName, size_t zfLen, const char* suffix) {
640 char tbuffer[32];
641 time_t rawtime;
642 17 time(&rawtime);
643 struct tm timeinfo;
644 17 localtime_r(&rawtime, &timeinfo);
645 17 strftime(tbuffer, sizeof(tbuffer), "%Y-%m-%d_%H-%M-%S", &timeinfo);
646 #ifdef PLATFORM_ARMBIAN
647 char sysType[] = "Armbian";
648 #elif defined(PLATFORM_BBB)
649 ✗ char sysType[] = "BBB";
650 #elif defined(PLATFORM_BB64)
651 ✗ char sysType[] = "BB64";
652 #elif defined(PLATFORM_PI)
653 ✗ char sysType[] = "Pi";
654 #elif defined(PLATFORM_OSX)
655 char sysType[] = "MacOS";
656 #elif defined(PLATFORM_DOCKER)
657 17 char sysType[] = "Docker";
658 #elif defined(PLATFORM_DEBIAN)
659 char sysType[] = "Debian";
660 #else
661 char sysType[] = "Unknown";
662 #endif
663
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 std::string SystemUUID = getSetting("SystemUUID", "unknown");
664 char safeUUID[64];
665 17 unsigned int uuidLen = 0;
666
3/4
✓ Branch 0 taken 119 times.
✓ Branch 1 taken 17 times.
✓ Branch 2 taken 119 times.
✗ Branch 3 not taken.
136 for (const char* p = SystemUUID.c_str(); *p != '\0' && uuidLen < sizeof(safeUUID) - 1; ++p) {
667 119 unsigned char c = *p;
668
7/18
✓ Branch 0 taken 119 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 102 times.
✓ Branch 7 taken 17 times.
✓ Branch 8 taken 102 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 102 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
119 bool safe = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '.';
669
1/2
✓ Branch 0 taken 119 times.
✗ Branch 1 not taken.
119 if (safe) {
670 119 safeUUID[uuidLen++] = c;
671 }
672 }
673 17 safeUUID[uuidLen] = '\0';
674
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 snprintf(zfName, zfLen, "crashes/fpp-%s-%s-%s-%s%s.zip", sysType, getFPPVersion(), safeUUID, tbuffer, suffix);
675 17 }
676
677 18 static void handleCrash(int s, siginfo_t* si, void* ctx) {
678 static volatile bool inCrashHandler = false;
679
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 if (inCrashHandler) {
680 // need to ignore any crashes in the crash handler
681 ✗ return;
682 }
683 18 inCrashHandler = true;
684 // SIGQUIT/SIGUSR1 (kill -3) ask for a stack dump of a running fppd, not a
685 // crash: fppd carries on afterwards, so they get the gdb stack and the
686 // context files but no ring freeze, crash report or upload.
687
3/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 17 times.
18 const bool requestedDump = (s == SIGQUIT || s == SIGUSR1);
688
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 1 times.
18 if (!requestedDump) {
689 // Everything this handler logs from here on -- the signal line, the gdb
690 // stack -- would otherwise push the pre-crash history out of the ring.
691 // Nothing unfreezes it, so a dump that carries on must not do this.
692
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 CrashLogRingFreeze();
693 }
694 // Wedge-proofing: nearly everything below is async-signal-UNSAFE —
695 // fork()/gdb, LogErr (malloc), system()/curl. When the fatal signal
696 // fired while this thread held a libc lock (a heap-corruption SIGABRT
697 // inside free() always does), any of it can self-deadlock; the process
698 // then hangs forever, systemd keeps seeing it as running, and
699 // Restart=always never fires. Arm a watchdog with SIGALRM restored to
700 // its default (terminate) disposition — both calls are
701 // async-signal-safe — so a wedged handler still dies and gets
702 // restarted. Normal paths finish in _exit(-1) long before it fires;
703 // the timeout is generous because the crash-report zip + curl upload
704 // legitimately take time. A requested dump interrupts whatever the
705 // target thread was doing just the same, so it is armed for those too,
706 // and disarmed again when the dump returns.
707 18 constexpr unsigned int kCrashHandlerWatchdogSec = 120;
708 struct sigaction almAct;
709 struct sigaction prevAlmAct;
710 18 memset(&almAct, 0, sizeof(almAct));
711 18 memset(&prevAlmAct, 0, sizeof(prevAlmAct));
712 18 almAct.sa_handler = SIG_DFL;
713 18 sigemptyset(&almAct.sa_mask);
714 18 sigaction(SIGALRM, &almAct, &prevAlmAct);
715 18 unsigned int prevAlarm = alarm(kCrashHandlerWatchdogSec);
716 // The value used only until the user has answered, which is now a short and
717 // bounded window: the setup wizard writes ShareCrashData on Finish whether
718 // it was touched or not, and menuHead.inc will not let a box reach the rest
719 // of the UI until the wizard has been through. After that this fallback is
720 // unreachable and the user's own choice applies.
721 //
722 // For that window, 1 -- stack traces only. It is the level that rests on
723 // legitimate interests rather than consent (GDPR Art 6(1)(f)): a crash
724 // stack, the build, the plugin list and the playlist state identify nobody,
725 // and items 4 and 6 made level 1 mean exactly that. 3 was indefensible as an
726 // unanswered default because it ships settings, configuration and logs, all
727 // of which carry personal data and therefore rest on consent. 0 would be
728 // wrong in the other direction: it forfeits data the project has a lawful
729 // basis to collect, and a crash on first boot is exactly the one nobody can
730 // reproduce later.
731
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 int crashLog = getSettingInt("ShareCrashData", 1);
732 #ifndef PLATFORM_OSX
733
3/6
✓ 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.
18 LogErr(VB_ALL, "Crash handler called in thread %u: signal=%d (SIG%s: %s) addr=%p si_code=%d\n",
734 gettid(), s, safe(sigabbrev_np(s)), safe(sigdescr_np(s)),
735 si ? si->si_addr : nullptr, si ? si->si_code : -1);
736 #else
737 uint64_t tid;
738 pthread_threadid_np(NULL, &tid);
739 LogErr(VB_ALL, "Crash handler called in thread %u: signal=%d addr=%p\n", tid, s, si ? si->si_addr : nullptr);
740 #endif
741
742 // Deliberately the raw member, not GetSeqFilenameCopy(): this is a signal
743 // handler, so it can interrupt a thread that already holds m_seqFilenameLock
744 // and taking it here would self-deadlock the crash report. A torn read is
745 // the acceptable trade for a log line that is already best-effort.
746 // sequence is null before main() creates it and after shutdown deletes it,
747 // and a crash in either window still needs its report.
748 18 Sequence* seq = sequence;
749
3/6
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 18 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
18 if (seq && !seq->m_seqFilename.empty()) {
750 ✗ LogErr(VB_ALL, " while playing %s at %d ms\n", seq->m_seqFilename.c_str(), seq->m_seqMSElapsed.load());
751 }
752 18 const char* gdbFailReason = nullptr;
753
3/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 17 times.
18 if (!dumpstack_gdb(&gdbFailReason)) {
754 void* callstack[128];
755
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 int i, frames = backtrace(callstack, 128);
756
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 int fd = open("/tmp/fppd_crash.log", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
757
758 // Safe half first (see the safeWrite* helpers): the reason, the raw
759 // return addresses and the module map are enough to reconstruct the
760 // stack off-box with addr2line, and they are written with nothing that
761 // can block or allocate. Everything below this point may not run.
762
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 safeWrite(fd, "=== gdb stack unavailable: ");
763
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 safeWrite(fd, gdbFailReason ? gdbFailReason : "unknown");
764
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 safeWrite(fd, " ===\n=== raw frames (subtract the module base from the map below) ===\n");
765
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 times.
13 for (i = 0; i < frames; i++) {
766
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 safeWriteHex(fd, (uintptr_t)callstack[i]);
767
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 safeWrite(fd, "\n");
768 }
769
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 safeWriteModuleMap(fd);
770
771 // Unsafe half: backtrace_symbols() allocates. Attempted last, and only
772 // for readability -- the record above already stands on its own.
773
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 safeWrite(fd, "\n=== symbolized frames (best effort) ===\n");
774 1 char** strs = backtrace_symbols(callstack, frames);
775
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (strs) {
776
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 times.
13 for (i = 0; i < frames; i++) {
777
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 LogErr(VB_ALL, " %s\n", strs[i]);
778 }
779
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 times.
13 for (i = 0; i < frames; i++) {
780
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 printf(" %s\n", strs[i]);
781
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 write(fd, strs[i], strlen(strs[i]));
782
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 write(fd, "\n", 1);
783 }
784 1 free(strs);
785 } else {
786 ✗ safeWrite(fd, "(backtrace_symbols failed)\n");
787 }
788
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 write(fd, "\n", 1);
789
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 close(fd);
790 }
791
792 // Context files, written unconditionally (the gdb path above produces only
793 // a stack). Separate files so the stack parser is unaffected.
794 //
795 // Split into two on PRIVACY grounds, and the split must be kept: the
796 // playlist state and the fault registers are bare scalars that identify
797 // nothing, whereas the log ring is log content and FPP's logs are full of
798 // host names, LAN addresses and discovered-device identifiers. The
799 // bundler therefore ships the first at every share level and the second
800 // only where logs/fppd.log already goes. Putting them in one file sends a
801 // user's network topology to anyone who selected "stack traces only".
802 {
803
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 int cfd = open("/tmp/fppd_crash_context.log", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
804
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (cfd >= 0) {
805
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 PlaylistDumpCrashState(cfd);
806 // The last call the main loop entered. A pointer to a string
807 // literal (see SetMainLoopPhase), so reading it here is
808 // signal-safe, and it is the only record of what the main thread
809 // was doing that survives a stack gdb cannot fully unwind.
810
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 safeWrite(cfd, "Main loop phase: ");
811
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 safeWrite(cfd, GetMainLoopPhase());
812
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 safeWrite(cfd, "\n");
813 // Written even when gdb succeeded: gdb reports the registers of
814 // the thread it happens to unwind, not necessarily the faulting
815 // one, and this costs nothing.
816
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 safeWriteFaultRegisters(cfd, ctx);
817 // Before the module map, which parsers read to the end of file.
818
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 safeWriteCodeAtFault(cfd, ctx);
819
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 safeWriteThermal(cfd);
820 // Also written even when gdb succeeded, and this is the half that
821 // was missing: gdb resolves what it can and leaves everything else
822 // as a bare address, and a frame it fails to unwind past is simply
823 // lost. Without the load addresses none of those can be recovered
824 // afterwards -- with them, any address in the dump maps back to a
825 // function with addr2line against the matching build. Addresses
826 // and library paths only, so it belongs with the registers rather
827 // than with the log ring.
828
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 safeWriteModuleMap(cfd);
829
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 close(cfd);
830 }
831
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 int rfd = open("/tmp/fppd_crash_log_ring.log", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
832
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (rfd >= 0) {
833
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 CrashLogRingDump(rfd);
834
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 close(rfd);
835 }
836 }
837
4/4
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 1 times.
18 if (crashLog >= 0 && !requestedDump) {
838 // (modification time, path) of every report in crashes/
839 15 std::vector<std::pair<std::filesystem::file_time_type, std::string>> reports;
840
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 std::string mediaDir = getFPPMediaDir();
841
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 std::string cdir = mediaDir + "/crashes";
842
843 15 chdir(mediaDir.c_str());
844 15 mkdir(cdir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
845
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 SetFilePerms(cdir, true);
846
847 15 bool hasRecent = false;
848 // The error_code overloads throughout: a report removed while this
849 // runs (the UI deleting one, a manual report being pruned) made the
850 // throwing ones throw, and an exception here aborts the crash handler.
851 15 std::error_code ec;
852
8/12
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 28 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 13 times.
✓ Branch 9 taken 15 times.
✓ Branch 10 taken 13 times.
✓ Branch 11 taken 15 times.
28 for (std::filesystem::directory_iterator it(cdir, ec), end; !ec && it != end; it.increment(ec)) {
853 13 std::error_code fec;
854 13 auto ftime = it->last_write_time(fec);
855
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (fec) {
856 1 continue; // gone since it was listed
857 }
858
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 reports.emplace_back(ftime, it->path().string());
859 // Reports the user asked for are not crashes (see ManualCrashReportCallback)
860
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 if (IsManualCrashReport(it->path().c_str())) {
861 1 continue;
862 }
863 12 std::chrono::system_clock::time_point stm;
864 #if defined(PLATFORM_OSX)
865 #if (__apple_build_version__ >= 14030022)
866 stm = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
867 std::chrono::file_clock::to_sys(ftime));
868 #else
869 std::time_t tt = decltype(ftime)::clock::to_time_t(ftime);
870 stm = std::chrono::system_clock::from_time_t(tt);
871 #endif
872 #else
873 12 stm = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
874 12 std::chrono::file_clock::to_sys(ftime));
875 #endif
876
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 auto tdiff = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - stm);
877
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (tdiff.count() < 60) {
878 ✗ hasRecent = true;
879 }
880 15 }
881 // Keep the ten newest. By time, not name: names start with the FPP
882 // version, so after an upgrade the oldest by name could be the newest.
883
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 std::sort(reports.begin(), reports.end());
884
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 15 times.
17 for (size_t i = 0; i + 10 < reports.size(); i++) {
885 2 unlink(reports[i].second.c_str());
886 }
887
888
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 if (!hasRecent) {
889 char zfName[256];
890
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 CrashReportName(zfName, sizeof(zfName), "");
891
892 // Use script to generate crash report with passwords redacted.
893 // A full report (gdb on every thread, the logs, the config) takes
894 // well over two minutes on a slow player with a slow card, and the
895 // watchdog armed above cut it off there. This far the handler has
896 // not wedged, so give the script and the upload their own budget,
897 // and bound the script with timeout(1) so it still can't hang
898 // the restart.
899 15 constexpr unsigned int kReportScriptSec = 300;
900 15 constexpr unsigned int kUploadSec = 90;
901
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 9 times.
15 alarm(kReportScriptSec + 15 + (crashLog >= 1 ? kUploadSec : 0));
902
3/6
✓ 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.
30 std::string scriptCmd = getFPPDDir("/scripts/generate_crash_report") + " " +
903
5/8
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 9 times.
✓ 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 std::to_string(crashLog == 0 ? 3 : crashLog) + " " + zfName;
904
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 if (FileExists("/usr/bin/timeout")) {
905
3/6
✓ 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.
15 scriptCmd = "/usr/bin/timeout -k 10 " + std::to_string(kReportScriptSec) + " " + scriptCmd;
906 }
907
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 system(scriptCmd.c_str());
908
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 SetFilePerms(zfName);
909
910
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 9 times.
15 if (crashLog >= 1) {
911 // Upload crash report
912 char uploadCmd[512];
913 6 snprintf(uploadCmd, sizeof(uploadCmd), "curl --connect-timeout 15 --max-time %u https://crashes.falconplayer.com/crashUpload/index.php -F userfile=@%s",
914 kUploadSec - 5, zfName);
915
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 system(uploadCmd);
916 }
917 15 } else {
918 ✗ LogErr(VB_ALL, "Very recent crash report found, not uploading\n");
919 }
920 15 }
921
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 17 times.
18 if (requestedDump) {
922 // fppd carries on: put SIGALRM and any alarm already pending (the
923 // loop watchdog's own exit timer) back as they were.
924 1 sigaction(SIGALRM, &prevAlmAct, nullptr);
925 1 alarm(prevAlarm);
926 1 inCrashHandler = false;
927 } else {
928 // For SIGBUS in non-main threads, terminate only the faulting thread
929 // instead of the entire process. This allows fppd to survive
930 // DRM/KMS buffer crashes in video consumer threads while keeping
931 // audio and other functionality running.
932 #ifndef PLATFORM_OSX
933
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 17 times.
17 if (s == SIGBUS && gettid() != getpid()) {
934 ✗ LogErr(VB_ALL, "SIGBUS in non-main thread %u — terminating thread only\n", gettid());
935 #else
936 if (s == SIGBUS && !pthread_main_np()) {
937 uint64_t tid = 0;
938 pthread_threadid_np(NULL, &tid);
939 LogErr(VB_ALL, "SIGBUS in non-main thread %llu — terminating thread only\n", tid);
940 #endif
941 ✗ WarningHolder::AddWarning(1, "Video thread crashed (SIGBUS). Video output may be unavailable.");
942 ✗ WarningHolder::WriteWarningsFile();
943 // Restore default SIGBUS handler for this thread so the
944 // pthread_exit cleanup doesn't loop back here if it touches
945 // the same bad mapping.
946 struct sigaction sa;
947 ✗ sa.sa_handler = SIG_DFL;
948 ✗ sigemptyset(&sa.sa_mask);
949 ✗ sa.sa_flags = 0;
950 ✗ sigaction(SIGBUS, &sa, NULL);
951 // The process survives this path — disarm the crash watchdog.
952 ✗ alarm(0);
953 ✗ inCrashHandler = false;
954 ✗ pthread_exit(NULL);
955 return; // unreachable, but satisfies compiler
956 }
957 17 runMainFPPDLoop = 0;
958
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 WarningHolder::StopNotifyThread();
959 // Written without warningsLock: the crashed thread may hold it, and
960 // waiting on it here left fppd dark until the watchdog fired.
961 // inCrashHandler stays set until _exit, so a fault from here on
962 // doesn't re-run the whole handler.
963
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 WarningHolder::WriteCrashedWarningsFile(1, "FPPD has crashed. Check crash reports.");
964 // _exit(), not exit(): exit() runs the global/static destructors, and
965 // those take locks the crashed thread may still hold -- a crash inside
966 // RTSPOutputManager::ApplyConfig() left ~RTSPOutputManager waiting on
967 // its apply lock until the watchdog above killed fppd two minutes
968 // later, leaving the show dark all that time. main() skips them for
969 // the same reason on a normal exit (see its end), so nothing depends
970 // on them running.
971
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 fflush(nullptr);
972 17 _exit(-1);
973 }
974 }
975
976 // A report the user asked for (POST /api/crashes/report), not a crash: level 3,
977 // named -manual. Built in media/tmp/manual-crash, as zip's temp file in
978 // crashes/ would look like a recent crash to handleCrash. No gdb stack:
979 // attaching pauses fppd. One build at a time, then a 10s cooldown so a caller
980 // in a loop cannot keep the player building back to back.
981 4 static std::string ManualCrashReportCallback(std::string& error) {
982 static std::mutex lock;
983 static std::chrono::steady_clock::time_point last;
984 static bool built = false;
985 4 std::unique_lock<std::mutex> l(lock, std::try_to_lock);
986
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (!l.owns_lock()) {
987
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 error = "busy";
988
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 return "";
989 }
990
7/10
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
✓ 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 taken 2 times.
3 if (built && std::chrono::steady_clock::now() - last < std::chrono::seconds(10)) {
991
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 error = "rate-limited";
992
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 return "";
993 }
994 // Counted from the end of every build, failed or timed out ones included
995 struct BuildDone {
996 1 ~BuildDone() {
997 1 last = std::chrono::steady_clock::now();
998 1 built = true;
999 1 }
1000 } buildDone;
1001
1002
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string mediaDir = getFPPMediaDir();
1003 char name[256];
1004
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 CrashReportName(name, sizeof(name), "-manual");
1005 // A truncated name would not end -manual.zip; a quote would break the command
1006
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
2 if (!IsManualCrashReport(name) || mediaDir.find('\'') != std::string::npos) {
1007 ✗ error = "build-failed";
1008 ✗ return "";
1009 }
1010
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string base = name + strlen("crashes/");
1011
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string tmpDir = mediaDir + "/tmp/manual-crash";
1012
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string tmpPath = tmpDir + "/" + base;
1013
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string cdir = mediaDir + "/crashes";
1014
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string path = cdir + "/" + base;
1015
1016 // Only this builds in tmpDir, so anything there was left by a build that
1017 // fppd restarting or crashing cut short (the zip, zip's temp file and the
1018 // script's working directory)
1019 2 std::error_code ec;
1020
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::filesystem::remove_all(tmpDir, ec);
1021 2 mkdir(tmpDir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
1022
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 SetFilePerms(tmpDir, true);
1023
1024 // "manual" leaves out the last crash's /tmp files. A hung script would hold
1025 // the lock until fppd restarts. Four minutes lets a Pi Zero on a slow SD card
1026 // finish; the timeout also answers inside PHP's 270s.
1027
5/10
✓ 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.
2 std::string cmd = getFPPDDir("/scripts/generate_crash_report") + " 3 '" + tmpPath + "' manual";
1028
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (FileExists("/usr/bin/timeout")) {
1029
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 cmd = "/usr/bin/timeout -k 10 240 " + cmd;
1030 }
1031
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 system(cmd.c_str());
1032
1033 struct stat st;
1034
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
1 if (stat(tmpPath.c_str(), &st) != 0 || st.st_size == 0) {
1035 ✗ unlink(tmpPath.c_str());
1036 ✗ error = "build-failed";
1037 ✗ return "";
1038 }
1039
1040 1 mkdir(cdir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
1041
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 SetFilePerms(cdir, true);
1042 // media/tmp may be on another filesystem, so fall back to copy and delete
1043 1 ec.clear();
1044
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::filesystem::rename(tmpPath, path, ec);
1045
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (ec) {
1046 ✗ ec.clear();
1047 ✗ std::filesystem::copy_file(tmpPath, path, ec);
1048 ✗ unlink(tmpPath.c_str());
1049 ✗ if (ec) {
1050 ✗ unlink(path.c_str());
1051 ✗ error = "build-failed";
1052 ✗ return "";
1053 }
1054 }
1055 // Keep at most two manual reports, counting the new one. Pruned after the
1056 // move so a failed move does not also lose the previous report. Oldest
1057 // first by time: the names start with the FPP version, so after an
1058 // upgrade sorting by name kept the wrong one.
1059 1 std::vector<std::pair<std::filesystem::file_time_type, std::string>> manual;
1060 1 ec.clear();
1061
8/12
✓ 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 1 times.
✓ Branch 9 taken 1 times.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 1 times.
2 for (std::filesystem::directory_iterator it(cdir, ec), end; !ec && it != end; it.increment(ec)) {
1062 1 std::error_code fec;
1063 1 auto t = it->last_write_time(fec);
1064
6/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 not taken.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 1 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
1 if (!fec && IsManualCrashReport(it->path().c_str()) && it->path() != path) {
1065 ✗ manual.emplace_back(t, it->path().string());
1066 }
1067 1 }
1068
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::sort(manual.begin(), manual.end());
1069
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 for (size_t i = 0; i + 1 < manual.size(); i++) {
1070 ✗ unlink(manual[i].second.c_str());
1071 }
1072
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 SetFilePerms(path);
1073 1 return base;
1074 3 }
1075
1076 1888 bool setupExceptionHandlers() {
1077 // old sig handlers
1078 static bool s_savedHandlers = false;
1079 static struct sigaction s_handlerFPE,
1080 s_handlerILL,
1081 s_handlerBUS,
1082 s_handlerSEGV,
1083 s_handlerABRT;
1084
1085 1888 bool ok = true;
1086
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 if (!s_savedHandlers) {
1087 // install the signal handler
1088 struct sigaction act;
1089
1090 // some systems extend it with non std fields, so zero everything
1091 1888 memset(&act, 0, sizeof(act));
1092
1093 1888 act.sa_sigaction = handleCrash;
1094 1888 sigemptyset(&act.sa_mask);
1095 1888 act.sa_flags = SA_SIGINFO;
1096
1097 1888 ok &= sigaction(SIGFPE, &act, &s_handlerFPE) == 0;
1098 1888 ok &= sigaction(SIGILL, &act, &s_handlerILL) == 0;
1099 1888 ok &= sigaction(SIGBUS, &act, &s_handlerBUS) == 0;
1100 1888 ok &= sigaction(SIGSEGV, &act, &s_handlerSEGV) == 0;
1101 1888 ok &= sigaction(SIGABRT, &act, &s_handlerABRT) == 0;
1102 1888 ok &= sigaction(SIGQUIT, &act, nullptr) == 0;
1103 1888 ok &= sigaction(SIGUSR1, &act, nullptr) == 0;
1104
1105 struct sigaction sigchld_action;
1106 1888 sigchld_action.sa_handler = SIG_DFL;
1107 1888 sigchld_action.sa_flags = SA_NOCLDWAIT;
1108 1888 sigemptyset(&sigchld_action.sa_mask);
1109 1888 ok &= sigaction(SIGCHLD, &sigchld_action, NULL) == 0;
1110
1111
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1888 times.
1888 if (!ok) {
1112 ✗ LogWarn(VB_ALL, "Failed to install our signal handler.\n");
1113 }
1114 1888 s_savedHandlers = true;
1115 ✗ } else if (s_savedHandlers) {
1116 // uninstall the signal handler
1117 ✗ ok &= sigaction(SIGFPE, &s_handlerFPE, NULL) == 0;
1118 ✗ ok &= sigaction(SIGILL, &s_handlerILL, NULL) == 0;
1119 ✗ ok &= sigaction(SIGBUS, &s_handlerBUS, NULL) == 0;
1120 ✗ ok &= sigaction(SIGSEGV, &s_handlerSEGV, NULL) == 0;
1121 ✗ ok &= sigaction(SIGABRT, &s_handlerABRT, NULL) == 0;
1122 ✗ if (!ok) {
1123 ✗ LogWarn(VB_ALL, "Failed to install default signal handlers.\n");
1124 }
1125 ✗ s_savedHandlers = false;
1126 }
1127 1888 return ok;
1128 }
1129
1130 1887 static void initCapeFromFile(const std::string& f) {
1131
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1880 times.
1887 if (FileExists(f)) {
1132
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 Json::Value root;
1133
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 if (LoadJsonFromFile(f, root, JsonRoot::Object)) {
1134 // if there are sources of sensor data, get them loaded first
1135
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 if (root.isMember("sensorSources")) {
1136
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 Sensors::INSTANCE.addSensorSources(root["sensorSources"]);
1137 }
1138
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 Sensors::INSTANCE.addSensors(root["sensors"]);
1139 }
1140 7 }
1141 1887 }
1142 1887 static void initCape() {
1143
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
1887 initCapeFromFile(FPP_DIR_MEDIA("/tmp/cape-sensors.json"));
1144 1887 }
1145
1146 // Leave without running the global/static destructors, as main()'s normal
1147 // exit and the crash handler already do (see the end of main()). An early
1148 // failure that called exit() ran them with fppd half started: the warnings
1149 // notify thread read WarningHolder's map while __cxa_finalize freed it, and
1150 // ~PluginManager's cleanup read settings that were already destroyed.
1151 1 [[noreturn]] static void ExitWithoutTeardown(int code) {
1152 1 fflush(nullptr);
1153 1 _exit(code);
1154 }
1155
1156 ✗ void usage(char* appname) {
1157 ✗ printf("Usage: %s [OPTION...]\n"
1158 "\n"
1159 "fppd is the Falcon Player daemon.\n"
1160 "\n"
1161 "Options:\n"
1162 " -f, --foreground - Don't daemonize the application. When run\n"
1163 " from a terminal, log lines are echoed to the\n"
1164 " console in addition to the log file. (Under\n"
1165 " systemd, where stdout is not a terminal, the\n"
1166 " console echo is suppressed to avoid doubling\n"
1167 " every line into the journal.)\n"
1168 " -d, --daemonize - Daemonize even if the config file says not to.\n"
1169 " -v, --volume VOLUME - Set a volume (over-written by config file)\n"
1170 " -m, --mode MODE - Set the mode: \"player\", \"bridge\",\n"
1171 " \"master\", or \"remote\"\n"
1172 " -l, --log-file FILENAME - Set the log file. Use \"stdout\" or \"stderr\"\n"
1173 " to log to the console instead of a file.\n"
1174 " -H --detect-hardware - Detect Falcon hardware on SPI port\n"
1175 " -C --configure-hardware - Configure detected Falcon hardware on SPI\n"
1176 " -h, --help - This menu.\n"
1177 " --log-level LEVEL - Set the global log output level (all loggers):\n"
1178 " \"info\", \"warn\", \"debug\", \"excess\")\n"
1179 " --log-level LEVEL:logger - Set the log level for one or more loggers.\n"
1180 " each level should be separated by semicolons\n"
1181 " with one or more loggers separated by commas.\n"
1182 " example: debug:schedule,player;excess:mqtt \n"
1183 " valid loggers are: \n"
1184 " ChannelData - channel data itself\n"
1185 " ChannelOut - channel output code\n"
1186 " Command - command processing\n"
1187 " Control - Control socket debugging\n"
1188 " E131Bridge - E1.31 bridge\n"
1189 " Effect - Effects sequences\n"
1190 " General - general messages\n"
1191 " GPIO - GPIO Input handling\n"
1192 " HTTP - HTTP API requests\n"
1193 " MediaOut - Media file handling\n"
1194 " Playlist - Playlist handling\n"
1195 " Plugin - Plugin handling\n"
1196 " Schedule - Playlist scheduling\n"
1197 " Sequence - Sequence parsing\n"
1198 " Setting - Settings parsing\n"
1199 " Sync - Player/Remote Synchronization\n"
1200 " The default logging is read from settings\n",
1201 appname);
1202 ✗ }
1203
1204 // The "-v" volume, applied once fppd has daemonized. Setting it while
1205 // parsing the arguments started the volume worker, which runs amixer and
1206 // pactl holding locks of its own, in the process that then forks into the
1207 // background: the daemon could inherit one of those locks held by a thread it
1208 // doesn't have, and never apply another volume change.
1209 static int s_argVolume = -1;
1210
1211 1888 int parseArguments(int argc, char** argv) {
1212 1888 char* s = NULL;
1213 int c;
1214
1215
4/8
✓ 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.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
7552 SetSetting("logFile", FPP_FILE_LOG);
1216
1217 while (1) {
1218
1/2
✓ Branch 0 taken 23810 times.
✗ Branch 1 not taken.
23810 int this_option_optind = optind ? optind : 1;
1219 23810 int option_index = 0;
1220 static struct option long_options[] = {
1221 { "displayvers", no_argument, 0, 'V' },
1222 { "foreground", no_argument, 0, 'f' },
1223 { "daemonize", no_argument, 0, 'd' },
1224 { "restarted", no_argument, 0, 'r' },
1225 { "volume", required_argument, 0, 'v' },
1226 { "mode", required_argument, 0, 'm' },
1227 { "log-file", required_argument, 0, 'l' },
1228 { "playlist", required_argument, 0, 'p' },
1229 { "position", required_argument, 0, 'P' },
1230 { "detect-piface", no_argument, 0, 4 },
1231 { "resume-unscheduled", required_argument, 0, 5 },
1232 { "help", no_argument, 0, 'h' },
1233 { "log-level", required_argument, 0, 2 },
1234 { 0, 0, 0, 0 }
1235 };
1236
1237 23810 c = getopt_long(argc, argv, "Vfdrv:m:l:p:P:h",
1238 long_options, &option_index);
1239
2/2
✓ Branch 0 taken 1888 times.
✓ Branch 1 taken 21922 times.
23810 if (c == -1)
1240 1888 break;
1241
1242
10/13
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 1883 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 5 times.
✓ Branch 6 taken 5 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 4 times.
✓ Branch 9 taken 20000 times.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
21922 switch (c) {
1243 ✗ case 'V':
1244 ✗ printVersionInfo();
1245 ✗ ExitWithoutTeardown(0);
1246 9 case 2: // log-level
1247
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 if (SetLogLevelComplex(optarg)) {
1248
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 std::cout << FPPLogger::INSTANCE.GetLogLevelString() << std::endl;
1249
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 LogInfo(VB_SETTING, "Log Level set to %d (%s)\n", FPPLogger::INSTANCE.MinimumLogLevel(), optarg);
1250 }
1251 9 break;
1252 1883 case 'f': // foreground
1253
2/4
✓ Branch 0 taken 1883 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1883 times.
✗ Branch 3 not taken.
1883 SetSetting("daemonize", 0);
1254 1883 break;
1255 5 case 'd': // daemonize
1256
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 SetSetting("daemonize", 1);
1257 5 break;
1258 9 case 'r': // restarted
1259
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 SetSetting("restarted", 1);
1260 9 break;
1261 5 case 'p': // playlist
1262
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
15 SetSetting("resumePlaylist", optarg);
1263 5 break;
1264 5 case 'P': // position
1265
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 SetSetting("resumePosition", atoi(optarg));
1266 5 break;
1267 1 case 5: // resume-unscheduled <repeat>
1268 // -p/-P alone only restore the POSITION of a playlist the scheduler
1269 // is about to start anyway (Scheduler::PlayScheduledItem). This
1270 // says "start it yourself if the scheduler doesn't", which is what
1271 // a self-initiated recovery restart of a manually-started show
1272 // needs (issue #2727). The argument carries the repeat mode.
1273
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 SetSetting("resumeUnscheduled", 1);
1274
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 SetSetting("resumeRepeat", atoi(optarg));
1275 1 break;
1276 4 case 'v': // volume
1277 // Applied after daemonizing (see s_argVolume), not here.
1278 4 s_argVolume = atoi(optarg);
1279 4 break;
1280 20000 case 'm': // mode
1281
3/6
✓ Branch 0 taken 20000 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20000 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20000 times.
✗ Branch 5 not taken.
60000 SetSetting("fppMode", optarg);
1282 20000 break;
1283 1 case 'l': // log-file
1284
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.
3 SetSetting("logFile", optarg);
1285 1 break;
1286 ✗ case 'h': // help
1287 ✗ usage(argv[0]);
1288 ✗ ExitWithoutTeardown(EXIT_SUCCESS);
1289 break;
1290 ✗ default:
1291 ✗ usage(argv[0]);
1292 ✗ ExitWithoutTeardown(EXIT_FAILURE);
1293 }
1294 21922 }
1295
1296 // fppd is the one binary that asks for the stdout copy to be dropped once a
1297 // line is in the log file: "-f" is how systemd runs it, so !daemonize is true
1298 // in normal operation and every line would otherwise be duplicated into
1299 // journald. (An interactive `fppd -f` still echoes -- log.cpp checks isatty.)
1300 // The suppression is opt-in precisely so fppoled and the other small services
1301 // that link this logger keep showing up in `journalctl -u <service>`.
1302 1888 SetSuppressDuplicateStdOut(true);
1303
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 160 times.
✗ Branch 5 not taken.
1888 SetLogFile(getSetting("logFile").c_str(), !getSettingInt("daemonize"));
1304
1305 1888 return 0;
1306 }
1307
1308 // Workaround ASan bug 27790 (https://bugs.llvm.org//show_bug.cgi?id=27790)
1309 // by logging if LD_LIBRARY_PATH doesn't include the path to argv[0] (our binary).
1310 // This is because ASan (and TSan, which shares the interceptor) intercepts dlopen
1311 // and causes calls to dlopen to use the sanitizer's rpath instead of the rpath of
1312 // the calling DSO, so the libfpp-co-*.so output plugins are not found. We cannot
1313 // change the LD_LIBRARY_PATH env var because the loader has already processed it
1314 // by the time that main() is called. We could fork()/execvpe(), but this has
1315 // implications for things like debugging that may be monitoring our pid.
1316 1888 bool checkASan(int argc, char** argv) {
1317 #if __SANITIZE_ADDRESS__ || __SANITIZE_THREAD__
1318 {
1319 struct FreeString {
1320 void operator()(char* p) {
1321 free(p);
1322 }
1323 };
1324 if (argc < 1 || !argv[0])
1325 abort(); // shouldn't happen
1326 std::unique_ptr<char[], FreeString> self(realpath(argv[0], NULL));
1327 if (!self)
1328 abort(); // shouldn't happen
1329 size_t sep = std::string_view(self.get()).rfind('/');
1330 if (sep == std::string_view::npos)
1331 abort(); // shouldn't happen
1332 self[sep] = '\0';
1333
1334 auto libraryPath = getenv("LD_LIBRARY_PATH");
1335 std::string_view libraryPathSv(libraryPath ? libraryPath : "");
1336 bool found = false;
1337 for (size_t start = 0, index = libraryPathSv.find(':');; start = index + 1, index = libraryPathSv.find(':', index + 1)) {
1338 if (auto pathSV = libraryPathSv.substr(start, index - start); !pathSV.empty()) {
1339 std::unique_ptr<char[], FreeString> resolved(realpath(std::string(pathSV).c_str(), NULL));
1340 if (resolved && strcmp(self.get(), resolved.get()) == 0) {
1341 found = true;
1342 break;
1343 }
1344 }
1345 if (index == libraryPathSv.npos)
1346 break;
1347 }
1348
1349 if (!found) {
1350 constexpr static char str[] = "When built with ASan or TSan, LD_LIBRARY_PATH must be set to include the path to fppd. Exiting.\n";
1351 LogErr(VB_GENERAL, str);
1352 fprintf(stderr, str);
1353 return false;
1354 }
1355 }
1356 #endif
1357
1358 1888 return true;
1359 }
1360
1361 1888 int main(int argc, char* argv[]) {
1362 3776 auto ensureEnvDefault = [](const char* key, const char* value) {
1363
2/2
✓ Branch 0 taken 3758 times.
✓ Branch 1 taken 18 times.
3776 if (!getenv(key)) {
1364 3758 setenv(key, value, 0);
1365 }
1366 3776 };
1367
1368 // Force line-buffered stdout so logs via systemd/journald appear promptly
1369 1888 setvbuf(stdout, NULL, _IOLBF, 0);
1370
1371
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 setupExceptionHandlers();
1372
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 FPPLogger::INSTANCE.Init();
1373
1374
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1888 times.
1888 if (!checkASan(argc, argv))
1375 ✗ ExitWithoutTeardown(1);
1376
1377
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 LoadSettings(argv[0]);
1378
1379 // Align runtime audio library backend defaults with configured FPP backend.
1380 // This is a safety net in case the service env file is stale/missing.
1381
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
1888 std::string mediaBackend = toLowerCopy(getSetting("MediaBackend"));
1382
8/10
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1812 times.
✓ Branch 3 taken 76 times.
✓ Branch 4 taken 1812 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1806 times.
✓ Branch 7 taken 6 times.
✓ Branch 8 taken 1882 times.
✓ Branch 9 taken 6 times.
1888 if (mediaBackend == "pipewire" || mediaBackend == "pipewire-simple") {
1383 1882 ensureEnvDefault("SDL_AUDIODRIVER", "pulse");
1384 1882 ensureEnvDefault("ALSOFT_DRIVERS", "pulse");
1385 } else {
1386 6 ensureEnvDefault("SDL_AUDIODRIVER", "alsa");
1387 6 ensureEnvDefault("ALSOFT_DRIVERS", "alsa");
1388 }
1389
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.
1888 LogInfo(VB_MEDIAOUT, "Audio env: SDL_AUDIODRIVER=%s ALSOFT_DRIVERS=%s\n",
1390 getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER") : "(unset)",
1391 getenv("ALSOFT_DRIVERS") ? getenv("ALSOFT_DRIVERS") : "(unset)");
1392
1393
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 curl_global_init(CURL_GLOBAL_ALL);
1394
1395 // Initialize Magick, but we don't want the Magick signal handlers as they interfere with our own
1396
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 MagickLib::InitializeMagickEx(nullptr, MAGICK_OPT_NO_SIGNAL_HANDER, nullptr);
1397
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 Magick::InitializeMagick(NULL);
1398 // Cap the size of any image decoded in fppd (overlay image uploads over
1399 // HTTP, Image effects, playlist images, virtual display backgrounds).
1400 // GraphicsMagick allocates whatever the image header asks for: a 1 MB PNG
1401 // claiming 30000x30000 took 4+ GB and fppd was OOM-killed. A header past
1402 // this limit now fails to read. Only the pixel count is capped, not the
1403 // width or height: the limits apply to every image fppd makes, and the
1404 // Text effect draws a long message as one strip tens of thousands of
1405 // pixels wide (it bounds its own size). The cap is a quarter of RAM in
1406 // pixel cache (8 bytes a pixel at QuantumDepth 16), up to 128M pixels (a
1407 // 61 MP camera photo fits on a 4 GB Pi). There is no floor above that
1408 // rule: on a 512 MB BeagleBone a quarter is 16M pixels, which still fits
1409 // a 12 MP photo, and the 32M floor this once had let a 4 KB PNG take
1410 // over 300 MB there, an OOM kill. 32M only when RAM can't be read.
1411 {
1412 1888 long long ram = (long long)sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGESIZE);
1413
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 long long pixels = ram > 0 ? ram / 4 / 8 : 32LL * 1024 * 1024;
1414 1888 pixels = std::min(pixels, 128LL * 1024 * 1024);
1415
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 MagickLib::SetMagickResourceLimit(MagickLib::PixelsResource, (MagickLib::magick_int64_t)pixels);
1416 }
1417
1418 // Parse our arguments first, override any defaults
1419
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 parseArguments(argc, argv);
1420
1421 // Check to see if we were restarted and should skip sending blanking data at startup
1422
4/8
✓ 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.
✗ Branch 6 not taken.
✓ Branch 7 taken 1888 times.
3776 if (FileExists(FPP_DIR_MEDIA("/tmp/fppd_restarted"))) {
1423 ✗ unlink(FPP_DIR_MEDIA("/tmp/fppd_restarted").c_str());
1424 ✗ setSetting("restarted", 1);
1425 }
1426
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 std::string restartFlag = getSetting("restartFlag", "0");
1427
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1888 times.
1888 if (restartFlag == "2") {
1428 // if this was a restart and a "quick" was requested, then mark as restarted
1429 ✗ setSetting("restartFlag", "0", true);
1430 }
1431
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
1888 if (loggingToFile()) {
1432
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 logVersionInfo();
1433 }
1434
1435
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
2048 FileMonitor::INSTANCE.AddFile(
1436
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
7392 "FPPD:Settings", FPP_DIR_MEDIA("/settings"), [argv]() {
1437 288 LogDebug(VB_SETTING, "Settings file changed, reloading settings\n");
1438 288 LoadSettings(argv[0], true); }, true);
1439
1440 // Start functioning
1441
3/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1883 times.
1888 if (getSettingInt("daemonize")) {
1442
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 CreateDaemon();
1443 }
1444
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1884 times.
1888 if (s_argVolume >= 0) {
1445
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 setVolume(s_argVolume);
1446 }
1447
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1888 times.
1888 PLAT_GPIO_CLASS* gpioUtil = new PLAT_GPIO_CLASS();
1448
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
1888 PinCapabilities::InitGPIO("FPPD", gpioUtil);
1449
1450 // No srand() needed: FPPrand() seeds a per-thread PRNG on first use.
1451
1452
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 CommandManager::INSTANCE.Init();
1453
4/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✓ Branch 5 taken 1852 times.
1888 if (getSetting("MQTTHost") != "") {
1454
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 LogInfo(VB_GENERAL, "Creating MQTT\n");
1455
8/18
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 36 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 36 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 36 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 36 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
110 mqtt = new MosquittoClient(getSetting("MQTTHost").c_str(), getSettingInt("MQTTPort"), getSetting("MQTTPrefix").c_str());
1456
1457
19/56
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 36 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 36 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 36 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 36 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✓ Branch 17 taken 36 times.
✓ Branch 18 taken 36 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 36 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 36 times.
✗ Branch 23 not taken.
✓ Branch 24 taken 36 times.
✗ Branch 25 not taken.
✓ Branch 26 taken 36 times.
✗ Branch 27 not taken.
✓ Branch 28 taken 36 times.
✗ Branch 29 not taken.
✓ Branch 30 taken 36 times.
✗ Branch 31 not taken.
✓ Branch 32 taken 36 times.
✗ Branch 33 not taken.
✓ Branch 34 taken 36 times.
✗ Branch 35 not taken.
✗ Branch 36 not taken.
✓ Branch 37 taken 36 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 not taken.
✗ Branch 45 not taken.
✗ Branch 46 not taken.
✗ Branch 47 not taken.
✗ Branch 48 not taken.
✗ Branch 49 not taken.
✗ Branch 50 not taken.
✗ Branch 51 not taken.
✗ Branch 52 not taken.
✗ Branch 53 not taken.
✗ Branch 54 not taken.
✗ Branch 55 not taken.
252 if (!mqtt || !mqtt->Init(getSetting("MQTTUsername").c_str(), getSetting("MQTTPassword").c_str(), getSetting("MQTTCaFile").c_str())) {
1458 ✗ LogWarn(VB_CONTROL, "MQTT Init failed. Starting without MQTT. -- Maybe MQTT host doesn't resolve\n");
1459 ✗ WarningHolder::AddWarning(4, "MQTT init failed at startup - check the MQTT host/port settings");
1460 } else {
1461
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 Events::AddEventHandler(mqtt);
1462 }
1463 }
1464
1465 std::function<void(const std::string&, const std::string&)>
1466 1888 fppd_callback = [](const std::string& topic_in,
1467 const std::string& payload) {
1468 ✗ LogDebug(VB_CONTROL, "System Callback for %s\n", topic_in.c_str());
1469
1470 ✗ if (0 == topic_in.compare(topic_in.length() - 5, 5, "/stop")) {
1471 ✗ LogInfo(VB_CONTROL, "Stopping FPP for request: %s\n", topic_in.c_str());
1472 ✗ ShutdownFPPD(false);
1473 ✗ } else if (0 == topic_in.compare(topic_in.length() - 8, 8, "/restart")) {
1474 ✗ LogInfo(VB_CONTROL, "Restarting FPP for request: %s\n", topic_in.c_str());
1475 ✗ ShutdownFPPD(true);
1476 } else {
1477 ✗ LogWarn(VB_CONTROL, "Invalid request in fppd_callback %s\n", topic_in.c_str());
1478 }
1479 1888 };
1480 std::function<void(const std::string&, const std::string&)>
1481 1888 system_callback = [](const std::string& topic_in,
1482 const std::string& payload) {
1483 ✗ LogDebug(VB_CONTROL, "System Callback for %s\n", topic_in.c_str());
1484 ✗ std::string rc = "";
1485
1486 ✗ if (0 == topic_in.compare(topic_in.length() - 9, 9, "/shutdown")) {
1487 ✗ LogInfo(VB_CONTROL, "Shutting down OS for request: %s\n", topic_in.c_str());
1488 ✗ urlPost("http://localhost/api/system/shutdown", "", rc);
1489 ✗ } else if (0 == topic_in.compare(topic_in.length() - 8, 8, "/restart")) {
1490 ✗ LogInfo(VB_CONTROL, "Restarting FPP for request: %s\n", topic_in.c_str());
1491 ✗ urlPost("http://localhost/api/system/reboot", "", rc);
1492 } else {
1493 ✗ LogWarn(VB_CONTROL, "Invalid request in mqtt_system_callback %s\n", topic_in.c_str());
1494 }
1495 1888 };
1496
1497
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
3776 Events::AddCallback("/system/fppd/#", fppd_callback);
1498
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
3776 Events::AddCallback("/system/shutdown", system_callback);
1499
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
1888 Events::AddCallback("/system/restart", system_callback);
1500
1501
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 WarningHolder::StartNotifyThread();
1502
1503 // Check if boot delay is in progress and add warning
1504
4/8
✓ 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.
✗ Branch 6 not taken.
✓ Branch 7 taken 1888 times.
3776 if (FileExists(getFPPMediaDir("/tmp/boot_delay"))) {
1505 ✗ std::string delayInfo = GetFileContents(getFPPMediaDir("/tmp/boot_delay"));
1506 ✗ TrimWhiteSpace(delayInfo);
1507 ✗ std::string msg;
1508
1509 // Parse the delay info - format is "startTime,duration" or "startTime,auto"
1510 ✗ size_t commaPos = delayInfo.find(',');
1511 ✗ if (commaPos != std::string::npos && commaPos + 1 < delayInfo.length()) {
1512 ✗ std::string delayValue = delayInfo.substr(commaPos + 1);
1513 ✗ if (delayValue == "auto") {
1514 ✗ msg = "Boot delay in progress: Waiting for valid system time (NTP/RTC) - up to 5 minutes";
1515 } else {
1516 ✗ msg = "Boot delay in progress: Waiting " + delayValue + " seconds before starting FPPD";
1517 }
1518 ✗ } else {
1519 // Fallback for old format or malformed data
1520 ✗ if (delayInfo == "auto") {
1521 ✗ msg = "Boot delay in progress: Waiting for valid system time (NTP/RTC) - up to 5 minutes";
1522 } else {
1523 ✗ msg = "Boot delay in progress: Waiting " + delayInfo + " seconds before starting FPPD";
1524 }
1525 }
1526 ✗ WarningHolder::AddWarning(12, msg);
1527 ✗ }
1528
1529
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 LogInfo(VB_GENERAL, "Creating Scheduler, Playlist, and Sequence\n");
1530
3/8
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1888 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
1888 scheduler = new Scheduler();
1531
3/8
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1888 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
1888 sequence = new Sequence();
1532
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 LogInfo(VB_GENERAL, "Creation of Scheduler, Playlist, and Sequence Complete\n");
1533
1534
3/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1887 times.
1888 if (!MultiSync::INSTANCE.Init()) {
1535 1 ExitWithoutTeardown(EXIT_FAILURE);
1536 }
1537
1538
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 Sensors::INSTANCE.DetectHWSensors();
1539
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 initCape();
1540
1541
6/10
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1887 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✓ Branch 9 taken 1882 times.
3774 if (FileExists(FPP_DIR_CONFIG("/sensors.json"))) {
1542
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 Json::Value root;
1543
6/10
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✓ Branch 9 taken 1 times.
10 if (LoadJsonFromFile(FPP_DIR_CONFIG("/sensors.json"), root, JsonRoot::Object)) {
1544
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 Sensors::INSTANCE.addSensors(root["sensors"]);
1545 }
1546 5 }
1547
1548
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 Player::INSTANCE.Init();
1549
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 PluginManager::INSTANCE.init();
1550
1551
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 InitMediaOutput();
1552 #ifdef HAS_AES67_GSTREAMER
1553
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 AES67Manager::INSTANCE.Init();
1554
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 AES67Manager::INSTANCE.ApplyConfig();
1555 #endif
1556 #ifdef HAS_OPUS_RTP_GSTREAMER
1557
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 OpusRTPManager::INSTANCE.Init();
1558
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 OpusRTPManager::INSTANCE.ApplyConfig();
1559 #endif
1560
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
1887 VideoInputManager::Instance().Init();
1561
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
1887 VideoOutputManager::Instance().Init();
1562 #ifdef HAS_RTSP_OUTPUT_GSTREAMER
1563 // After the video managers: the RTSP mounts read the intervideo channels
1564 // those publish on, and its audio bridges want the PipeWire graph up.
1565
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 RTSPOutputManager::INSTANCE.Init();
1566
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 RTSPOutputManager::INSTANCE.ApplyConfig();
1567 #endif
1568
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 PixelOverlayManager::INSTANCE.Initialize();
1569
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 PingManager::INSTANCE.Initialize();
1570
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 WLEDAPIResponder::INSTANCE.Initialize();
1571
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 WLEDAudioSync::INSTANCE.Initialize();
1572
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 InitializeChannelOutputs();
1573
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 PluginManager::INSTANCE.loadUserPlugins();
1574
1575
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 InitEffects();
1576
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 ChannelTester::INSTANCE.RegisterCommands();
1577
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 LocaleHolder::RegisterCommands();
1578
1579
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 multiSync->WriteRuntimeInfoFile();
1580
1581
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1887 MainLoop();
1582 // DISABLED: Stats collected while fppd is shutting down
1583 // incomplete and cause problems with summary
1584 // PublishStatsForce("Shutdown"); // not background
1585
1586 // Sequence presets (SEQUENCE_STOPPED and the FSEQ frame-triggered commands)
1587 // are queued for the main loop rather than run at their call site, which
1588 // holds the sequence locks. A preset queued during the final loop
1589 // iteration -- a sequence that ended, or a stop command, landing just as
1590 // runMainFPPDLoop goes to 0 -- has no later tick to fire on. Drain it here
1591 // instead: still before FPPD_STOPPED, so the ordering a user would expect
1592 // between the two is preserved, and well before CommandManager::Cleanup().
1593
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 if (sequence) {
1594
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 sequence->drainPendingPresets();
1595 }
1596
1597
3/6
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1867 times.
3734 if (CommandManager::INSTANCE.HasPreset("FPPD_STOPPED")) {
1598 ✗ CommandManager::INSTANCE.TriggerPreset("FPPD_STOPPED");
1599 }
1600
1601 // turn off processing of events so we don't get
1602 // events while we are shutting down
1603
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 Events::PrepareForShutdown();
1604
1605 // Scheduled commands run on detached threads; give any still running a
1606 // moment to finish before the media, outputs and command registry below go
1607 // away under them. Bounded so a stuck command can't hold up shutdown.
1608
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 Scheduler::WaitForCommandThreads(2000);
1609
1610 // Tear down mDNS *first* so the goodbye packets for our advertised
1611 // services go out on the wire before anything slower (media output
1612 // shutdown, channel-output close, or the WS responder draining its
1613 // client list) has a chance to hang. If shutdown is force-killed
1614 // partway through (e.g. fppd_stop's 1s timeout), at least other
1615 // discovery clients won't keep our entries cached for the TTL.
1616
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 MDNSManager::INSTANCE.Cleanup();
1617
1618 #ifdef HAS_AUDIO_LEVEL_MONITOR
1619 // Its poll thread reads the level elements of the pipelines torn down
1620 // below; stop it, and drop the meters, first. Nothing else ever joined
1621 // that thread, so it outlived the pipelines and was never reaped.
1622
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 AudioLevelMonitor::INSTANCE.Shutdown();
1623 #endif
1624 #ifdef HAS_RTSP_OUTPUT_GSTREAMER
1625 // Before the video managers it reads from, so no client is still being
1626 // served off an intervideo channel that is about to go away.
1627
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 RTSPOutputManager::INSTANCE.Shutdown();
1628 #endif
1629
2/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
1867 VideoInputManager::Instance().Shutdown();
1630
2/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
1867 VideoOutputManager::Instance().Shutdown();
1631 #ifdef HAS_AES67_GSTREAMER
1632
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 AES67Manager::INSTANCE.Shutdown();
1633 #endif
1634 #ifdef HAS_OPUS_RTP_GSTREAMER
1635
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 OpusRTPManager::INSTANCE.Shutdown();
1636 #endif
1637
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 CleanupMediaOutput();
1638
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 CloseEffects();
1639
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 CloseChannelOutputs();
1640
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 WLEDAPIResponder::INSTANCE.Cleanup();
1641
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 WLEDAudioSync::INSTANCE.Cleanup();
1642
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 PingManager::INSTANCE.Cleanup();
1643
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 OutputMonitor::INSTANCE.Cleanup();
1644 // Plugins must clean up before CommandManager: their unregister hooks
1645 // pull the commands they own out of the registry so CommandManager's
1646 // bulk delete below doesn't free objects a plugin still references.
1647
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 PluginManager::INSTANCE.Cleanup();
1648
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 CommandManager::INSTANCE.Cleanup();
1649
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 MultiSync::INSTANCE.ShutdownSync();
1650
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 GPIOManager::INSTANCE.Cleanup();
1651
1652 // Stop the notify thread before the deletes below, not after: its listeners
1653 // run arbitrary code on a warning change or notify timeout, and the status
1654 // listeners among them build a full status payload from scheduler and
1655 // sequence. Leaving it running across the deletes gives every shutdown a
1656 // window where a warning change reads freed objects.
1657
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 WarningHolder::StopNotifyThread();
1658
1659 // Cleared before the delete, not left dangling: the crash handler and any
1660 // straggler that checks these for null must not reach a freed object.
1661 1867 Scheduler* deadScheduler = scheduler;
1662 1867 scheduler = nullptr;
1663
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 delete deadScheduler;
1664 1867 Sequence* deadSequence = sequence;
1665 1867 sequence = nullptr;
1666
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 delete deadSequence;
1667 1867 runMainFPPDLoop = -1;
1668
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 Sensors::INSTANCE.Close();
1669
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 RecurringTasks::INSTANCE.Close();
1670
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 Variables::INSTANCE.Close();
1671
1672
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 1831 times.
1867 if (mqtt) {
1673
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 Events::RemoveEventHandler(mqtt);
1674
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 delete mqtt;
1675 }
1676
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 delete gpioUtil;
1677
1678 // No DestroyMagick()/curl_global_cleanup(): every path from here ends in
1679 // execlp() or _exit(), which drop both libraries with the address space,
1680 // and tearing them down first pulled their state out from under any
1681 // detached thread (a scheduled command, an HTTP fetch) still inside them.
1682
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 std::string logLevelString = FPPLogger::INSTANCE.GetLogLevelString();
1683
1684 // Decide the restart resume HERE, while the log file is still open:
1685 // everything logged after CloseOpenFiles() below is silently discarded
1686 // (which is why "Performing Restart." has never appeared in a support log),
1687 // and "my show never came back after a restart" is otherwise
1688 // indistinguishable from "no playlist was running" (issue #2727).
1689 //
1690 // A manually-started playlist is normally NOT resumed across a restart -- a
1691 // user-requested restart shouldn't silently re-start a show they stopped by
1692 // restarting. A recovery restart we initiated ourselves is the opposite
1693 // case: the playlist was running, nobody asked for it to stop, and coming
1694 // back idle strands every synced remote mid-clip on a frozen frame.
1695 1867 std::string resumePlaylist;
1696 1867 std::string resumePosition;
1697 1867 std::string resumeRepeat;
1698 1867 bool resumeUnscheduled = false;
1699
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 1857 times.
1867 if (restartFPPD) {
1700
5/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 4 times.
16 if ((Player::INSTANCE.GetStatus() == FPP_STATUS_PLAYLIST_PLAYING) &&
1701
5/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
6 (Player::INSTANCE.WasScheduled() || RestartShouldResumePlaylist())) {
1702
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 resumePlaylist = Player::INSTANCE.GetPlaylistName();
1703
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 resumePosition = std::to_string(Player::INSTANCE.GetResumePosition());
1704
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 resumeRepeat = std::to_string(Player::INSTANCE.GetRepeat());
1705 // Force-start only what the scheduler won't restore itself. A
1706 // scheduled playlist is left to CheckIfShouldBePlayingNow(), which
1707 // is schedule-window aware -- forcing that one would restart a show
1708 // whose window closed while we were restarting.
1709
5/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
6 resumeUnscheduled = RestartShouldResumePlaylist() && !Player::INSTANCE.WasScheduled();
1710
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 LogInfo(VB_GENERAL, "Restart: resuming playlist '%s' at position %s (repeat %s, unscheduled resume %d)\n",
1711 resumePlaylist.c_str(), resumePosition.c_str(), resumeRepeat.c_str(),
1712 resumeUnscheduled ? 1 : 0);
1713 } else {
1714
6/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
4 LogInfo(VB_GENERAL, "Restart: not resuming a playlist (status %d, scheduled %d, resume requested %d)\n",
1715 (int)Player::INSTANCE.GetStatus(), Player::INSTANCE.WasScheduled(),
1716 RestartShouldResumePlaylist() ? 1 : 0);
1717 }
1718 }
1719
1720
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 CloseCommand();
1721 // Not closed here: threads still running (a media teardown, GStreamer's
1722 // and PipeWire's) use them until the exec or _exit() below, which close
1723 // them; the restarted fppd must just not inherit them.
1724
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 MarkOpenFilesCloseOnExec();
1725
1726
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 WarningHolder::ClearWarningsFile();
1727
1728
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 1857 times.
1867 if (restartFPPD) {
1729
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
20 remove(FPP_DIR_MEDIA("/fpp-info.json").c_str());
1730
1731
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 std::string fppdDir = getFPPDDir("/src/fppd");
1732
3/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 9 times.
10 const char* dmode = getSettingInt("daemonize") ? "-d" : "-f";
1733
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
10 if (!resumePlaylist.empty()) {
1734
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 if (resumeUnscheduled) {
1735 // We asked for this restart, so the show must come back even
1736 // when no schedule would start it (issue #2727). -p/-P alone
1737 // only restore the position of a playlist the scheduler is
1738 // already starting.
1739 2 execlp(fppdDir.c_str(), fppdDir.c_str(), dmode, "--log-level", logLevelString.c_str(), "-r", "-p", resumePlaylist.c_str(), "-P", resumePosition.c_str(), "--resume-unscheduled", resumeRepeat.c_str(), NULL);
1740 }
1741 4 execlp(fppdDir.c_str(), fppdDir.c_str(), dmode, "--log-level", logLevelString.c_str(), "-r", "-p", resumePlaylist.c_str(), "-P", resumePosition.c_str(), NULL);
1742 } else {
1743 4 execlp(fppdDir.c_str(), fppdDir.c_str(), dmode, "--log-level", logLevelString.c_str(), "-r", NULL);
1744 }
1745 ✗ }
1746
1747 // Every subsystem was torn down explicitly above, so the C++ global/static
1748 // singleton destructors that would run on a normal return are redundant --
1749 // and crash-prone. Two distinct failure modes show up in the crash reports:
1750 // * macOS: a noexcept destructor locks a mutex belonging to an
1751 // already-destroyed singleton (or one a still-draining drogon thread is
1752 // touching); libc++ throws EINVAL ("mutex lock failed: Invalid
1753 // argument") from that lock, which escapes the noexcept dtor and calls
1754 // std::terminate.
1755 // * Linux: a file-static std::thread (the Events publish thread, the
1756 // WarningHolder notify thread, ...) that is still joinable when
1757 // __cxa_finalize destroys it -> std::thread::~thread calls
1758 // std::terminate. The orderly shutdown above joins these, but a late
1759 // restart (e.g. an event reconfigure during the drawn-out shutdown) can
1760 // leave one joinable again. This is the single most common fppd crash
1761 // on the dashboard, so the same _exit() shortcut macOS uses is applied
1762 // on Linux too.
1763 // Flush and exit now, skipping the redundant (and crash-prone) teardown.
1764
1/2
✓ Branch 0 taken 1857 times.
✗ Branch 1 not taken.
1857 fflush(nullptr);
1765 1857 _exit(0);
1766 return 0;
1767 ✗ }
1768
1769 13 void ShutdownFPPDCallback(bool restart) {
1770 13 LogInfo(VB_GENERAL, "Shutting down main loop.\n");
1771
1772 13 restartFPPD = restart;
1773 13 runMainFPPDLoop = 0;
1774 13 }
1775
1776 // ──────────────────────────────────────────────────────────────────────────────
1777 // Main-loop stall watchdog (issue #2727).
1778 //
1779 // The main loop services the MultiSync control socket, the `fpp` command socket
1780 // and (in remote mode) media processing. The HTTP API answers on its own
1781 // threads, so when something blocks the main loop the box looks alive from the
1782 // outside while doing nothing: the UI keeps reporting the last status it saw
1783 // ("playing" forever), the received-sync-packet counters stop moving because
1784 // ProcessControlPacket() never runs again, and "Restart FPPD" is a no-op
1785 // because that command is delivered through the same unserviced command
1786 // socket. That combination is exactly what a wedged remote looks like, and
1787 // nothing short of a power cycle recovers it.
1788 //
1789 // So heartbeat the loop and supervise it from a thread it cannot block: once
1790 // the loop goes quiet, dump a backtrace of every thread (the diagnostic that
1791 // has been missing from every report of this), tell the UI that the status it
1792 // is showing is stale, and if the loop never comes back, exit so systemd
1793 // (Restart=always) starts a clean process. Exiting rather than re-exec'ing is
1794 // deliberate: a stalled loop normally means a wedged decoder/DRM fd, and open
1795 // fds without CLOEXEC survive exec — only a fresh process gets them back from
1796 // the kernel.
1797 //
1798 // The show is restarted through the same resume path a wedge restart uses.
1799 // The snapshot lives in tmpfs so it can never resurrect a playlist after a
1800 // reboot, and it is taken by the main loop (never read out of Player from the
1801 // watchdog thread, which would race with whatever the stalled thread is doing).
1802 // ──────────────────────────────────────────────────────────────────────────────
1803 static constexpr const char* STALL_RESUME_STATE_FILE = "/run/fppd/stall_resume";
1804 static std::atomic<long long> s_mainLoopHeartbeat{ 0 };
1805 static std::atomic<bool> s_mainLoopWatchdogRunning{ false };
1806
1807 // Which call the main loop last entered (SetMainLoopPhase, in common.cpp, so
1808 // code inside libfpp can narrow it further). Only ever set to string literals,
1809 // so the watchdog can read the pointer from another thread and print it even
1810 // while the loop is wedged inside that call. A stalled loop that gdb cannot
1811 // unwind (the 2026-07-27 FPPv4-4 capture: the main thread's backtrace stopped
1812 // at a single bare libc frame) still tells us which call it is in.
1813 #define MAIN_LOOP_PHASE(p) SetMainLoopPhase(p)
1814
1815 static std::mutex s_mainLoopShowLock;
1816 static std::string s_mainLoopShowPlaylist;
1817 static int s_mainLoopShowPosition = 0;
1818 static int s_mainLoopShowRepeat = 0;
1819 static bool s_mainLoopShowScheduled = false;
1820
1821 473393 static long long MonotonicMS() {
1822 // Deliberately not GetTimeMS(): that is wall clock, and an NTP step would
1823 // read as a multi-second "stall".
1824
1/2
✓ Branch 0 taken 473393 times.
✗ Branch 1 not taken.
473393 return std::chrono::duration_cast<std::chrono::milliseconds>(
1825 946786 std::chrono::steady_clock::now().time_since_epoch())
1826 946786 .count();
1827 }
1828
1829 // Called from the main loop only.
1830 445665 static void RecordMainLoopShowSnapshot() {
1831 445665 std::unique_lock<std::mutex> lock(s_mainLoopShowLock, std::try_to_lock);
1832
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 445665 times.
445665 if (!lock.owns_lock()) {
1833 ✗ return;
1834 }
1835
3/4
✓ Branch 0 taken 445665 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290232 times.
✓ Branch 3 taken 155433 times.
445665 if (Player::INSTANCE.GetStatus() == FPP_STATUS_PLAYLIST_PLAYING) {
1836
1/2
✓ Branch 0 taken 290232 times.
✗ Branch 1 not taken.
290232 s_mainLoopShowPlaylist = Player::INSTANCE.GetPlaylistName();
1837
1/2
✓ Branch 0 taken 290232 times.
✗ Branch 1 not taken.
290232 s_mainLoopShowPosition = Player::INSTANCE.GetPosition() - 1;
1838
1/2
✓ Branch 0 taken 290232 times.
✗ Branch 1 not taken.
290232 s_mainLoopShowRepeat = Player::INSTANCE.GetRepeat();
1839
1/2
✓ Branch 0 taken 290232 times.
✗ Branch 1 not taken.
290232 s_mainLoopShowScheduled = Player::INSTANCE.WasScheduled();
1840
2/2
✓ Branch 0 taken 956 times.
✓ Branch 1 taken 154477 times.
155433 } else if (!s_mainLoopShowPlaylist.empty()) {
1841 956 s_mainLoopShowPlaylist.clear();
1842 }
1843
1/2
✓ Branch 0 taken 445665 times.
✗ Branch 1 not taken.
445665 }
1844
1845 // Last act before the watchdog exits the process: leave behind whatever the
1846 // show was doing so the fresh process picks it back up.
1847 1 static void WriteStallResumeState() {
1848 1 std::unique_lock<std::mutex> lock(s_mainLoopShowLock, std::try_to_lock);
1849
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
1 if (!lock.owns_lock() || s_mainLoopShowPlaylist.empty()) {
1850 ✗ return;
1851 }
1852
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::ofstream out(STALL_RESUME_STATE_FILE, std::ios::trunc);
1853
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (!out) {
1854 ✗ return; // no /run/fppd (macOS, docker) -- nothing to resume from
1855 }
1856
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 out << s_mainLoopShowPlaylist << "\n"
1857
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 << s_mainLoopShowPosition << "\n"
1858
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 << s_mainLoopShowRepeat << "\n"
1859
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 << (s_mainLoopShowScheduled ? 1 : 0) << "\n";
1860
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 }
1861
1862 // Startup counterpart: turn a stall-restart snapshot into the same settings a
1863 // self-restart would have passed on the command line, then consume the file.
1864 1885 static void ApplyStallResumeState() {
1865
3/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1884 times.
✓ Branch 3 taken 1 times.
1885 if (!FileExists(STALL_RESUME_STATE_FILE)) {
1866 1884 return;
1867 }
1868 1 std::string playlist;
1869 1 int position = 0, repeat = 0, scheduled = 0;
1870 {
1871
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::ifstream in(STALL_RESUME_STATE_FILE);
1872 1 std::string line;
1873
7/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 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.
1 if (std::getline(in, playlist) && std::getline(in, line)) {
1874 1 position = atoi(line.c_str());
1875
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 if (std::getline(in, line)) {
1876 1 repeat = atoi(line.c_str());
1877 }
1878
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 if (std::getline(in, line)) {
1879 1 scheduled = atoi(line.c_str());
1880 }
1881 }
1882 1 }
1883 1 (void)remove(STALL_RESUME_STATE_FILE);
1884
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (playlist.empty()) {
1885 ✗ return;
1886 }
1887
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_GENERAL, "Previous fppd was killed by the main-loop watchdog while playing '%s' — resuming at position %d\n",
1888 playlist.c_str(), position);
1889
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 SetSetting("resumePlaylist", playlist);
1890
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 SetSetting("resumePosition", position);
1891
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!scheduled) {
1892 // Same rule as a wedge restart: the scheduler restores a scheduled
1893 // playlist itself (and is window-aware about it); an unscheduled show
1894 // has to be force-started or every synced remote is stranded.
1895
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 SetSetting("resumeUnscheduled", 1);
1896
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 SetSetting("resumeRepeat", repeat);
1897 }
1898
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 }
1899
1900 #ifndef PLATFORM_OSX
1901 // Read a small /proc file into a stack buffer. Everything in this dump is
1902 // deliberately heap-free: the whole point of running it is that some thread is
1903 // stuck, and if what it is stuck on is the malloc arena lock then anything that
1904 // allocates would wedge the watchdog too.
1905 136 static ssize_t ReadProcFile(const char* path, char* buf, size_t bufLen) {
1906 136 int fd = open(path, O_RDONLY | O_CLOEXEC);
1907
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 136 times.
136 if (fd < 0) {
1908 ✗ return -1;
1909 }
1910 136 ssize_t len = read(fd, buf, bufLen - 1);
1911 136 close(fd);
1912
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 136 times.
136 if (len < 0) {
1913 ✗ len = 0;
1914 }
1915 136 buf[len] = 0;
1916
4/6
✓ Branch 0 taken 238 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 102 times.
✓ Branch 3 taken 136 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 136 times.
238 while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == ' ')) {
1917 102 buf[--len] = 0;
1918 }
1919 136 return len;
1920 }
1921
1922 // Turn a runtime address into "libfoo.so+0x1234" so a PC captured on someone
1923 // else's Pi can be resolved offline against the matching binary. gdb prints
1924 // the raw address only, which is useless once the process is gone.
1925 34 static void ResolveAddr(unsigned long addr, char* out, size_t outLen) {
1926 34 snprintf(out, outLen, "0x%lx", addr);
1927
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 int fd = open("/proc/self/maps", O_RDONLY | O_CLOEXEC);
1928
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if (fd < 0) {
1929 34 return;
1930 }
1931 char buf[4096];
1932 char line[512];
1933 34 size_t lineLen = 0;
1934 ssize_t got;
1935
2/4
✓ Branch 0 taken 544 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 544 times.
✗ Branch 3 not taken.
544 while ((got = read(fd, buf, sizeof(buf))) > 0) {
1936
2/2
✓ Branch 0 taken 2165562 times.
✓ Branch 1 taken 510 times.
2166072 for (ssize_t i = 0; i < got; i++) {
1937
2/2
✓ Branch 0 taken 2145264 times.
✓ Branch 1 taken 20298 times.
2165562 if (buf[i] != '\n') {
1938
1/2
✓ Branch 0 taken 2145264 times.
✗ Branch 1 not taken.
2145264 if (lineLen < sizeof(line) - 1) {
1939 2145264 line[lineLen++] = buf[i];
1940 }
1941 2145264 continue;
1942 }
1943 20298 line[lineLen] = 0;
1944 20298 lineLen = 0;
1945
1946 20298 unsigned long start = 0, end = 0, fileOff = 0;
1947 char perms[8], path[256];
1948 20298 path[0] = 0;
1949 40596 if (sscanf(line, "%lx-%lx %7s %lx %*s %*u %255s",
1950 20298 &start, &end, perms, &fileOff, path) >= 4 &&
1951
7/10
✓ Branch 0 taken 20298 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20298 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 34 times.
✓ Branch 5 taken 20264 times.
✓ Branch 6 taken 34 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 34 times.
✓ Branch 9 taken 20264 times.
20298 addr >= start && addr < end && path[0] == '/') {
1952 34 const char* base = strrchr(path, '/');
1953
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
34 snprintf(out, outLen, "%s+0x%lx", base ? base + 1 : path,
1954
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 addr - start + fileOff);
1955
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 close(fd);
1956 34 return;
1957 }
1958 }
1959 }
1960 ✗ close(fd);
1961 }
1962
1963 // One line per thread naming the syscall it is parked in, the kernel wait
1964 // channel, and the resolved user-space PC. This is the diagnostic gdb could
1965 // not give us: on 32-bit ARM the unwinder gives up inside libc's lock/futex
1966 // stubs, so the one thread that matters -- the stalled one -- comes back as a
1967 // single unnamed frame. /proc always answers.
1968 2 static void DumpThreadStates(void) {
1969 struct LinuxDirent64 {
1970 uint64_t d_ino;
1971 int64_t d_off;
1972 unsigned short d_reclen;
1973 unsigned char d_type;
1974 char d_name[];
1975 };
1976
1977
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 int fd = open("/proc/self/task", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
1978
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (fd < 0) {
1979 ✗ return;
1980 }
1981 // glibc futexes on the mutex word itself, which for a std::mutex is offset
1982 // 0 -- so a "syscall=98(<addr>,...)" line below whose first argument is one
1983 // of these is naming the exact lock the thread is stuck on. Anything else
1984 // means the contended lock is internal to GStreamer/PipeWire/libc.
1985
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_GENERAL, "Main loop watchdog: known locks: mediaOutputLock=%p\n",
1986 (void*)&mediaOutputLock);
1987
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_GENERAL, "Main loop watchdog: thread states (tid name state wchan syscall pc):\n");
1988
1989 char dirBuf[8192];
1990 long n;
1991
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 while ((n = syscall(SYS_getdents64, fd, dirBuf, sizeof(dirBuf))) > 0) {
1992
2/2
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 2 times.
40 for (long off = 0; off < n;) {
1993 38 LinuxDirent64* d = (LinuxDirent64*)(dirBuf + off);
1994 38 off += d->d_reclen;
1995
3/4
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 34 times.
38 if (d->d_name[0] < '0' || d->d_name[0] > '9') {
1996 4 continue;
1997 }
1998
1999 34 char path[128], comm[64] = "?", wchan[64] = "?", sc[256] = "?", stat[512];
2000 34 snprintf(path, sizeof(path), "/proc/self/task/%s/comm", d->d_name);
2001
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 ReadProcFile(path, comm, sizeof(comm));
2002 34 snprintf(path, sizeof(path), "/proc/self/task/%s/wchan", d->d_name);
2003
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 ReadProcFile(path, wchan, sizeof(wchan));
2004 34 snprintf(path, sizeof(path), "/proc/self/task/%s/syscall", d->d_name);
2005
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 ReadProcFile(path, sc, sizeof(sc));
2006
2007 // /proc/<tid>/stat: the state letter is the field after the
2008 // parenthesised comm, which can itself contain spaces/parens.
2009 34 char state = '?';
2010 34 snprintf(path, sizeof(path), "/proc/self/task/%s/stat", d->d_name);
2011
2/4
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
34 if (ReadProcFile(path, stat, sizeof(stat)) > 0) {
2012 34 const char* p = strrchr(stat, ')');
2013
3/6
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 34 times.
✗ Branch 5 not taken.
34 if (p && p[1] && p[2]) {
2014 34 state = p[2];
2015 }
2016 }
2017
2018 // "<nr> <arg0..arg5> <sp> <pc>", or "running", or "-1 <sp> <pc>".
2019 34 long nr = -1;
2020 34 unsigned long args[6] = { 0 }, sp = 0, pc = 0;
2021 34 int fields = sscanf(sc, "%ld 0x%lx 0x%lx 0x%lx 0x%lx 0x%lx 0x%lx 0x%lx 0x%lx",
2022 &nr, &args[0], &args[1], &args[2], &args[3], &args[4],
2023 &args[5], &sp, &pc);
2024 34 char pcStr[128] = "-";
2025
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 if (fields == 9) {
2026
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 ResolveAddr(pc, pcStr, sizeof(pcStr));
2027 // arg0 of a futex wait is the lock address -- the only way to
2028 // tell "two threads fighting over one mutex" from "two threads
2029 // idle on their own condvars".
2030
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 LogErr(VB_GENERAL, " tid %-6s %-16s %c %-24s syscall=%ld(0x%lx,0x%lx,0x%lx) pc=%s\n",
2031 d->d_name, comm, state, wchan, nr, args[0], args[1], args[2], pcStr);
2032 } else {
2033 ✗ LogErr(VB_GENERAL, " tid %-6s %-16s %c %-24s syscall=%s\n",
2034 d->d_name, comm, state, wchan, sc);
2035 }
2036 }
2037 }
2038
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 close(fd);
2039 }
2040 #else
2041 static void DumpThreadStates(void) {}
2042 #endif
2043
2044 // Kill the process now, leaving the show behind for the next one. Used for
2045 // both a stalled loop and a stalled shutdown.
2046 1 static void WatchdogExit(const char* why, long long stalledMS) {
2047 // Everything below can block if the stalled thread happens to hold a
2048 // libc/log lock, so make sure the process dies either way first (both
2049 // calls are async-signal-safe; the crash handler uses the same trick).
2050 struct sigaction almAct;
2051 1 memset(&almAct, 0, sizeof(almAct));
2052 1 almAct.sa_handler = SIG_DFL;
2053 1 sigemptyset(&almAct.sa_mask);
2054 1 sigaction(SIGALRM, &almAct, nullptr);
2055 1 alarm(20);
2056
2057
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogErr(VB_GENERAL, "Main loop watchdog: %s for %lld seconds in '%s' — exiting so a clean fppd is started\n",
2058 why, stalledMS / 1000, GetMainLoopPhase());
2059
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 WriteStallResumeState();
2060
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 WarningHolder::WriteWarningsFile();
2061
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 fflush(nullptr);
2062 1 _Exit(1);
2063 }
2064
2065 // The three timeouts, re-read on every pass. Reading them once at startup
2066 // meant a change made while fppd was running was silently ignored -- which is
2067 // exactly how these get set, since they are hand-edited hidden settings with no
2068 // UI (2026-07-27: MainLoopStallRestartTimeout="0" was set to keep a stalled
2069 // remote alive for inspection, and the box restarted itself anyway). Note a
2070 // hand edit of /home/fpp/media/settings still needs something to reload the
2071 // file into fppd; setting it through the UI/API applies immediately.
2072 6626 static void ReadWatchdogTimeouts(int& warnSec, int& restartSec, int& shutdownSec) {
2073 6626 warnSec = getSettingInt("MainLoopStallTimeout", 30);
2074 6626 restartSec = getSettingInt("MainLoopStallRestartTimeout", 180);
2075 6626 shutdownSec = getSettingInt("ShutdownStallTimeout", 120);
2076
2/4
✓ Branch 0 taken 6626 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6626 times.
6626 if (restartSec > 0 && restartSec < warnSec) {
2077 ✗ restartSec = warnSec;
2078 }
2079 6626 }
2080
2081 1885 static void MainLoopWatchdog(int warnSec, int restartSec, int shutdownSec) {
2082
2/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
1885 SetThreadName("FPP-LoopWatchdog");
2083 1885 bool warned = false;
2084 1885 long long shutdownStarted = 0;
2085
1/2
✓ Branch 0 taken 6625 times.
✗ Branch 1 not taken.
6625 while (s_mainLoopWatchdogRunning) {
2086
1/2
✓ Branch 0 taken 4741 times.
✗ Branch 1 not taken.
6625 std::this_thread::sleep_for(std::chrono::seconds(2));
2087
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4741 times.
4741 if (!s_mainLoopWatchdogRunning) {
2088 ✗ break;
2089 }
2090 4741 int prevRestartSec = restartSec;
2091 4741 ReadWatchdogTimeouts(warnSec, restartSec, shutdownSec);
2092
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4741 times.
4741 if (restartSec != prevRestartSec) {
2093 ✗ LogInfo(VB_GENERAL, "Main loop watchdog: restart timeout changed to %d seconds%s\n",
2094 restartSec, restartSec > 0 ? "" : " (restart disabled)");
2095 }
2096
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4741 times.
4741 if (warnSec <= 0) {
2097 ✗ continue; // disabled at runtime
2098 }
2099 // <= 0, not !: main() sets it to -1 partway through teardown, and a
2100 // shutdown that wedged after that point was reported as a loop stall.
2101
2/2
✓ Branch 0 taken 245 times.
✓ Branch 1 taken 4496 times.
4741 if (runMainFPPDLoop <= 0) {
2102 // Teardown is underway. It ends in _exit()/execlp(), so this
2103 // thread simply stops existing on a healthy shutdown; if it is
2104 // still here much later, the shutdown itself wedged and systemd
2105 // will never see the process go away.
2106
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 245 times.
245 if (shutdownSec <= 0) {
2107 ✗ break;
2108 }
2109 245 long long nowMS = MonotonicMS();
2110
2/2
✓ Branch 0 taken 227 times.
✓ Branch 1 taken 18 times.
245 if (!shutdownStarted) {
2111 227 shutdownStarted = nowMS;
2112
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 } else if ((nowMS - shutdownStarted) >= (long long)shutdownSec * 1000) {
2113 ✗ dumpstack_gdb();
2114 ✗ WatchdogExit("shutdown did not complete", nowMS - shutdownStarted);
2115 }
2116 245 continue;
2117 245 }
2118 4496 long long hb = s_mainLoopHeartbeat.load();
2119
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4496 times.
4496 if (hb == 0) {
2120 ✗ continue;
2121 }
2122 4496 long long stalledMS = MonotonicMS() - hb;
2123
2/2
✓ Branch 0 taken 4492 times.
✓ Branch 1 taken 4 times.
4496 if (stalledMS < (long long)warnSec * 1000) {
2124
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4491 times.
4492 if (warned) {
2125 1 LogWarn(VB_GENERAL, "Main loop watchdog: loop resumed (was in '%s')\n",
2126 GetMainLoopPhase());
2127
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.
3 WarningHolder::RemoveWarning(59, "fppd main loop is stalled - the status shown may be stale and sync packets are not being processed");
2128 1 warned = false;
2129 }
2130 4492 continue;
2131 }
2132
2133
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (!warned) {
2134 2 warned = true;
2135 2 LogErr(VB_GENERAL, "Main loop watchdog: no main loop iteration for %lld seconds, stalled in '%s' — dumping thread state\n",
2136 stalledMS / 1000, GetMainLoopPhase());
2137
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
6 WarningHolder::AddWarning(59, "fppd main loop is stalled - the status shown may be stale and sync packets are not being processed");
2138 2 WarningHolder::WriteWarningsFile();
2139 // Cheap and always available, so take it first: it names the
2140 // syscall and lock address of every thread even when the unwinder
2141 // cannot walk the stalled stack.
2142 2 DumpThreadStates();
2143 // The one diagnostic that says *where* it is stuck. gdb stops the
2144 // process briefly; it is already doing nothing.
2145 2 dumpstack_gdb();
2146 }
2147
2148
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
4 if (restartSec > 0 && stalledMS >= (long long)restartSec * 1000) {
2149 1 WatchdogExit("main loop stalled", stalledMS);
2150 }
2151 }
2152 ✗ }
2153
2154 1885 static void StartMainLoopWatchdog() {
2155 // 0 disables the watchdog entirely; a stall shorter than the warn time is
2156 // normal (a large sequence load, a slow media start).
2157 1885 int warnSec = 0, restartSec = 0, shutdownSec = 0;
2158
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 ReadWatchdogTimeouts(warnSec, restartSec, shutdownSec);
2159
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
1885 if (warnSec <= 0) {
2160 ✗ LogInfo(VB_GENERAL, "Main loop watchdog disabled (MainLoopStallTimeout=%d)\n", warnSec);
2161 ✗ return;
2162 }
2163 // Logged so a soak log answers "which timeouts was it actually using?"
2164 // without having to guess at what the settings file said at the time.
2165
2/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
1885 LogInfo(VB_GENERAL, "Main loop watchdog: warn after %ds, restart after %ds%s, shutdown limit %ds\n",
2166 warnSec, restartSec, restartSec > 0 ? "" : " (restart disabled)", shutdownSec);
2167
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 s_mainLoopHeartbeat.store(MonotonicMS());
2168 1885 s_mainLoopWatchdogRunning = true;
2169 // Detached on purpose: it outlives the main loop to supervise the shutdown
2170 // that follows it, and every exit path from there is _exit()/execlp(),
2171 // which takes the thread with it. (A joinable file-static std::thread
2172 // reaching static destruction is its own crash -- see ShutdownFPPD.)
2173
2/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
1885 std::thread(MainLoopWatchdog, warnSec, restartSec, shutdownSec).detach();
2174 }
2175
2176 // How long to wait after statistics are switched on before the first upload.
2177 // Long enough that somebody who ticked the wrong box, or was looking around the
2178 // setup wizard, can put it back before anything is sent.
2179 #define STATS_OPT_IN_GRACE_SECONDS 120
2180
2181 // Publish when the user turns statistics ON, rather than leaving them waiting
2182 // for the next scheduled ask.
2183 //
2184 // The main loop's ask is rate limited to once a day, and a box that booted with
2185 // statistics off has usually already spent that ask -- so somebody who enabled
2186 // them during setup could see nothing published for up to 24 hours, with no way
2187 // to tell whether it had worked. This makes the act of enabling the trigger.
2188 //
2189 // The grace period is the point of the delay: the setting is read AGAIN when the
2190 // timer fires, so switching it back off within the window cancels the upload
2191 // rather than merely delaying it. A generation counter means repeated toggling
2192 // leaves exactly one pending timer that acts, no matter how many were started.
2193 static std::atomic<uint64_t> statsOptInGeneration{ 0 };
2194
2195 1887 static void RegisterStatsOptInListener() {
2196
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
9115 registerSettingsListener("fppd-stats-optin", "statsPublish", [](const std::string& value) {
2197
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (value != "Enabled") {
2198 // Bumping the generation is what cancels a pending timer.
2199 1 statsOptInGeneration++;
2200 1 return;
2201 }
2202 2 uint64_t gen = ++statsOptInGeneration;
2203 2 std::thread([gen]() {
2204
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::this_thread::sleep_for(std::chrono::seconds(STATS_OPT_IN_GRACE_SECONDS));
2205
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (statsOptInGeneration != gen) {
2206 // LogInfo, not LogDebug: this and the check below are the two
2207 // ways an opt-in ends in nothing being sent, and "nothing was
2208 // sent and nothing was said" is indistinguishable from the timer
2209 // never having been scheduled -- for someone reading the log to
2210 // work out why, and for a test trying to prove the cancel path
2211 // actually ran.
2212 1 LogInfo(VB_GENERAL, "Statistics opt-in superseded by a later change, not publishing\n");
2213 1 return;
2214 }
2215
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.
1 if (getSetting("statsPublish") != "Enabled") {
2216 ✗ LogInfo(VB_GENERAL, "Statistics were turned back off within %d seconds, not publishing\n",
2217 STATS_OPT_IN_GRACE_SECONDS);
2218 ✗ return;
2219 }
2220
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 PublishStatsForce("Statistics enabled");
2221
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 }).detach();
2222 });
2223 1887 }
2224
2225 1887 void MainLoop(void) {
2226
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 RegisterShutdownHandler(ShutdownFPPDCallback);
2227
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 RegisterManualCrashReportHandler(ManualCrashReportCallback);
2228
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 RegisterStatsOptInListener();
2229
2230 1887 PlaylistStatus prevFPPstatus = FPP_STATUS_IDLE;
2231 1887 int sleepms = 50;
2232 1887 int publishCounter = 200; // about 40 seconds after boot
2233
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 std::string publishReason("Startup");
2234 1887 std::map<int, std::function<bool(int)>> callbacks;
2235
2236
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 LogDebug(VB_GENERAL, "MainLoop()\n");
2237
2238 #ifdef PLATFORM_OSX
2239 // Lets media show a video window: this main thread pumps Cocoa from its
2240 // wait in EPollManager::waitForEvents(). Only in a GUI login session.
2241 if (MacOSAppInit()) {
2242 LogInfo(VB_GENERAL, "macOS: GUI session found, video output windows enabled\n");
2243 } else {
2244 LogInfo(VB_GENERAL, "macOS: no GUI session, video output windows disabled\n");
2245 }
2246 #endif
2247
2248
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1887 int sock = Command_Initialize();
2249
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1886 LogDebug(VB_GENERAL, "Command socket: %d\n", sock);
2250
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1886 if (sock >= 0) {
2251
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
3772 callbacks[sock] = [](int i) {
2252 340 MAIN_LOOP_PHASE("fpp command socket");
2253 340 CommandProc();
2254 340 return false;
2255 1886 };
2256 }
2257
2258 1886 sock = multiSync->GetControlSocket();
2259
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1886 LogDebug(VB_GENERAL, "Multisync socket: %d\n", sock);
2260
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1886 if (sock >= 0) {
2261
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
3772 callbacks[sock] = [](int i) {
2262 5625 MAIN_LOOP_PHASE("multisync ProcessControlPacket");
2263 5625 multiSync->ProcessControlPacket();
2264 5625 return false;
2265 1886 };
2266 }
2267
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1886 Bridge_Initialize(callbacks);
2268
2269
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1886 APIServer apiServer;
2270
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1886 apiServer.Init();
2271
2272
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 OutputMonitor::INSTANCE.Initialize(callbacks);
2273
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 GPIOManager::INSTANCE.Initialize(callbacks);
2274
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 PluginManager::INSTANCE.addControlCallbacks(callbacks);
2275
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 NetworkMonitor::INSTANCE.Init(callbacks);
2276
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 Sensors::INSTANCE.Init(callbacks);
2277
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 Variables::INSTANCE.Init();
2278
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 RecurringTasks::INSTANCE.Init();
2279
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 FileMonitor::INSTANCE.Initialize(callbacks);
2280
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 MDNSManager::INSTANCE.Initialize(callbacks);
2281
2282 // Get the last release date from rpi-imager file to use as minimum valid date
2283 // Systems without RTC boot with filesystem timestamps, which will be older than the release
2284 // If clock shows a date before the last OS release, it's definitely wrong
2285 1885 std::tm minValidDate = {};
2286 1885 minValidDate.tm_year = 125; // 2025 fallback
2287 1885 minValidDate.tm_mon = 0;
2288 1885 minValidDate.tm_mday = 1;
2289
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 Json::Value imagerRoot;
2290
4/8
✓ 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.
3770 if (LoadJsonFromFile(getFPPDDir("/rpi-imager/rpi-imager_falcon_player.json"), imagerRoot, JsonRoot::Object)) {
2291
6/12
✓ 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 1885 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1885 times.
✗ Branch 11 not taken.
1885 if (imagerRoot.isMember("os_list") && imagerRoot["os_list"].isArray()) {
2292 1885 std::time_t latestRelease = 0;
2293 // Find the latest release date across all OS images
2294
8/14
✓ 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 7540 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 7540 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 9425 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 7540 times.
✓ Branch 13 taken 1885 times.
9425 for (const auto& osEntry : imagerRoot["os_list"]) {
2295 // Parsed without throwing: std::stoi on a malformed date (and
2296 // isMember/asString on an entry of the wrong JSON type) threw
2297 // out of MainLoop and took fppd down at startup. An entry
2298 // that doesn't parse is skipped.
2299
6/12
✓ Branch 0 taken 7540 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7540 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7540 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7540 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 7540 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 7540 times.
7540 if (!osEntry.isObject() || !osEntry["release_date"].isString()) {
2300 ✗ continue;
2301 }
2302 // Parse "YYYY-MM-DD" format
2303 7540 int year = 0, month = 0, day = 0;
2304
2/4
✓ Branch 0 taken 7540 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7540 times.
✗ Branch 3 not taken.
7540 if (sscanf(osEntry["release_date"].asCString(), "%4d-%2d-%2d", &year, &month, &day) != 3 ||
2305
7/14
✓ Branch 0 taken 7540 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7540 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7540 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7540 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 7540 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 7540 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 7540 times.
7540 year < 1970 || month < 1 || month > 12 || day < 1 || day > 31) {
2306 ✗ continue;
2307 }
2308 7540 std::tm releaseTime = {};
2309 7540 releaseTime.tm_year = year - 1900;
2310 7540 releaseTime.tm_mon = month - 1;
2311 7540 releaseTime.tm_mday = day;
2312 7540 std::time_t releaseTimestamp = std::mktime(&releaseTime);
2313
2/2
✓ Branch 0 taken 1885 times.
✓ Branch 1 taken 5655 times.
7540 if (releaseTimestamp > latestRelease) {
2314 1885 latestRelease = releaseTimestamp;
2315 1885 minValidDate = releaseTime;
2316 }
2317 }
2318 }
2319 }
2320 1885 std::time_t minValidTime = std::mktime(&minValidDate);
2321 1885 bool clockWarningAdded = false;
2322
2323
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 StartChannelOutputThread();
2324
3/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1876 times.
✓ Branch 3 taken 9 times.
1885 if (!getSettingInt("restarted")) {
2325
1/2
✓ Branch 0 taken 1876 times.
✗ Branch 1 not taken.
1876 sequence->SendBlankingData();
2326 }
2327
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 bool alwaysTransmit = (bool)getSettingInt("alwaysTransmit");
2328
2329 // A process killed by the main-loop watchdog leaves its show behind in
2330 // tmpfs instead of on the command line (there is no orderly re-exec to put
2331 // it there) -- fold it into the same resume settings a restart would have
2332 // set, before either resume path below reads them (issue #2727).
2333
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 ApplyStallResumeState();
2334
2335
3/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1848 times.
✓ Branch 3 taken 37 times.
1885 if (getFPPmode() & PLAYER_MODE) {
2336 // Don't start scheduler if clock is obviously wrong (date before last release)
2337 // This prevents scheduling issues on systems without RTC that boot with incorrect time
2338 // The scheduler will start when time is corrected via time jump detection
2339 1848 std::time_t now = time(nullptr);
2340
2341
2/2
✓ Branch 0 taken 1843 times.
✓ Branch 1 taken 5 times.
1848 if (now >= minValidTime) {
2342
1/2
✓ Branch 0 taken 1843 times.
✗ Branch 1 not taken.
1843 scheduler->CheckIfShouldBePlayingNow();
2343 } else {
2344 struct tm timeinfo;
2345 5 localtime_r(&now, &timeinfo);
2346
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogWarn(VB_SCHEDULE, "Clock appears incorrect (date %04d-%02d-%02d before release), delaying scheduler start until time sync\n",
2347 timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday);
2348
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
15 WarningHolder::AddWarning(55, "System clock not set - scheduler start delayed until time sync");
2349 5 clockWarningAdded = true;
2350 }
2351
2352 // If a scheduled item covered the resume, CheckIfShouldBePlayingNow()
2353 // above already started it and cleared resumePlaylist. Anything left
2354 // here is a show that was running with no schedule behind it when we
2355 // restarted ourselves to recover the video decoder -- start it, or the
2356 // player comes back idle and every synced remote sits on its last frame
2357 // waiting for sync packets that never arrive (issue #2727).
2358
1/2
✓ Branch 0 taken 1848 times.
✗ Branch 1 not taken.
1848 std::string resumePlaylist = getSetting("resumePlaylist");
2359
6/8
✓ Branch 0 taken 1848 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1846 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 1846 times.
1850 if (getSettingInt("resumeUnscheduled") && !resumePlaylist.empty() &&
2360
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 Player::INSTANCE.GetStatus() == FPP_STATUS_IDLE) {
2361
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 int resumePosition = getSettingInt("resumePosition");
2362
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 int resumeRepeat = getSettingInt("resumeRepeat");
2363
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogInfo(VB_PLAYLIST, "Resuming unscheduled playlist '%s' at position %d (repeat %d) after restart\n",
2364 resumePlaylist.c_str(), resumePosition, resumeRepeat);
2365
3/6
✓ 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.
8 SetSetting("resumePlaylist", "");
2366
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 SetSetting("resumePosition", 0);
2367
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 SetSetting("resumeUnscheduled", 0);
2368
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Player::INSTANCE.StartPlaylist(resumePlaylist, resumeRepeat, resumePosition);
2369 }
2370 1848 }
2371
3/6
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1885 times.
3770 if (CommandManager::INSTANCE.HasPreset("FPPD_STARTED")) {
2372 ✗ CommandManager::INSTANCE.TriggerPreset("FPPD_STARTED");
2373 }
2374
2375
2/2
✓ Branch 0 taken 7547 times.
✓ Branch 1 taken 1885 times.
9432 for (auto& a : callbacks) {
2376
1/2
✓ Branch 0 taken 7547 times.
✗ Branch 1 not taken.
7547 EPollManager::INSTANCE.addFileDescriptor(a.first, a.second);
2377 }
2378
2379
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 multiSync->Discover();
2380
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 Events::Ready();
2381
2382 // Wire up EPollManager timer to fire Timers, so epoll_wait/kevent
2383 // wakes precisely when the next timer is due
2384
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 EPollManager::INSTANCE.setTimerCallback([]() {
2385 19593 Timers::INSTANCE.fireTimers();
2386 19593 });
2387
2388
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 LogInfo(VB_GENERAL, "Starting main processing loop\n");
2389
2390
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 WarningHolder::UpdateLogLevelWarnings();
2391
2392 1885 int idleCount = 0;
2393
2394
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 StartMainLoopWatchdog();
2395
2396 // Relaxed: a stop flag and a timestamp for the watchdog, read or written
2397 // on every pass, with nothing published through either.
2398
2/2
✓ Branch 0 taken 466767 times.
✓ Branch 1 taken 1867 times.
470519 while (runMainFPPDLoop.load(std::memory_order_relaxed)) {
2399
1/2
✓ Branch 0 taken 466767 times.
✗ Branch 1 not taken.
466767 s_mainLoopHeartbeat.store(MonotonicMS(), std::memory_order_relaxed);
2400
1/2
✓ Branch 0 taken 466767 times.
✗ Branch 1 not taken.
466767 MAIN_LOOP_PHASE("epoll/callbacks");
2401
1/2
✓ Branch 0 taken 466749 times.
✗ Branch 1 not taken.
466767 EPollManager::WaitResult epollresult = EPollManager::INSTANCE.waitForEvents(sleepms);
2402 466749 bool pushBridgeData = epollresult == EPollManager::WaitResult::SOME_TRUE;
2403
2/2
✓ Branch 0 taken 1924 times.
✓ Branch 1 taken 464825 times.
466749 if (epollresult == EPollManager::WaitResult::INTERRUPTED) {
2404 // We get interrupted when media players finish
2405 1924 continue;
2406 }
2407
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 464825 times.
464825 if (epollresult == EPollManager::WaitResult::FAILED) {
2408 ✗ LogErr(VB_GENERAL, "Main epoll() failed: %s\n", FPPstrerror(errno));
2409 ✗ runMainFPPDLoop = 0;
2410 ✗ continue;
2411 }
2412 // Check to see if we need to start up the output thread.
2413
1/2
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
464825 MAIN_LOOP_PHASE("channel output thread check");
2414
5/6
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87570 times.
✓ Branch 3 taken 377255 times.
✓ Branch 4 taken 29 times.
✓ Branch 5 taken 464796 times.
552395 if ((!ChannelOutputThreadIsRunning()) &&
2415
2/4
✓ Branch 0 taken 87570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87570 times.
✗ Branch 3 not taken.
87570 ((PixelOverlayManager::INSTANCE.hasActiveOverlays()) ||
2416
3/6
✓ Branch 0 taken 87570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87570 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 87570 times.
✗ Branch 5 not taken.
87570 (ChannelTester::INSTANCE.Testing()) ||
2417
4/6
✓ Branch 0 taken 87570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87541 times.
✓ Branch 3 taken 29 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 87541 times.
87570 (alwaysTransmit) || sequence->hasBridgeData() ||
2418 pushBridgeData)) {
2419
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 StartChannelOutputThread();
2420 }
2421
2422
3/4
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 445665 times.
✓ Branch 3 taken 19160 times.
464825 if (getFPPmode() & PLAYER_MODE) {
2423 // Keep the watchdog's idea of what is playing current -- it cannot
2424 // read this out of Player itself once the loop is stalled.
2425
1/2
✓ Branch 0 taken 445665 times.
✗ Branch 1 not taken.
445665 RecordMainLoopShowSnapshot();
2426
2427
3/4
✓ Branch 0 taken 445665 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 298513 times.
✓ Branch 3 taken 147152 times.
445665 if (Player::INSTANCE.IsPlaying()) {
2428
2/2
✓ Branch 0 taken 765 times.
✓ Branch 1 taken 297748 times.
298513 if (prevFPPstatus == FPP_STATUS_IDLE) {
2429 765 sleepms = 10;
2430 }
2431
2432 // Check again here in case PlayListPlayingInit
2433 // didn't find anything and put us back to IDLE
2434
2/4
✓ Branch 0 taken 298513 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 298513 times.
✗ Branch 3 not taken.
298513 if (Player::INSTANCE.IsPlaying()) {
2435
1/2
✓ Branch 0 taken 298513 times.
✗ Branch 1 not taken.
298513 MAIN_LOOP_PHASE("Player::Process");
2436
1/2
✓ Branch 0 taken 298513 times.
✗ Branch 1 not taken.
298513 Player::INSTANCE.Process();
2437 }
2438 }
2439
2440 // Read once: a stop from another thread (the API, MQTT) can land
2441 // between two reads, and then the second recorded "idle" as the
2442 // previous status and the playing-to-idle change was never seen,
2443 // so the stopped playlist was never cleaned up and the scheduler
2444 // never asked whether something should be playing. For the same
2445 // reason "stopping now" counts below: a stop holds it while the
2446 // entry stops, and a pass that read it then never saw the change.
2447
1/2
✓ Branch 0 taken 445665 times.
✗ Branch 1 not taken.
445665 PlaylistStatus curStatus = Player::INSTANCE.GetStatus();
2448 445665 int reactivated = 0;
2449 // Set when the change to idle is left for a later pass to see.
2450 445665 bool unrecorded = false;
2451
2/2
✓ Branch 0 taken 147234 times.
✓ Branch 1 taken 298431 times.
445665 if (curStatus == FPP_STATUS_IDLE) {
2452
4/4
✓ Branch 0 taken 144471 times.
✓ Branch 1 taken 2763 times.
✓ Branch 2 taken 144468 times.
✓ Branch 3 taken 3 times.
147234 if ((prevFPPstatus == FPP_STATUS_PLAYLIST_PLAYING) ||
2453
2/2
✓ Branch 0 taken 144233 times.
✓ Branch 1 taken 235 times.
144468 (prevFPPstatus == FPP_STATUS_PLAYLIST_PAUSED) ||
2454
2/2
✓ Branch 0 taken 144225 times.
✓ Branch 1 taken 8 times.
144233 (prevFPPstatus == FPP_STATUS_STOPPING_NOW) ||
2455
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 144223 times.
144225 (prevFPPstatus == FPP_STATUS_STOPPING_GRACEFULLY) ||
2456 (prevFPPstatus == FPP_STATUS_STOPPING_GRACEFULLY_AFTER_LOOP)) {
2457
1/2
✓ Branch 0 taken 3011 times.
✗ Branch 1 not taken.
3011 MAIN_LOOP_PHASE("Player::Cleanup");
2458 // Read after the status: a switch holds its scope from
2459 // before its stop, so idle in one is seen with it held.
2460 // Cleanup() returns 0 if a start landed since the read.
2461 // Either way nothing stopped, and asking the scheduler
2462 // would pre-empt the new playlist (issue #3140). If the
2463 // start fails, a later pass sees the idle and acts.
2464
8/10
✓ Branch 0 taken 3011 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 560 times.
✓ Branch 3 taken 2451 times.
✓ Branch 4 taken 560 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 559 times.
✓ Branch 8 taken 2452 times.
✓ Branch 9 taken 559 times.
3011 if (Player::Switching() || !Player::INSTANCE.Cleanup()) {
2465 2452 unrecorded = true;
2466 } else {
2467
3/4
✓ Branch 0 taken 559 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 356 times.
✓ Branch 3 taken 203 times.
559 if (Player::INSTANCE.GetForceStopped())
2468
2/4
✓ Branch 0 taken 356 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 356 times.
✗ Branch 3 not taken.
356 scheduler->CheckIfShouldBePlayingNow(0, Player::INSTANCE.GetScheduleEntry());
2469 else
2470
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 scheduler->CheckIfShouldBePlayingNow();
2471
2472
3/4
✓ Branch 0 taken 559 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✓ Branch 3 taken 511 times.
559 if (Player::INSTANCE.GetStatus() != FPP_STATUS_IDLE)
2473 48 reactivated = 1;
2474 else
2475 511 sleepms = 50;
2476 }
2477 }
2478 }
2479
2480
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 445617 times.
445665 if (reactivated)
2481 48 prevFPPstatus = FPP_STATUS_IDLE;
2482
2/2
✓ Branch 0 taken 443165 times.
✓ Branch 1 taken 2452 times.
445617 else if (!unrecorded)
2483 443165 prevFPPstatus = curStatus;
2484
2485 // Process() frees retired playlists and entries, but only runs
2486 // while something plays; free them while idle too.
2487
3/4
✓ Branch 0 taken 445665 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 147334 times.
✓ Branch 3 taken 298331 times.
445665 if (!Player::INSTANCE.IsPlaying()) {
2488
1/2
✓ Branch 0 taken 147334 times.
✗ Branch 1 not taken.
147334 Playlist::ReleaseRetired();
2489 }
2490
2491
2/4
✓ Branch 0 taken 19160 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19160 times.
✗ Branch 3 not taken.
19160 } else if (getFPPmode() == REMOTE_MODE) {
2492
2/2
✓ Branch 0 taken 11389 times.
✓ Branch 1 taken 7771 times.
19160 if (mediaOutputStatus.status == MEDIAOUTPUTSTATUS_PLAYING) {
2493
1/2
✓ Branch 0 taken 11389 times.
✗ Branch 1 not taken.
11389 MAIN_LOOP_PHASE("Player::ProcessMedia");
2494
1/2
✓ Branch 0 taken 11389 times.
✗ Branch 1 not taken.
11389 Player::INSTANCE.ProcessMedia();
2495 } else {
2496
1/2
✓ Branch 0 taken 7771 times.
✗ Branch 1 not taken.
7771 MAIN_LOOP_PHASE("CheckSyncedMediaIdleTimeout");
2497 // Media that ran to its end and was never stopped by the
2498 // player (issue #2727) -- only checked when not playing, so
2499 // free-running through a sync gap is untouched.
2500
1/2
✓ Branch 0 taken 7771 times.
✗ Branch 1 not taken.
7771 multiSync->CheckSyncedMediaIdleTimeout();
2501 }
2502 }
2503
2504 // Only run scheduler if clock appears valid (same check as initial scheduler start)
2505 464825 std::time_t now = time(nullptr);
2506
2/2
✓ Branch 0 taken 464648 times.
✓ Branch 1 taken 177 times.
464825 if (now >= minValidTime) {
2507
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 464648 times.
464648 if (clockWarningAdded) {
2508 // clock was corrected (time sync) since startup — clear the warning
2509 ✗ WarningHolder::RemoveWarning(55, "System clock not set - scheduler start delayed until time sync");
2510 ✗ clockWarningAdded = false;
2511 }
2512
1/2
✓ Branch 0 taken 464648 times.
✗ Branch 1 not taken.
464648 MAIN_LOOP_PHASE("scheduler");
2513
1/2
✓ Branch 0 taken 464648 times.
✗ Branch 1 not taken.
464648 scheduler->ScheduleProc();
2514 }
2515
2516
2/2
✓ Branch 0 taken 3165 times.
✓ Branch 1 taken 461660 times.
464825 if (pushBridgeData) {
2517
1/2
✓ Branch 0 taken 3165 times.
✗ Branch 1 not taken.
3165 MAIN_LOOP_PHASE("ForceChannelOutputNow");
2518
1/2
✓ Branch 0 taken 3165 times.
✗ Branch 1 not taken.
3165 ForceChannelOutputNow();
2519 }
2520 464825 bool doPing = false;
2521
2/2
✓ Branch 0 taken 418285 times.
✓ Branch 1 taken 46540 times.
464825 if (epollresult == EPollManager::WaitResult::TIMEOUT) {
2522 418285 idleCount++;
2523
2/2
✓ Branch 0 taken 17557 times.
✓ Branch 1 taken 400728 times.
418285 if (idleCount >= 20) {
2524 17557 doPing = true;
2525 }
2526
2/2
✓ Branch 0 taken 6571 times.
✓ Branch 1 taken 39969 times.
46540 } else if (idleCount > 0) {
2527 6571 doPing = true;
2528 } else {
2529 39969 idleCount--;
2530
2/2
✓ Branch 0 taken 1608 times.
✓ Branch 1 taken 38361 times.
39969 if (idleCount < -20) {
2531 1608 doPing = true;
2532 }
2533 }
2534
2/2
✓ Branch 0 taken 25736 times.
✓ Branch 1 taken 439089 times.
464825 if (doPing) {
2535 25736 idleCount = 0;
2536
1/2
✓ Branch 0 taken 25736 times.
✗ Branch 1 not taken.
25736 MAIN_LOOP_PHASE("multisync PeriodicPing");
2537
1/2
✓ Branch 0 taken 25736 times.
✗ Branch 1 not taken.
25736 multiSync->PeriodicPing();
2538
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 25716 times.
25736 if (--publishCounter < 0) {
2539
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 PublishStatsBackground(publishReason);
2540 // This counter only decides how often the (very cheap) time
2541 // check inside PublishStatsBackground runs; that check enforces
2542 // the real cadence. It has to be comfortably finer than the
2543 // cadence or it becomes the limiting factor: doPing runs about
2544 // once a second when idle, so the previous 60480 was roughly 17
2545 // hours and a 24-hour guard would have landed near 34.
2546 20 publishCounter = 3600;
2547
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 publishReason = "normal";
2548 }
2549 // A cape whose signature is not bound to this hardware is detected
2550 // at boot, before there is a network to report it over. This is the
2551 // point where there is one.
2552
1/2
✓ Branch 0 taken 25736 times.
✗ Branch 1 not taken.
25736 MAIN_LOOP_PHASE("cape license notify");
2553
1/2
✓ Branch 0 taken 25736 times.
✗ Branch 1 not taken.
25736 CapeLicenseNotifyBackground();
2554 // also do the periodic work in the api server while idle
2555
1/2
✓ Branch 0 taken 25736 times.
✗ Branch 1 not taken.
25736 MAIN_LOOP_PHASE("apiServer periodicWork");
2556
1/2
✓ Branch 0 taken 25736 times.
✗ Branch 1 not taken.
25736 apiServer.periodicWork();
2557 }
2558
1/2
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
464825 MAIN_LOOP_PHASE("timers");
2559
1/2
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
464825 Timers::INSTANCE.fireTimers();
2560
1/2
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
464825 MAIN_LOOP_PHASE("curls");
2561
1/2
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
464825 CurlManager::INSTANCE.processCurls();
2562
1/2
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
464825 MAIN_LOOP_PHASE("GPIO inputs");
2563
1/2
✓ Branch 0 taken 464825 times.
✗ Branch 1 not taken.
464825 GPIOManager::INSTANCE.CheckGPIOInputs();
2564 }
2565
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 MAIN_LOOP_PHASE("loop exited");
2566
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 FileMonitor::INSTANCE.Cleanup();
2567
2568
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 LogInfo(VB_GENERAL, "Stopping channel output thread.\n");
2569
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 StopChannelOutputThread();
2570
2571
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 Bridge_Shutdown();
2572
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 EPollManager::INSTANCE.shutdown();
2573
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 LogInfo(VB_GENERAL, "Main Loop complete, shutting down.\n");
2574 1867 }
2575
2576 // Each process CreateDaemon() leaves behind exits without running static
2577 // destructors: they belong to the daemon. A destructor can block on a thread
2578 // started before the fork: glibc's pthread_cond_destroy() waits for its
2579 // waiters, and when "-v" started the volume worker that early, exit() never
2580 // returned and whatever ran "fppd -d" never saw its process end.
2581 10 static void LeaveForkedProcess(int code) {
2582 10 fflush(nullptr);
2583 10 _exit(code);
2584 }
2585
2586 5 void CreateDaemon(void) {
2587 /* Fork and terminate parent so we can run in the background */
2588 /* Fork off the parent process */
2589 5 pid = fork();
2590
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (pid < 0) {
2591 ✗ LeaveForkedProcess(EXIT_FAILURE);
2592 }
2593 /* If we got a good PID, then
2594 we can exit the parent process. */
2595
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
10 if (pid > 0) {
2596 5 LeaveForkedProcess(EXIT_SUCCESS);
2597 }
2598
2599 /* Change the file mode mask */
2600 5 umask(0);
2601
2602 /* Create a new SID for the child process */
2603 5 sid = setsid();
2604
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (sid < 0) {
2605 /* Log any failures here */
2606 ✗ LeaveForkedProcess(EXIT_FAILURE);
2607 }
2608
2609 /* Fork a second time to get rid of session leader */
2610 /* Fork off the parent process */
2611 5 pid = fork();
2612
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (pid < 0) {
2613 ✗ LeaveForkedProcess(EXIT_FAILURE);
2614 }
2615 /* If we got a good PID, then
2616 we can exit the parent process. */
2617
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
10 if (pid > 0) {
2618 5 LeaveForkedProcess(EXIT_SUCCESS);
2619 }
2620
2621 /* Point the standard file descriptors at /dev/null. Closed, they were
2622 taken by whatever fppd opened next (see OpenStandardDescriptors() in
2623 common_mini.cpp) */
2624 5 int devNull = open("/dev/null", O_RDWR);
2625
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (devNull >= 0) {
2626 5 dup2(devNull, STDIN_FILENO);
2627 5 dup2(devNull, STDOUT_FILENO);
2628 5 dup2(devNull, STDERR_FILENO);
2629
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (devNull > STDERR_FILENO) {
2630 5 close(devNull);
2631 }
2632 }
2633 5 }
2634
2635 1 void PublishStatsForce(std::string reason) {
2636
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 SetThreadName("FPP-PubStats");
2637
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 std::string result("");
2638
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 urlPost("http://localhost/api/statistics/usage?reason=" + reason, "", result);
2639
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogInfo(VB_GENERAL, "Publishing statistics because of \"%s\" = %s\n", reason.c_str(), result.c_str());
2640 1 }
2641
2642 // How often this daemon ASKS the statistics route whether to publish. It is
2643 // deliberately much shorter than the publish interval itself, which lives in the
2644 // route (STATS_PUBLISH_INTERVAL_DAYS) because that is the side that can see the
2645 // payload and hold a timestamp across restarts.
2646 //
2647 // The two must not be equal. This counter resets on every restart, so if it
2648 // matched the publish interval a box that rebooted just before the interval
2649 // elapsed would be told "not yet", spend its guard, and not ask again for a full
2650 // interval -- pushing the periodic publish out to nearly double, and with it the
2651 // only signal that says the device is still alive. Asking daily bounds that
2652 // error at one day.
2653 #define STATS_PUBLISH_ASK_HOURS 24
2654
2655 20 void PublishStatsBackground(std::string reason) {
2656 // Rate limit on ASKING, not on sending. Whether anything is actually
2657 // uploaded is the route's decision: a restart publishes when the device's
2658 // configuration signature has moved and stays quiet when it has not, and a
2659 // device that never changes still checks in on the publish interval so the
2660 // project can see it is alive.
2661 // steady_clock: a wall-clock step back held the ask off for the size of the step
2662
3/8
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
20 static auto lastAsk = std::chrono::steady_clock::now() - std::chrono::hours(STATS_PUBLISH_ASK_HOURS);
2663 20 auto now_ts = std::chrono::steady_clock::now();
2664
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 auto hours = std::chrono::duration_cast<std::chrono::hours>(now_ts - lastAsk);
2665
2666
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (hours.count() >= STATS_PUBLISH_ASK_HOURS) {
2667
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 std::string mode = getSetting("statsPublish");
2668
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 20 times.
20 if (mode == "Enabled") {
2669 // Spend the budget only on a call that can actually do something.
2670 // It used to be spent here unconditionally, which meant a box that
2671 // booted with statistics off burned its one ask of the day on a
2672 // branch that contacts nobody -- so a user who then turned them on
2673 // during setup published nothing for up to 24 hours, unless a reboot
2674 // happened to reset this static. Now the first tick after they opt
2675 // in is the one that publishes.
2676 ✗ lastAsk = now_ts;
2677 ✗ std::thread t(PublishStatsForce, reason);
2678 ✗ t.detach();
2679 ✗ } else {
2680 // Not spending lastAsk means this branch is reached on every idle
2681 // tick rather than daily, so the message needs its own limiter or it
2682 // becomes a per-second log. The setting lookup itself is a cheap map
2683 // read and is fine to repeat.
2684
3/8
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
20 static auto lastLog = std::chrono::steady_clock::now() - std::chrono::hours(STATS_PUBLISH_ASK_HOURS);
2685
3/6
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 20 times.
20 if (std::chrono::duration_cast<std::chrono::hours>(now_ts - lastLog).count() >= STATS_PUBLISH_ASK_HOURS) {
2686 ✗ lastLog = now_ts;
2687 ✗ LogInfo(VB_GENERAL, "Not Publishing statistics as mode is '%s'\n", mode.c_str());
2688 }
2689 }
2690 20 } else {
2691 ✗ LogDebug(VB_GENERAL, "PublishStats called, but not time to ask yet.\n");
2692 }
2693 20 }
2694