| 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 |