| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-2022 by the Falcon Player Developers. | ||
| 4 | * | ||
| 5 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 6 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 7 | * file for descriptions of what files are covered by each license. | ||
| 8 | * | ||
| 9 | * This source file is covered under the LGPL v2.1 as described in the | ||
| 10 | * included LICENSE.LGPL file. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "fpp-pch.h" | ||
| 14 | #include <cstring> | ||
| 15 | |||
| 16 | #include <algorithm> | ||
| 17 | #include <atomic> | ||
| 18 | #include <fcntl.h> | ||
| 19 | #include <sys/stat.h> | ||
| 20 | #include <sys/syscall.h> | ||
| 21 | #include <sys/time.h> | ||
| 22 | #include <sys/utsname.h> | ||
| 23 | #include <pwd.h> | ||
| 24 | #include <sys/types.h> | ||
| 25 | #include <chrono> | ||
| 26 | #include <mutex> | ||
| 27 | #include <sstream> | ||
| 28 | #include <stdarg.h> | ||
| 29 | #include <stdbool.h> | ||
| 30 | #include <time.h> | ||
| 31 | #include <unistd.h> | ||
| 32 | |||
| 33 | #include "fppversion.h" | ||
| 34 | #include "log.h" | ||
| 35 | |||
| 36 | // int logLevel = LOG_INFO; | ||
| 37 | // int logMask = VB_MOST; | ||
| 38 | |||
| 39 | // Create Global Instance | ||
| 40 | FPPLogger FPPLogger::INSTANCE = FPPLogger(); | ||
| 41 | |||
| 42 | char logFileName[1024] = ""; | ||
| 43 | bool logToStdOut = true; | ||
| 44 | |||
| 45 | // Whether a line already written to the log file should be kept off stdout. | ||
| 46 | // Opt-in, and only fppd opts in: it is the one FPP binary whose stdout is a | ||
| 47 | // firehose into journald (it runs as `fppd -f` under systemd), so its stdout | ||
| 48 | // copy is pure duplication of what fppd.log already has. The small services -- | ||
| 49 | // fppoled and friends -- log a handful of lines that users and support look for | ||
| 50 | // with `journalctl -u fppoled`, so they stay on both. See _LogWrite(). | ||
| 51 | static bool suppressDuplicateStdOut = false; | ||
| 52 | |||
| 53 | // --------------------------------------------------------------------------- | ||
| 54 | // Crash-time log ring | ||
| 55 | // | ||
| 56 | // The most useful line for diagnosing a crash is routinely one nobody kept: the | ||
| 57 | // trigger is logged at LogDebug, production runs at info, so it is filtered out | ||
| 58 | // before it is ever formatted. This ring formats and retains those lines in | ||
| 59 | // memory without writing them anywhere, and the crash handler dumps it into the | ||
| 60 | // report -- debug-level detail at info-level cost on disk. | ||
| 61 | // | ||
| 62 | // Constraints that shaped it: | ||
| 63 | // * No locking. The normal log path takes logFileLock, and a crash handler | ||
| 64 | // that blocks on a mutex the faulting thread already holds hangs the | ||
| 65 | // process -- exactly the case (heap corruption inside free()) that most | ||
| 66 | // needs the trace. A slot may be torn between a writer and the dump; a | ||
| 67 | // smeared line in a crash report is a fine price for never deadlocking. | ||
| 68 | // * No allocation, for the same reason: a fixed array of fixed slots, so the | ||
| 69 | // dump is a bounded series of write() calls and is async-signal-safe. | ||
| 70 | // * LOG_EXCESSIVE is excluded. It fires per frame in the output path, and | ||
| 71 | // formatting it unconditionally would be a real cost on a single-core BBB; | ||
| 72 | // LOG_DEBUG and coarser covers the state changes worth reconstructing. | ||
| 73 | // --------------------------------------------------------------------------- | ||
| 74 | static constexpr size_t kCrashRingSlots = 256; | ||
| 75 | static constexpr size_t kCrashRingSlotSize = 240; | ||
| 76 | static char s_crashRing[kCrashRingSlots][kCrashRingSlotSize]; | ||
| 77 | static std::atomic<uint64_t> s_crashRingSeq{ 0 }; | ||
| 78 | |||
| 79 | // Set once the crash handler starts, so its own output -- the gdb stack, the | ||
| 80 | // "Crash handler called" line -- stops evicting the pre-crash history that is | ||
| 81 | // the entire reason the ring exists. That output is already captured in the | ||
| 82 | // stack file; duplicating it here would only cost us the lines we want. | ||
| 83 | static std::atomic<bool> s_crashRingFrozen{ false }; | ||
| 84 | |||
| 85 | 17 | void CrashLogRingFreeze() { | |
| 86 | 17 | s_crashRingFrozen.store(true, std::memory_order_relaxed); | |
| 87 | 17 | } | |
| 88 | |||
| 89 | // Zero-init statics, so this is safe from any point in startup with no | ||
| 90 | // dependency on static initialization order. | ||
| 91 | 2357045 | static void CrashLogRingPush(const char* line, size_t len) { | |
| 92 |
2/2✓ Branch 0 taken 46 times.
✓ Branch 1 taken 2356999 times.
|
2357045 | if (s_crashRingFrozen.load(std::memory_order_relaxed)) { |
| 93 | 46 | return; | |
| 94 | } | ||
| 95 | 2356999 | uint64_t seq = s_crashRingSeq.fetch_add(1, std::memory_order_relaxed); | |
| 96 | 2356999 | char* slot = s_crashRing[seq % kCrashRingSlots]; | |
| 97 |
2/2✓ Branch 0 taken 21604 times.
✓ Branch 1 taken 2335395 times.
|
2356999 | if (len >= kCrashRingSlotSize) { |
| 98 | 21604 | len = kCrashRingSlotSize - 1; | |
| 99 | } | ||
| 100 | 2356999 | memcpy(slot, line, len); | |
| 101 | 2356999 | slot[len] = '\0'; | |
| 102 | } | ||
| 103 | |||
| 104 | // Two independent filters, both needed: | ||
| 105 | // * facility opt-out excludes the per-frame data facilities outright (see | ||
| 106 | // FPPLoggerInstance::crashRingCapture -- HexDump emits at LogInfo, so a | ||
| 107 | // level test alone would not have caught them); | ||
| 108 | // * LOG_EXCESSIVE is dropped everywhere, since it is the per-frame level by | ||
| 109 | // convention in every facility that has one. | ||
| 110 | 7931093 | bool CrashLogRingWillCapture(int level, FPPLoggerInstance& facility) { | |
| 111 |
4/4✓ Branch 0 taken 6882642 times.
✓ Branch 1 taken 1048451 times.
✓ Branch 2 taken 2357045 times.
✓ Branch 3 taken 4525597 times.
|
7931093 | return facility.crashRingCapture && level <= LOG_DEBUG; |
| 112 | } | ||
| 113 | |||
| 114 | // Async-signal-safe: only write(), no allocation, no locks. | ||
| 115 | 18 | void CrashLogRingDump(int fd) { | |
| 116 | 18 | uint64_t seq = s_crashRingSeq.load(std::memory_order_relaxed); | |
| 117 |
1/2✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
|
18 | uint64_t count = seq < kCrashRingSlots ? seq : kCrashRingSlots; |
| 118 | static const char hdr[] = "=== recent log lines (ring buffer, oldest first) ===\n"; | ||
| 119 |
1/2✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
|
18 | (void)!write(fd, hdr, sizeof(hdr) - 1); |
| 120 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
|
18 | if (count == 0) { |
| 121 | static const char none[] = "(empty)\n"; | ||
| 122 | ✗ | (void)!write(fd, none, sizeof(none) - 1); | |
| 123 | ✗ | return; | |
| 124 | } | ||
| 125 |
2/2✓ Branch 0 taken 2034 times.
✓ Branch 1 taken 18 times.
|
2052 | for (uint64_t i = seq - count; i < seq; i++) { |
| 126 | 2034 | const char* slot = s_crashRing[i % kCrashRingSlots]; | |
| 127 | 2034 | size_t len = strnlen(slot, kCrashRingSlotSize); | |
| 128 |
1/2✓ Branch 0 taken 2034 times.
✗ Branch 1 not taken.
|
2034 | if (len) { |
| 129 |
1/2✓ Branch 0 taken 2034 times.
✗ Branch 1 not taken.
|
2034 | (void)!write(fd, slot, len); |
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | // True when stdout is an interactive terminal (a person ran `fppd -f` in a | ||
| 135 | // shell) rather than a pipe to journald (how systemd runs `fppd -f`). Used to | ||
| 136 | // decide whether the stdout copy of a line already written to the log file is | ||
| 137 | // a useful on-screen echo or a pure journald duplicate. Cached: fppd's stdout | ||
| 138 | // is not reassigned after startup, and isatty() is a syscall per log line. | ||
| 139 | 374969 | static bool stdoutIsInteractive() { | |
| 140 |
3/4✓ Branch 0 taken 1882 times.
✓ Branch 1 taken 373087 times.
✓ Branch 2 taken 1882 times.
✗ Branch 3 not taken.
|
374969 | static const bool interactive = isatty(fileno(stdout)) != 0; |
| 141 | 374969 | return interactive; | |
| 142 | } | ||
| 143 | |||
| 144 | // Program name for the syslog-style program field. fppd.log is a merged, | ||
| 145 | // sequential record -- fppd itself, the fppd_start/stop/restart scripts, and | ||
| 146 | // operation breadcrumbs all append to it -- so every line has to say who wrote | ||
| 147 | // it. The [facility] field cannot carry this: it is a subsystem *within* fppd | ||
| 148 | // (General/ChannelOut/Playlist/...), not a program. | ||
| 149 | 2388107 | static const char* logProgramName() { | |
| 150 | #ifdef PLATFORM_OSX | ||
| 151 | return getprogname(); | ||
| 152 | #else | ||
| 153 | 2388107 | return program_invocation_short_name; | |
| 154 | #endif | ||
| 155 | } | ||
| 156 | // char logLevelStr[16]; | ||
| 157 | // char logMaskStr[1024]; | ||
| 158 | |||
| 159 | // Persistent handle for the log file so each log line is a single write() | ||
| 160 | // instead of a full fopen/write/fclose cycle. On slow SD cards the per-line | ||
| 161 | // open/close metadata traffic was enough that a card-internal stall could | ||
| 162 | // block the (real-time) channel output thread mid-log and drop frames. | ||
| 163 | // | ||
| 164 | // logrotate rotates fppd.log by rename (no copytruncate and no signal to | ||
| 165 | // fppd), so we periodically stat() the path and reopen when the inode under | ||
| 166 | // our name changes or the file disappears. | ||
| 167 | // | ||
| 168 | // A recursive_timed_mutex guards the handle: recursive because the crash | ||
| 169 | // handler calls LogErr and may re-enter on the same thread; timed so that if | ||
| 170 | // another thread is wedged inside a write (e.g. stalled storage) a crashing | ||
| 171 | // thread gives up and falls back to stderr instead of hanging the process. | ||
| 172 | // Owner the log file should end up with, resolved once. Looked up rather than | ||
| 173 | // hardcoded to a uid, and returns false where there is no such user (macOS runs | ||
| 174 | // as the developer's own account, not fpp) so the chown is simply skipped there. | ||
| 175 | 1871 | static bool logFileOwner(uid_t& uid, gid_t& gid) { | |
| 176 | static bool resolved = false; | ||
| 177 | static bool haveOwner = false; | ||
| 178 | static uid_t cachedUid = 0; | ||
| 179 | static gid_t cachedGid = 0; | ||
| 180 |
2/2✓ Branch 0 taken 1870 times.
✓ Branch 1 taken 1 times.
|
1871 | if (!resolved) { |
| 181 | 1870 | resolved = true; | |
| 182 | 1870 | struct passwd* pw = getpwnam("fpp"); | |
| 183 |
2/2✓ Branch 0 taken 160 times.
✓ Branch 1 taken 1710 times.
|
1870 | if (pw) { |
| 184 | 160 | cachedUid = pw->pw_uid; | |
| 185 | 160 | cachedGid = pw->pw_gid; | |
| 186 | 160 | haveOwner = true; | |
| 187 | } | ||
| 188 | } | ||
| 189 | 1871 | uid = cachedUid; | |
| 190 | 1871 | gid = cachedGid; | |
| 191 | 1871 | return haveOwner; | |
| 192 | } | ||
| 193 | |||
| 194 | static std::recursive_timed_mutex logFileLock; | ||
| 195 | static FILE* persistentLogFile = nullptr; | ||
| 196 | static ino_t persistentLogIno = 0; | ||
| 197 | static dev_t persistentLogDev = 0; | ||
| 198 | static time_t lastRotationCheck = 0; | ||
| 199 | |||
| 200 | // Must be called with logFileLock held. | ||
| 201 | 1958 | static void closePersistentLogFile() { | |
| 202 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1957 times.
|
1958 | if (persistentLogFile) { |
| 203 | 1 | fclose(persistentLogFile); | |
| 204 | 1 | persistentLogFile = nullptr; | |
| 205 | 1 | persistentLogIno = 0; | |
| 206 | 1 | persistentLogDev = 0; | |
| 207 | 1 | lastRotationCheck = 0; | |
| 208 | } | ||
| 209 | 1958 | } | |
| 210 | |||
| 211 | // Must be called with logFileLock held. Returns the persistent handle for | ||
| 212 | // logFileName, (re)opening it as needed, or nullptr if it cannot be opened. | ||
| 213 | 375281 | static FILE* getPersistentLogFile(time_t now) { | |
| 214 |
4/4✓ Branch 0 taken 373287 times.
✓ Branch 1 taken 1994 times.
✓ Branch 2 taken 4759 times.
✓ Branch 3 taken 368528 times.
|
375281 | if (persistentLogFile && now != lastRotationCheck) { |
| 215 | // At most once a second, check whether the file we have open is still | ||
| 216 | // the one at logFileName (logrotate renames it, a user may delete it). | ||
| 217 | 4759 | lastRotationCheck = now; | |
| 218 | struct stat st; | ||
| 219 | 4759 | if ((stat(logFileName, &st) != 0) || | |
| 220 |
6/8✓ Branch 0 taken 4758 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 4758 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4758 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 4758 times.
|
4759 | (st.st_ino != persistentLogIno) || (st.st_dev != persistentLogDev)) { |
| 221 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | closePersistentLogFile(); |
| 222 | } | ||
| 223 | } | ||
| 224 |
2/2✓ Branch 0 taken 1995 times.
✓ Branch 1 taken 373286 times.
|
375281 | if (!persistentLogFile) { |
| 225 | 1995 | bool existed = (access(logFileName, F_OK) == 0); | |
| 226 | 1995 | persistentLogFile = fopen(logFileName, "a"); | |
| 227 |
2/2✓ Branch 0 taken 1890 times.
✓ Branch 1 taken 105 times.
|
1995 | if (persistentLogFile) { |
| 228 | // Line buffered: each log line still hits the kernel immediately | ||
| 229 | // (matching the old open/write/close behavior) but as one write(). | ||
| 230 | 1890 | setvbuf(persistentLogFile, nullptr, _IOLBF, 8192); | |
| 231 |
2/2✓ Branch 0 taken 1871 times.
✓ Branch 1 taken 19 times.
|
1890 | if (!existed) { |
| 232 | // We just created the log, and fppd runs as root (fppd.service | ||
| 233 | // sets no User=), so it would be root:root 0644 in an fpp-owned | ||
| 234 | // directory -- locking out every writer that is not root. That | ||
| 235 | // matters because fppd is no longer the only one: the | ||
| 236 | // fppd_start/stop/restart scripts append here, and so do the PHP | ||
| 237 | // breadcrumbs (backup, mp3gain) which run as Apache/fpp. Their | ||
| 238 | // writes are best-effort and error-suppressed, so they would fail | ||
| 239 | // SILENTLY. | ||
| 240 | // | ||
| 241 | // logrotate's `create 0664 fpp fpp` normally gets here first, but | ||
| 242 | // it cannot cover every case: resetConfig.php deletes logs/*, and | ||
| 243 | // a fresh install writes here before FPPINIT's setFileOwnership | ||
| 244 | // runs. Whoever creates the file has to set the ownership, so do | ||
| 245 | // it here too. Best-effort: as non-root the chown simply fails, | ||
| 246 | // which is the case where we are already the right owner. | ||
| 247 | uid_t uid; | ||
| 248 | gid_t gid; | ||
| 249 |
6/8✓ Branch 0 taken 1871 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 160 times.
✓ Branch 3 taken 1711 times.
✓ Branch 4 taken 160 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 160 times.
✓ Branch 7 taken 1711 times.
|
1871 | if (logFileOwner(uid, gid) && fchown(fileno(persistentLogFile), uid, gid) == 0) { |
| 250 | 160 | fchmod(fileno(persistentLogFile), 0664); | |
| 251 | } | ||
| 252 | } | ||
| 253 | struct stat st; | ||
| 254 |
1/2✓ Branch 0 taken 1890 times.
✗ Branch 1 not taken.
|
1890 | if (fstat(fileno(persistentLogFile), &st) == 0) { |
| 255 | 1890 | persistentLogIno = st.st_ino; | |
| 256 | 1890 | persistentLogDev = st.st_dev; | |
| 257 | } | ||
| 258 | 1890 | lastRotationCheck = now; | |
| 259 | } | ||
| 260 | } | ||
| 261 | 375281 | return persistentLogFile; | |
| 262 | } | ||
| 263 | |||
| 264 | 4298 | void FPPLogger::Init() { | |
| 265 |
2/2✓ Branch 0 taken 1889 times.
✓ Branch 1 taken 2409 times.
|
4298 | if (all.empty()) { |
| 266 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::General)); |
| 267 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::ChannelOut)); |
| 268 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::ChannelData)); |
| 269 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Command)); |
| 270 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::E131Bridge)); |
| 271 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Effect)); |
| 272 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::MediaOut)); |
| 273 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Playlist)); |
| 274 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Schedule)); |
| 275 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Sequence)); |
| 276 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Settings)); |
| 277 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Control)); |
| 278 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Sync)); |
| 279 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::Plugin)); |
| 280 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::GPIO)); |
| 281 |
1/2✓ Branch 0 taken 1889 times.
✗ Branch 1 not taken.
|
1889 | all.push_back(&(FPPLogger::HTTP)); |
| 282 | } | ||
| 283 | 4298 | } | |
| 284 | |||
| 285 | 471 | bool FPPLogger::SetLevel(const char* name, const char* level) { | |
| 286 | 471 | LogLevel newLevel = LOG_ERR; | |
| 287 | 471 | bool found = false; | |
| 288 |
3/4✓ Branch 0 taken 469 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 469 times.
|
471 | if (level == nullptr || name == nullptr) { |
| 289 | 2 | LogErr(VB_SETTING, "Log level (%s) and name (%s) must be specified\n", level, name); | |
| 290 | 2 | return false; | |
| 291 | } | ||
| 292 | |||
| 293 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 469 times.
|
469 | if (strcmp(level, "error") == 0) { |
| 294 | ✗ | newLevel = LOG_ERR; | |
| 295 | ✗ | found = true; | |
| 296 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 468 times.
|
469 | } else if (strcmp(level, "warn") == 0) { |
| 297 | 1 | newLevel = LOG_WARN; | |
| 298 | 1 | found = true; | |
| 299 |
2/2✓ Branch 0 taken 230 times.
✓ Branch 1 taken 238 times.
|
468 | } else if (strcmp(level, "info") == 0) { |
| 300 | 230 | newLevel = LOG_INFO; | |
| 301 | 230 | found = true; | |
| 302 |
2/2✓ Branch 0 taken 182 times.
✓ Branch 1 taken 56 times.
|
238 | } else if (strcmp(level, "debug") == 0) { |
| 303 | 182 | newLevel = LOG_DEBUG; | |
| 304 | 182 | found = true; | |
| 305 |
2/2✓ Branch 0 taken 53 times.
✓ Branch 1 taken 3 times.
|
56 | } else if (strcmp(level, "excess") == 0) { |
| 306 | 53 | newLevel = LOG_EXCESSIVE; | |
| 307 | 53 | found = true; | |
| 308 | } | ||
| 309 | |||
| 310 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 466 times.
|
469 | if (!found) { |
| 311 | 3 | LogErr(VB_SETTING, "Invalid name for LogLevel: %s\n", level); | |
| 312 | 3 | return false; | |
| 313 | } else { | ||
| 314 |
2/4✓ Branch 0 taken 466 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 466 times.
✗ Branch 3 not taken.
|
932 | return SetLevel(std::string(name), newLevel); |
| 315 | } | ||
| 316 | } | ||
| 317 | |||
| 318 | 466 | bool FPPLogger::SetLevel(std::string name, LogLevel level) { | |
| 319 | 466 | bool madeChange = false; | |
| 320 |
1/2✓ Branch 0 taken 466 times.
✗ Branch 1 not taken.
|
466 | std::string prefix("LogLevel_"); |
| 321 |
3/4✓ Branch 0 taken 466 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 306 times.
✓ Branch 3 taken 160 times.
|
466 | if (name.compare(0, prefix.size(), prefix) == 0) { |
| 322 |
1/2✓ Branch 0 taken 306 times.
✗ Branch 1 not taken.
|
306 | name = name.substr(prefix.size()); |
| 323 | } | ||
| 324 | |||
| 325 | 466 | std::vector<FPPLoggerInstance*>::iterator it; | |
| 326 |
1/2✓ Branch 0 taken 466 times.
✗ Branch 1 not taken.
|
466 | Init(); /* Just incase */ |
| 327 | |||
| 328 | // "All" sets every logger (fpp --log-level LEVEL). | ||
| 329 |
2/2✓ Branch 0 taken 7456 times.
✓ Branch 1 taken 466 times.
|
7922 | for (it = all.begin(); it != all.end(); it++) { |
| 330 | 7456 | FPPLoggerInstance* ptr = *it; | |
| 331 |
7/8✓ Branch 0 taken 6992 times.
✓ Branch 1 taken 464 times.
✓ Branch 2 taken 6992 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✓ Branch 5 taken 6976 times.
✓ Branch 6 taken 480 times.
✓ Branch 7 taken 6976 times.
|
7456 | if (name == ptr->name || name == "All") { |
| 332 |
1/2✓ Branch 0 taken 480 times.
✗ Branch 1 not taken.
|
480 | SetFacilityLevel(*ptr, level); |
| 333 | 480 | madeChange = true; | |
| 334 | } | ||
| 335 | } | ||
| 336 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 465 times.
|
466 | if (!madeChange) { |
| 337 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogWarn(VB_SETTING, "Invalid name for LoggerInstance: %s\n", name.c_str()); |
| 338 | } | ||
| 339 | |||
| 340 | 466 | return madeChange; | |
| 341 | 466 | } | |
| 342 | |||
| 343 | 1926 | int FPPLogger::MinimumLogLevel() { | |
| 344 | 1926 | int rc = LOG_ERR; | |
| 345 |
1/2✓ Branch 0 taken 1926 times.
✗ Branch 1 not taken.
|
1926 | Init(); /* Just incase */ |
| 346 | 1926 | std::vector<FPPLoggerInstance*>::iterator it; | |
| 347 | |||
| 348 |
2/2✓ Branch 0 taken 30816 times.
✓ Branch 1 taken 1926 times.
|
32742 | for (it = all.begin(); it != all.end(); it++) { |
| 349 |
1/2✓ Branch 0 taken 30816 times.
✗ Branch 1 not taken.
|
30816 | int level = FacilityLevel(**it); |
| 350 |
2/2✓ Branch 0 taken 1980 times.
✓ Branch 1 taken 28836 times.
|
30816 | if (level > rc) { |
| 351 | 1980 | rc = level; | |
| 352 | } | ||
| 353 | } | ||
| 354 | |||
| 355 | 1926 | return rc; | |
| 356 | } | ||
| 357 | |||
| 358 | 1876 | std::string FPPLogger::GetLogLevelString() { | |
| 359 |
1/2✓ Branch 0 taken 1876 times.
✗ Branch 1 not taken.
|
1876 | std::stringstream rc; |
| 360 | 1876 | LogLevel currentLevel = LOG_ERR; | |
| 361 | 1876 | bool firstLevel = true; | |
| 362 | 1876 | bool firstLogger = true; | |
| 363 | 1876 | std::vector<FPPLoggerInstance*>::iterator it; | |
| 364 | |||
| 365 |
2/2✓ Branch 0 taken 30016 times.
✓ Branch 1 taken 1876 times.
|
31892 | for (it = all.begin(); it != all.end(); it++) { |
| 366 |
1/2✓ Branch 0 taken 30016 times.
✗ Branch 1 not taken.
|
30016 | LogLevel level = FacilityLevel(**it); |
| 367 |
3/4✓ Branch 0 taken 28043 times.
✓ Branch 1 taken 1973 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 28043 times.
|
30016 | if ((currentLevel != level) || firstLevel) { |
| 368 |
2/2✓ Branch 0 taken 97 times.
✓ Branch 1 taken 1876 times.
|
1973 | if (!firstLevel) { |
| 369 |
1/2✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
|
97 | rc << ";"; |
| 370 | } | ||
| 371 | 1973 | firstLevel = false; | |
| 372 | 1973 | firstLogger = true; | |
| 373 |
3/6✓ Branch 0 taken 1973 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1973 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1973 times.
✗ Branch 5 not taken.
|
1973 | rc << LogLevelToString(level) << ":"; |
| 374 | 1973 | currentLevel = level; | |
| 375 | } | ||
| 376 |
2/2✓ Branch 0 taken 1973 times.
✓ Branch 1 taken 28043 times.
|
30016 | if (firstLogger) { |
| 377 | 1973 | firstLogger = false; | |
| 378 | } else { | ||
| 379 |
1/2✓ Branch 0 taken 28043 times.
✗ Branch 1 not taken.
|
28043 | rc << ","; |
| 380 | } | ||
| 381 |
1/2✓ Branch 0 taken 30016 times.
✗ Branch 1 not taken.
|
30016 | rc << (*it)->name; |
| 382 | } | ||
| 383 |
1/2✓ Branch 0 taken 1876 times.
✗ Branch 1 not taken.
|
3752 | return rc.str(); |
| 384 | 1876 | } | |
| 385 | |||
| 386 | 18 | void FPPLogger::SetAllLevel(LogLevel level) { | |
| 387 | 18 | std::vector<FPPLoggerInstance*>::iterator it; | |
| 388 |
1/2✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
|
18 | Init(); /* Just incase */ |
| 389 | |||
| 390 |
2/2✓ Branch 0 taken 288 times.
✓ Branch 1 taken 18 times.
|
306 | for (it = all.begin(); it != all.end(); it++) { |
| 391 |
1/2✓ Branch 0 taken 288 times.
✗ Branch 1 not taken.
|
288 | SetFacilityLevel(**it, level); |
| 392 | } | ||
| 393 | 18 | } | |
| 394 | |||
| 395 | 1263322 | std::string TruncateForLog(const std::string& s, size_t maxLen) { | |
| 396 | // Escape embedded newlines first (on the ORIGINAL string, not just the | ||
| 397 | // truncated slice) - formatLogLines() below re-prefixes every physical | ||
| 398 | // line it's given (deliberately, for attribution in this merged log | ||
| 399 | // file), so an unescaped \n in otherwise-unbounded data (a Variable's | ||
| 400 | // value, raw command output, etc.) turns one logical log entry into | ||
| 401 | // several prefixed-but-uncorrelated-looking lines. A short multi-line | ||
| 402 | // value is just as confusing as a long one, so this isn't gated behind | ||
| 403 | // the length check below. | ||
| 404 | // Only the part that survives truncation is escaped, so logging a large | ||
| 405 | // body costs maxLen rather than a copy of the whole thing. | ||
| 406 | 1263322 | std::string escaped; | |
| 407 |
1/2✓ Branch 0 taken 1263322 times.
✗ Branch 1 not taken.
|
1263322 | escaped.reserve(std::min(s.size(), maxLen + 1)); |
| 408 |
2/2✓ Branch 0 taken 46454011 times.
✓ Branch 1 taken 1251838 times.
|
47705849 | for (char c : s) { |
| 409 |
2/2✓ Branch 0 taken 11484 times.
✓ Branch 1 taken 46442527 times.
|
46454011 | if (escaped.size() > maxLen) { |
| 410 | 11484 | break; | |
| 411 | } | ||
| 412 |
2/2✓ Branch 0 taken 80693 times.
✓ Branch 1 taken 46361834 times.
|
46442527 | if (c == '\n') { |
| 413 |
1/2✓ Branch 0 taken 80693 times.
✗ Branch 1 not taken.
|
80693 | escaped += "\\n"; |
| 414 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 46361834 times.
|
46361834 | } else if (c == '\r') { |
| 415 | ✗ | escaped += "\\r"; | |
| 416 | } else { | ||
| 417 |
1/2✓ Branch 0 taken 46361834 times.
✗ Branch 1 not taken.
|
46361834 | escaped += c; |
| 418 | } | ||
| 419 | } | ||
| 420 |
2/2✓ Branch 0 taken 1251838 times.
✓ Branch 1 taken 11484 times.
|
1263322 | if (escaped.size() <= maxLen) { |
| 421 | 1251838 | return escaped; | |
| 422 | } | ||
| 423 |
5/10✓ Branch 0 taken 11484 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11484 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11484 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 11484 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 11354 times.
✗ Branch 9 not taken.
|
11484 | return escaped.substr(0, maxLen) + "...(" + std::to_string(s.size()) + " bytes total)"; |
| 424 | 1263322 | } | |
| 425 | |||
| 426 | 8515254 | bool WillLog(int level, FPPLoggerInstance& facility) { | |
| 427 | // Don't log if we're not concerned about anything at this level | ||
| 428 |
2/2✓ Branch 0 taken 8128301 times.
✓ Branch 1 taken 386953 times.
|
8515254 | if (FacilityLevel(facility) < level) |
| 429 | 8128301 | return false; | |
| 430 | |||
| 431 | 386953 | return true; | |
| 432 | } | ||
| 433 | |||
| 434 | // atomic_ref rather than an atomic member: FPPLoggerInstance's layout is | ||
| 435 | // compiled into plugins (see log.h). | ||
| 436 | 8576646 | LogLevel FacilityLevel(FPPLoggerInstance& facility) { | |
| 437 | 8576646 | return std::atomic_ref<LogLevel>(facility.level).load(std::memory_order_relaxed); | |
| 438 | } | ||
| 439 | 768 | void SetFacilityLevel(FPPLoggerInstance& facility, LogLevel level) { | |
| 440 | 768 | std::atomic_ref<LogLevel>(facility.level).store(level, std::memory_order_relaxed); | |
| 441 | 768 | } | |
| 442 | |||
| 443 | 85 | void _LogWrite(const char* file, int line, int level, FPPLoggerInstance& facility, const std::string& str, ...) { | |
| 444 | // Same gate as the char* overload -- a ring-only line still has to reach it. | ||
| 445 |
2/6✗ Branch 0 not taken.
✓ Branch 1 taken 85 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 85 times.
|
85 | if (!WillLog(level, facility) && !CrashLogRingWillCapture(level, facility)) |
| 446 | ✗ | return; | |
| 447 | // The string is the message, not a format: callers build it from | ||
| 448 | // sequence names, config values and exception text, and a '%' there | ||
| 449 | // used to be read as a conversion with no argument behind it. | ||
| 450 | 85 | _LogWrite(file, line, level, facility, "%s", str.c_str()); | |
| 451 | } | ||
| 452 | |||
| 453 | // Append msg to out, applying prefix to EVERY physical line. | ||
| 454 | // | ||
| 455 | // The prefix used to be snprintf'd together with the caller's format string and | ||
| 456 | // handed to vfprintf, which applied it once per LogX() call rather than per | ||
| 457 | // line: any message containing \n produced several physical lines with only the | ||
| 458 | // first one identifying itself. That was survivable while fppd owned this file | ||
| 459 | // outright, but fppd.log is now a merged, sequential record -- fppd, the | ||
| 460 | // fppd_start/stop/restart scripts and the operation breadcrumbs all append to it | ||
| 461 | // -- and an unlabelled line there is an unattributable line. | ||
| 462 | // | ||
| 463 | // A message that does not end in \n gets one; previously it left the line open | ||
| 464 | // and the next log call concatenated onto it. | ||
| 465 | 2388107 | static void formatLogLines(std::string& out, const char* prefix, const char* msg) { | |
| 466 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2388107 times.
|
2388107 | if (!*msg) { |
| 467 | ✗ | out.append(prefix); | |
| 468 | ✗ | out.append("\n"); | |
| 469 | ✗ | return; | |
| 470 | } | ||
| 471 | 2388107 | const char* p = msg; | |
| 472 |
2/2✓ Branch 0 taken 2399897 times.
✓ Branch 1 taken 2382543 times.
|
4782440 | while (*p) { |
| 473 | 2399897 | const char* nl = strchr(p, '\n'); | |
| 474 |
2/2✓ Branch 0 taken 2394333 times.
✓ Branch 1 taken 5564 times.
|
2399897 | size_t len = nl ? (size_t)(nl - p) : strlen(p); |
| 475 | 2399897 | out.append(prefix); | |
| 476 | 2399897 | out.append(p, len); | |
| 477 | 2399897 | out.append("\n"); | |
| 478 |
2/2✓ Branch 0 taken 5564 times.
✓ Branch 1 taken 2394333 times.
|
2399897 | if (!nl) { |
| 479 | 5564 | break; | |
| 480 | } | ||
| 481 | 2394333 | p = nl + 1; | |
| 482 | } | ||
| 483 | } | ||
| 484 | |||
| 485 | // __FILE__ is whatever path the compiler was handed. A local make passes | ||
| 486 | // paths relative to src/ ("non-gpl/BBShiftString/BBShiftString.cpp"), but a | ||
| 487 | // distributed compile (nocc) passes an absolute path rooted in the remote | ||
| 488 | // cache, e.g. "/var/cache/nocc/cpp/clients/<id>/opt/fpp/src/...", which buries | ||
| 489 | // the part that matters. Trim everything up to and including "/fpp/src/" so | ||
| 490 | // both builds log the same name. Paths without it (plugins) are untouched. | ||
| 491 | 2388107 | static const char* logSourceFile(const char* file) { | |
| 492 | static constexpr char marker[] = "/fpp/src/"; | ||
| 493 | 2388107 | const char* s = strstr(file, marker); | |
| 494 |
2/2✓ Branch 0 taken 21328 times.
✓ Branch 1 taken 2366779 times.
|
2388107 | return s ? s + sizeof(marker) - 1 : file; |
| 495 | } | ||
| 496 | |||
| 497 | 7931093 | void _LogWrite(const char* file, int line, int level, FPPLoggerInstance& facility, const char* format, ...) { | |
| 498 | // A line is formatted if it is either being logged normally OR retained in | ||
| 499 | // the crash ring -- the ring is the whole point of formatting a line the | ||
| 500 | // configured level would discard. | ||
| 501 |
1/2✓ Branch 0 taken 7931093 times.
✗ Branch 1 not taken.
|
7931093 | const bool willLog = WillLog(level, facility); |
| 502 |
1/2✓ Branch 0 taken 7931093 times.
✗ Branch 1 not taken.
|
7931093 | const bool willRing = CrashLogRingWillCapture(level, facility); |
| 503 |
4/4✓ Branch 0 taken 7547746 times.
✓ Branch 1 taken 383347 times.
✓ Branch 2 taken 5542986 times.
✓ Branch 3 taken 2004760 times.
|
7931093 | if (!willLog && !willRing) |
| 504 | 7547851 | return; | |
| 505 | |||
| 506 | va_list arg; | ||
| 507 | |||
| 508 | struct timeval tv; | ||
| 509 | 2388107 | gettimeofday(&tv, nullptr); | |
| 510 | 2388107 | int ms = tv.tv_usec / 1000; | |
| 511 | |||
| 512 | uint64_t tid; | ||
| 513 | #ifdef PLATFORM_OSX | ||
| 514 | pthread_threadid_np(NULL, &tid); | ||
| 515 | #else | ||
| 516 | 2388107 | tid = gettid(); | |
| 517 | #endif | ||
| 518 | |||
| 519 | // The "YYYY-MM-DD HH:MM:SS" field changes once per second, but building it | ||
| 520 | // cost a localtime_r plus seven width-qualified integer conversions on | ||
| 521 | // EVERY line -- together the largest single component of a log call. That | ||
| 522 | // is invisible on a desktop and not on the small ARM boards: measured on an | ||
| 523 | // AM335x, localtime_r ~5us and the full prefix snprintf ~12us, against | ||
| 524 | // ~1.7us to render the caller's actual message. | ||
| 525 | // | ||
| 526 | // So cache it, keyed on the second it describes. thread_local rather than | ||
| 527 | // shared: this is on every log call from every thread, and a lock or atomic | ||
| 528 | // here would cost more than it saves, while a torn shared buffer would | ||
| 529 | // corrupt timestamps. Per-thread cost is one time_t and 20 bytes. | ||
| 530 | // | ||
| 531 | // A timezone change (DST) is picked up on the next second boundary, since | ||
| 532 | // the key is the second itself. | ||
| 533 | thread_local time_t cachedSec = (time_t)-1; | ||
| 534 | thread_local char cachedDateTime[20]; // "YYYY-MM-DD HH:MM:SS" | ||
| 535 |
2/2✓ Branch 0 taken 32554 times.
✓ Branch 1 taken 2355553 times.
|
2388107 | if (tv.tv_sec != cachedSec) { |
| 536 | struct tm tm; | ||
| 537 | 32554 | localtime_r(&tv.tv_sec, &tm); | |
| 538 | 32554 | snprintf(cachedDateTime, sizeof(cachedDateTime), | |
| 539 | "%4d-%.2d-%.2d %.2d:%.2d:%.2d", | ||
| 540 | 32554 | 1900 + tm.tm_year, tm.tm_mon + 1, tm.tm_mday, | |
| 541 | tm.tm_hour, tm.tm_min, tm.tm_sec); | ||
| 542 | 32554 | cachedSec = tv.tv_sec; | |
| 543 | } | ||
| 544 | |||
| 545 | // The remaining numeric fields are emitted directly rather than through | ||
| 546 | // snprintf; integer conversion is the expensive part of a format string, | ||
| 547 | // and milliseconds are always exactly three digits here. | ||
| 548 | char prefix[512]; | ||
| 549 | 2388107 | char* p = prefix; | |
| 550 | 2388107 | memcpy(p, cachedDateTime, 19); | |
| 551 | 2388107 | p += 19; | |
| 552 | 2388107 | *p++ = '.'; | |
| 553 | 2388107 | *p++ = (char)('0' + (ms / 100) % 10); | |
| 554 | 2388107 | *p++ = (char)('0' + (ms / 10) % 10); | |
| 555 | 2388107 | *p++ = (char)('0' + ms % 10); | |
| 556 | 2388107 | *p++ = ' '; | |
| 557 | 2388107 | snprintf(p, sizeof(prefix) - (p - prefix), "%s(%llu) [%s] %s:%d: ", | |
| 558 | logProgramName(), tid, facility.name.c_str(), logSourceFile(file), line); | ||
| 559 | |||
| 560 | // Render the caller's message first so it can be split on newlines. The | ||
| 561 | // stack buffer covers essentially every real log line; the heap path exists | ||
| 562 | // so a long one (a dumped config blob) is not silently truncated. | ||
| 563 | char stackBuf[1024]; | ||
| 564 | 2388107 | va_start(arg, format); | |
| 565 | 2388107 | int msgLen = vsnprintf(stackBuf, sizeof(stackBuf), format, arg); | |
| 566 | 2388107 | va_end(arg); | |
| 567 | |||
| 568 | 2388107 | const char* msg = stackBuf; | |
| 569 | 2388107 | std::string heapBuf; | |
| 570 |
2/2✓ Branch 0 taken 405 times.
✓ Branch 1 taken 2387702 times.
|
2388107 | if (msgLen >= (int)sizeof(stackBuf)) { |
| 571 |
1/2✓ Branch 0 taken 405 times.
✗ Branch 1 not taken.
|
405 | heapBuf.resize(msgLen + 1); |
| 572 | 405 | va_start(arg, format); | |
| 573 | 405 | vsnprintf(&heapBuf[0], msgLen + 1, format, arg); | |
| 574 | 405 | va_end(arg); | |
| 575 | 405 | msg = heapBuf.c_str(); | |
| 576 | } | ||
| 577 | |||
| 578 | 2388107 | std::string out; | |
| 579 |
1/2✓ Branch 0 taken 2388107 times.
✗ Branch 1 not taken.
|
2388107 | out.reserve((msgLen > 0 ? (size_t)msgLen : 0) + 128); |
| 580 |
1/2✓ Branch 0 taken 2388107 times.
✗ Branch 1 not taken.
|
2388107 | formatLogLines(out, prefix, msg); |
| 581 | |||
| 582 |
2/2✓ Branch 0 taken 2357045 times.
✓ Branch 1 taken 31062 times.
|
2388107 | if (willRing) { |
| 583 | 2357045 | CrashLogRingPush(out.data(), out.size()); | |
| 584 | } | ||
| 585 |
2/2✓ Branch 0 taken 2004760 times.
✓ Branch 1 taken 383347 times.
|
2388107 | if (!willLog) { |
| 586 | // Retained for the crash report only; the configured level says this | ||
| 587 | // line does not belong in the log file or on stdout. | ||
| 588 | 2004760 | return; | |
| 589 | } | ||
| 590 | |||
| 591 | // One fwrite per log call: it keeps the whole (possibly multi-line) message | ||
| 592 | // contiguous, which matters now that shell writers append to this same file. | ||
| 593 | 383347 | bool wroteLogFile = false; | |
| 594 |
2/2✓ Branch 0 taken 381414 times.
✓ Branch 1 taken 1933 times.
|
383347 | if (logFileName[0]) { |
| 595 |
2/2✓ Branch 0 taken 6133 times.
✓ Branch 1 taken 375281 times.
|
381414 | if (!strcmp(logFileName, "stderr")) { |
| 596 |
1/2✓ Branch 0 taken 6133 times.
✗ Branch 1 not taken.
|
6133 | fwrite(out.data(), 1, out.size(), stderr); |
| 597 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 375281 times.
|
375281 | } else if (!strcmp(logFileName, "stdout")) { |
| 598 | ✗ | fwrite(out.data(), 1, out.size(), stdout); | |
| 599 | } else { | ||
| 600 | 375281 | std::unique_lock<std::recursive_timed_mutex> lock(logFileLock, std::defer_lock); | |
| 601 |
2/4✓ Branch 0 taken 375281 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 375281 times.
✗ Branch 3 not taken.
|
375281 | if (lock.try_lock_for(std::chrono::seconds(2))) { |
| 602 |
1/2✓ Branch 0 taken 375281 times.
✗ Branch 1 not taken.
|
375281 | FILE* logFile = getPersistentLogFile(tv.tv_sec); |
| 603 |
2/2✓ Branch 0 taken 375176 times.
✓ Branch 1 taken 105 times.
|
375281 | if (logFile) { |
| 604 |
1/2✓ Branch 0 taken 375176 times.
✗ Branch 1 not taken.
|
375176 | fwrite(out.data(), 1, out.size(), logFile); |
| 605 | 375176 | wroteLogFile = true; | |
| 606 | } else { | ||
| 607 |
1/2✓ Branch 0 taken 105 times.
✗ Branch 1 not taken.
|
105 | fprintf(stderr, "Error: Unable to open log file for writing!\n"); |
| 608 |
1/2✓ Branch 0 taken 105 times.
✗ Branch 1 not taken.
|
105 | fwrite(out.data(), 1, out.size(), stderr); |
| 609 | 105 | return; | |
| 610 | } | ||
| 611 | } else { | ||
| 612 | // Could not get the log file lock (holder likely wedged on | ||
| 613 | // stalled storage); don't hang here - emit to stderr instead. | ||
| 614 | ✗ | fwrite(out.data(), 1, out.size(), stderr); | |
| 615 | ✗ | return; | |
| 616 | } | ||
| 617 |
2/2✓ Branch 0 taken 375176 times.
✓ Branch 1 taken 105 times.
|
375281 | } |
| 618 | } | ||
| 619 | // For fppd, once the line is in a real log file a second copy on stdout is a | ||
| 620 | // pure duplicate: it runs as `fppd -f` under systemd, so logToStdOut stays | ||
| 621 | // set and journald captured every line a second time -- doubling the volume | ||
| 622 | // for a copy that does not outlive the boot, while fppd.log is what the | ||
| 623 | // Support Zip ships and what users are asked for. Suppress it at the source, | ||
| 624 | // here, for the callers that ask (SetSuppressDuplicateStdOut). | ||
| 625 | // | ||
| 626 | // Do NOT instead mute the stream in fppd.service: fppd's stdout is inherited | ||
| 627 | // by every child it forks (cape detection, plugin callbacks), so muting it | ||
| 628 | // there discards their output too. That was tried, and reverted. | ||
| 629 | // | ||
| 630 | // Every other binary that links the logger keeps writing both places. They | ||
| 631 | // are low volume, and `journalctl -u fppoled` / `systemctl status` is where | ||
| 632 | // a service's output is looked for -- that is what suppressing this | ||
| 633 | // globally, rather than per-caller, silently took away. | ||
| 634 | // | ||
| 635 | // `fppd -l stdout` still logs to stdout, and nothing suppresses stdout when | ||
| 636 | // no real log file is in use (early startup, before SetLogFile). | ||
| 637 | // | ||
| 638 | // The duplicate-suppression only applies when stdout is a pipe to journald. | ||
| 639 | // When a person runs `fppd -f` in a terminal, stdout is a TTY and the copy | ||
| 640 | // is a genuine on-screen echo of the log file, not a journald duplicate, so | ||
| 641 | // keep echoing every line -- that is the interactive foreground behavior. | ||
| 642 |
5/6✓ Branch 0 taken 383242 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 376910 times.
✓ Branch 3 taken 6332 times.
✓ Branch 4 taken 1971 times.
✓ Branch 5 taken 381271 times.
|
760152 | if (strcmp(logFileName, "stdout") && logToStdOut && |
| 643 |
6/6✓ Branch 0 taken 374977 times.
✓ Branch 1 taken 1933 times.
✓ Branch 2 taken 374969 times.
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 30 times.
✓ Branch 5 taken 374939 times.
|
376910 | (!wroteLogFile || !suppressDuplicateStdOut || stdoutIsInteractive())) { |
| 644 |
1/2✓ Branch 0 taken 1971 times.
✗ Branch 1 not taken.
|
1971 | fwrite(out.data(), 1, out.size(), stdout); |
| 645 | } | ||
| 646 |
4/4✓ Branch 0 taken 383242 times.
✓ Branch 1 taken 2004865 times.
✓ Branch 2 taken 383242 times.
✓ Branch 3 taken 2004865 times.
|
4392972 | } |
| 647 | |||
| 648 | 1888 | void SetSuppressDuplicateStdOut(bool suppress) { | |
| 649 | 1888 | suppressDuplicateStdOut = suppress; | |
| 650 | 1888 | } | |
| 651 | |||
| 652 | 1957 | void SetLogFile(const char* filename, bool toStdOut) { | |
| 653 |
1/2✓ Branch 0 taken 1957 times.
✗ Branch 1 not taken.
|
1957 | std::unique_lock<std::recursive_timed_mutex> lock(logFileLock); |
| 654 |
1/2✓ Branch 0 taken 1957 times.
✗ Branch 1 not taken.
|
1957 | closePersistentLogFile(); |
| 655 | 1957 | snprintf(logFileName, sizeof(logFileName), "%s", filename); | |
| 656 | 1957 | logToStdOut = toStdOut; | |
| 657 | 1957 | } | |
| 658 | |||
| 659 | 2533 | std::string LogLevelToString(LogLevel level) { | |
| 660 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2530 times.
|
2533 | if (level == LOG_WARN) { |
| 661 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
6 | return "warn"; |
| 662 |
2/2✓ Branch 0 taken 263 times.
✓ Branch 1 taken 2267 times.
|
2530 | } else if (level == LOG_DEBUG) { |
| 663 |
1/2✓ Branch 0 taken 263 times.
✗ Branch 1 not taken.
|
526 | return "debug"; |
| 664 |
2/2✓ Branch 0 taken 2202 times.
✓ Branch 1 taken 65 times.
|
2267 | } else if (level == LOG_INFO) { |
| 665 |
1/2✓ Branch 0 taken 2202 times.
✗ Branch 1 not taken.
|
4404 | return "info"; |
| 666 |
1/2✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
|
65 | } else if (level == LOG_EXCESSIVE) { |
| 667 |
1/2✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
|
130 | return "excess"; |
| 668 | ✗ | } else if (level == LOG_ERR) { | |
| 669 | ✗ | return "error"; | |
| 670 | } else { | ||
| 671 | ✗ | return "unknown"; | |
| 672 | } | ||
| 673 | } | ||
| 674 | |||
| 675 | /* | ||
| 676 | * Parse a string like debug:schedule,player;excess:mqtt for log level | ||
| 677 | */ | ||
| 678 | 217 | bool SetLogLevelComplex(std::string& input) { | |
| 679 |
1/2✓ Branch 0 taken 217 times.
✗ Branch 1 not taken.
|
217 | LogDebug(VB_SETTING, "Attempting to parse Log Levels from %s\n", input.c_str()); |
| 680 | |||
| 681 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 217 times.
|
217 | if (input.length() == 0) { |
| 682 | ✗ | LogWarn(VB_SETTING, "Ignoring Empty String sent to SetLogLevelComplex"); | |
| 683 | ✗ | return false; | |
| 684 | } | ||
| 685 | // Check if simple method | ||
| 686 |
2/2✓ Branch 0 taken 205 times.
✓ Branch 1 taken 12 times.
|
217 | if (input.find(':') == std::string::npos) { |
| 687 |
1/2✓ Branch 0 taken 205 times.
✗ Branch 1 not taken.
|
205 | LogDebug(VB_SETTING, "Using simple loging for input \"%s\" as no colon found.\n", input.c_str()); |
| 688 |
1/2✓ Branch 0 taken 205 times.
✗ Branch 1 not taken.
|
205 | return SetLogLevel(input.c_str()); |
| 689 | } | ||
| 690 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | if (input.find(';') == std::string::npos) { |
| 691 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | input = input + ';'; |
| 692 | } | ||
| 693 | |||
| 694 | 12 | bool madeChange = false; | |
| 695 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | std::istringstream ss(input); |
| 696 | 12 | std::string token1; | |
| 697 | |||
| 698 |
4/6✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✓ Branch 5 taken 12 times.
|
24 | while (std::getline(ss, token1, ';')) { |
| 699 | // token1 should be debug:scheduler,player | ||
| 700 | |||
| 701 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | LogDebug(VB_SETTING, "Outer Prase is %s\n", token1.c_str()); |
| 702 | 12 | size_t pos = token1.find(':'); | |
| 703 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | if (pos != std::string::npos) { |
| 704 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | std::string level = token1.substr(0, pos); |
| 705 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | std::string loggers = token1.substr(pos + 1); |
| 706 |
1/2✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
|
12 | std::istringstream st(loggers); |
| 707 | 12 | std::string logger; | |
| 708 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 9 times.
|
12 | if (loggers.find(',') == std::string::npos) { |
| 709 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | loggers = loggers + ","; |
| 710 | } | ||
| 711 |
4/6✓ Branch 0 taken 159 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 159 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 147 times.
✓ Branch 5 taken 12 times.
|
159 | while (std::getline(st, logger, ',')) { |
| 712 |
1/2✓ Branch 0 taken 147 times.
✗ Branch 1 not taken.
|
147 | LogDebug(VB_SETTING, "Attempting to set %s to %s \n", logger.c_str(), level.c_str()); |
| 713 |
4/6✓ Branch 0 taken 147 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 146 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
|
147 | madeChange = FPPLogger::INSTANCE.SetLevel(logger.c_str(), level.c_str()) || madeChange; |
| 714 | } | ||
| 715 | 12 | } else { | |
| 716 | ✗ | LogErr(VB_SETTING, "Invalid Log Level Token: %s\n", token1.c_str()); | |
| 717 | } | ||
| 718 | } | ||
| 719 | |||
| 720 | 12 | return madeChange; | |
| 721 | 12 | } | |
| 722 | 9 | bool SetLogLevelComplex(const char* input) { | |
| 723 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | std::string s = input; |
| 724 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
18 | return SetLogLevelComplex(s); |
| 725 | 9 | } | |
| 726 | |||
| 727 | 206 | bool SetLogLevel(const char* newLevel) { | |
| 728 | 206 | LogErr(VB_SETTING, "Using Legacy Global Logging to set all to same Log Level of %s\n", newLevel); | |
| 729 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 206 times.
|
206 | if (!strcmp(newLevel, "warn")) { |
| 730 | ✗ | FPPLogger::INSTANCE.SetAllLevel(LOG_WARN); | |
| 731 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 197 times.
|
206 | } else if (!strcmp(newLevel, "debug")) { |
| 732 | 9 | FPPLogger::INSTANCE.SetAllLevel(LOG_DEBUG); | |
| 733 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 191 times.
|
197 | } else if (!strcmp(newLevel, "info")) { |
| 734 | 6 | FPPLogger::INSTANCE.SetAllLevel(LOG_INFO); | |
| 735 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 188 times.
|
191 | } else if (!strcmp(newLevel, "excess")) { |
| 736 | 3 | FPPLogger::INSTANCE.SetAllLevel(LOG_EXCESSIVE); | |
| 737 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 188 times.
|
188 | } else if (!strcmp(newLevel, "error")) { |
| 738 | ✗ | FPPLogger::INSTANCE.SetAllLevel(LOG_ERR); | |
| 739 | } else { | ||
| 740 | 188 | LogErr(VB_SETTING, "Unknown Log Level: %s\n", newLevel); | |
| 741 | 188 | return false; | |
| 742 | } | ||
| 743 | |||
| 744 | 18 | return true; | |
| 745 | } | ||
| 746 | |||
| 747 | 1888 | int loggingToFile(void) { | |
| 748 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | if ((logFileName[0]) && |
| 749 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | (strcmp(logFileName, "stderr")) && |
| 750 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | (strcmp(logFileName, "stdout"))) |
| 751 | 1888 | return 1; | |
| 752 | |||
| 753 | ✗ | return 0; | |
| 754 | } | ||
| 755 | |||
| 756 | 1888 | void logVersionInfo(void) { | |
| 757 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | LogErr(VB_ALL, "=========================================\n"); |
| 758 |
2/4✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
|
1888 | LogErr(VB_ALL, "FPP %s\n", getFPPVersion()); |
| 759 |
2/4✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
|
1888 | LogErr(VB_ALL, "Branch: %s\n", getFPPBranch()); |
| 760 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | int o = open("/proc/device-tree/model", O_RDONLY); |
| 761 |
2/2✓ Branch 0 taken 160 times.
✓ Branch 1 taken 1728 times.
|
1888 | if (o >= 0) { |
| 762 | // A short read continues at buf + c; reading back into buf replaced | ||
| 763 | // the start of the model with its tail. | ||
| 764 | char buf[256]; | ||
| 765 | 160 | int c = 0; | |
| 766 |
1/2✓ Branch 0 taken 320 times.
✗ Branch 1 not taken.
|
320 | while (c < 255) { |
| 767 |
1/2✓ Branch 0 taken 320 times.
✗ Branch 1 not taken.
|
320 | ssize_t i = read(o, buf + c, 255 - c); |
| 768 |
2/2✓ Branch 0 taken 160 times.
✓ Branch 1 taken 160 times.
|
320 | if (i <= 0) { |
| 769 | 160 | break; | |
| 770 | } | ||
| 771 | 160 | c += i; | |
| 772 | } | ||
| 773 |
1/2✓ Branch 0 taken 160 times.
✗ Branch 1 not taken.
|
160 | if (c > 0) { |
| 774 | 160 | buf[c] = 0; | |
| 775 |
1/2✓ Branch 0 taken 160 times.
✗ Branch 1 not taken.
|
160 | LogErr(VB_ALL, "Model: %s\n", buf); |
| 776 | } | ||
| 777 |
1/2✓ Branch 0 taken 160 times.
✗ Branch 1 not taken.
|
160 | close(o); |
| 778 | } | ||
| 779 | // Kernel matters for triage far more often than it looks: the Pi's V4L2 | ||
| 780 | // decoder wedge (issues #2695/#2727) and several DRM/KMS bugs are fixed or | ||
| 781 | // broken per kernel, and asking a user for `uname -a` days after the fact | ||
| 782 | // is unreliable -- the box may have been updated since. Log it with the | ||
| 783 | // rest of the version banner so every fppd.log answers it on its own. | ||
| 784 | struct utsname un; | ||
| 785 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | if (uname(&un) == 0) { |
| 786 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | LogErr(VB_ALL, "Kernel: %s %s (%s)\n", un.sysname, un.release, un.machine); |
| 787 | } | ||
| 788 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | LogErr(VB_ALL, "=========================================\n"); |
| 789 | 1888 | } | |
| 790 |