GCC Code Coverage Report


Directory: ./
File: RecurringTasks.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 301 343 87.8%
Functions: 22 22 100.0%
Branches: 358 725 49.4%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2026 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include <chrono>
18 #include <cstring>
19 #include <pthread.h>
20 #include <regex>
21 #include <set>
22 #include <thread>
23
24 #include "RecurringTasks.h"
25 #include "Warnings.h"
26 #include "Timers.h"
27 #include "Variables.h"
28 #include "common.h"
29 #include "log.h"
30 #include "settings.h"
31
32 // A single async task's Result shouldn't be allowed to accumulate forever if
33 // its command never finishes (e.g. a hung URL fetch) - drop it and warn past
34 // this bound.
35 static constexpr long long PENDING_RESULT_MAX_MS = 60000;
36
37 // Server-side floor on task interval, independent of the admin UI's own
38 // 1-second-granularity input field - see load()'s use of this. Raised from
39 // 1000ms: most real commands (URL fetches, scripts) can easily take longer
40 // than 1s, and runTask()'s overlap guard means a too-low interval just
41 // means a task spends most of its life being skipped rather than actually
42 // running - 30s is a more realistic floor for I/O-bound work.
43 static constexpr long long MIN_TASK_INTERVAL_MS = 30000;
44
45 // How often to re-check in-flight async Results while any are pending.
46 // Results actually complete inside CurlManager::processCurls() (run every
47 // main-loop iteration), so this only bounds how soon after completion the
48 // value lands in its Variable - plenty fine against a 30s interval floor.
49 static constexpr long long PENDING_POLL_MS = 100;
50 static const std::string PENDING_POLL_TIMER_NAME = "RecurringTasks-pendingPoll";
51
52 // Timers is one namespace shared by every subsystem, and a task's name is
53 // whatever the user typed: a task named after a core timer (or the poll timer
54 // above) replaced that timer when it started and stopped it on the next
55 // Reload(). Every task timer carries this prefix, which no other timer uses.
56 29 static std::string TaskTimerName(const std::string& taskName) {
57 29 return "RecurringTask:" + taskName;
58 }
59
60 RecurringTasks RecurringTasks::INSTANCE;
61
62 2346 RecurringTasks::RecurringTasks() {
63 2346 }
64 458 RecurringTasks::~RecurringTasks() {
65 458 }
66
67 36 RecurringTasks::RecurringTask RecurringTasks::ParseTask(const Json::Value& j) {
68 36 RecurringTask task;
69
4/4
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 31 times.
✓ Branch 3 taken 3 times.
36 task.name = j["name"].asString();
70
3/6
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
31 task.enabled = j.get("enabled", true).asBool();
71
4/6
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✓ Branch 5 taken 1 times.
33 task.intervalMS = j.get("intervalMS", 60000).asInt64();
72
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.type = j["type"].asString();
73
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.preset = j["preset"].asString();
74
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.command = j["command"].asString();
75
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.args = j["args"];
76
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.resultVariable = j["resultVariable"].asString();
77
3/6
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
30 task.persistResult = j.get("persistResult", false).asBool();
78
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.filterType = j["filterType"].asString();
79
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.filterJsonPath = j["filterJsonPath"].asString();
80
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.filterBetweenStart = j["filterBetweenStart"].asString();
81
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.filterBetweenEnd = j["filterBetweenEnd"].asString();
82
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 task.filterRegex = j["filterRegex"].asString();
83 30 return task;
84 6 }
85
86 // The JSON Field and Between Markers filters (see ApplyResultFilter()).
87 11 static std::string FilterResult(const std::string& raw, const std::string& filterType,
88 const std::string& jsonPath, const std::string& betweenStart,
89 const std::string& betweenEnd) {
90
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 8 times.
11 if (filterType == "json") {
91
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (jsonPath.empty()) {
92 ✗ return raw;
93 }
94
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 Json::Value cur = LoadJsonFromString(raw);
95
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 2 times.
8 for (auto const& part : split(jsonPath, '.')) {
96
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (part.empty()) {
97 ✗ continue;
98 }
99
7/10
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 5 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 5 times.
6 if (!cur.isObject() || !cur.isMember(part)) {
100
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_GENERAL, "RecurringTasks: JSON filter path \"%s\" - \"%s\" not found in the result\n",
101 jsonPath.c_str(), part.c_str());
102
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 return "";
103 }
104
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 cur = cur[part];
105
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 }
106
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 if (cur.isString()) {
107
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return cur.asString();
108 }
109
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (cur.isNull()) {
110 ✗ return "";
111 }
112
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 return SaveJsonToString(cur);
113 3 }
114
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 if (filterType == "between") {
115 1 size_t startPos = 0;
116
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!betweenStart.empty()) {
117 1 size_t p = raw.find(betweenStart);
118
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (p == std::string::npos) {
119 ✗ LogDebug(VB_GENERAL, "RecurringTasks: between-filter start marker \"%s\" not found in the result\n",
120 betweenStart.c_str());
121 ✗ return "";
122 }
123 1 startPos = p + betweenStart.length();
124 }
125
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (betweenEnd.empty()) {
126 ✗ return raw.substr(startPos);
127 }
128 1 size_t endPos = raw.find(betweenEnd, startPos);
129
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (endPos == std::string::npos) {
130 ✗ LogDebug(VB_GENERAL, "RecurringTasks: between-filter end marker \"%s\" not found in the result\n",
131 betweenEnd.c_str());
132 ✗ return "";
133 }
134 1 return raw.substr(startPos, endPos - startPos);
135 }
136 7 return raw;
137 }
138
139 // The most a regex filter searches, and the stack its search runs on.
140 // libstdc++'s std::regex backtracks by recursing once per character a repeat
141 // consumes, so the stack a search needs grows with the input, and nothing
142 // catches a stack overflow. Measured per character: up to ~0.7 KB in an
143 // optimized build and ~2.5 KB under ASan ("(.|\n)*", "((a|b)+)c"), so 16 KB of
144 // input needs ~11 MB, or ~40 MB under ASan: more than the 8 MB the main loop
145 // and the HTTP threads have, which is why the search gets a thread of its
146 // own. Its stack is only reserved address space, committed as the recursion
147 // touches it, and it leaves 4x room for ASan. The limit also bounds time: a
148 // search that finds nothing is quadratic in the input.
149 static constexpr size_t MAX_REGEX_INPUT = 16 * 1024;
150 static constexpr size_t REGEX_STACK_SIZE = 192 * 1024 * 1024;
151
152 namespace {
153 struct RegexSearch {
154 const std::string* raw;
155 const std::string* pattern;
156 bool ok = false;
157 std::string out;
158 };
159 } // namespace
160
161 7 static void* RunRegexSearch(void* arg) {
162 7 RegexSearch* r = (RegexSearch*)arg;
163 try {
164
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
7 std::regex re(*r->pattern);
165 6 std::smatch m;
166 6 r->ok = true;
167
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 if (std::regex_search(*r->raw, m, re)) {
168
3/10
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
6 r->out = m.size() > 1 ? m[1].str() : m[0].str();
169 }
170
1/3
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
7 } catch (const std::regex_error& e) {
171
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_GENERAL, "RecurringTasks: invalid filter regex \"%s\": %s\n", r->pattern->c_str(), e.what());
172 1 r->ok = false;
173
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 r->out = std::string("Invalid Regex filter pattern: ") + e.what();
174 1 } catch (const std::exception& e) {
175 // std::regex_error(error_complexity / error_stack) and bad_alloc
176 ✗ r->ok = false;
177 ✗ r->out = std::string("Regex filter failed: ") + e.what();
178 ✗ }
179 7 return nullptr;
180 }
181
182 10 static bool RegexFilter(const std::string& raw, const std::string& pattern, std::string& out) {
183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (pattern.empty()) {
184 ✗ out = raw;
185 ✗ return true;
186 }
187 // Refused rather than cut to the limit: a value past it came back empty,
188 // which a task reported as a successful run.
189
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
10 if (raw.size() > MAX_REGEX_INPUT) {
190
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
6 out = "The result is " + std::to_string(raw.size()) + " bytes; a Regex filter searches at most " +
191
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
9 std::to_string(MAX_REGEX_INPUT / 1024) + " KB (use a JSON Field or Between Markers filter)";
192 3 return false;
193 }
194 7 RegexSearch search;
195 7 search.raw = &raw;
196 7 search.pattern = &pattern;
197 pthread_attr_t attr;
198 7 pthread_attr_init(&attr);
199 7 pthread_attr_setstacksize(&attr, REGEX_STACK_SIZE);
200 pthread_t thread;
201 7 int err = pthread_create(&thread, &attr, RunRegexSearch, &search);
202 7 pthread_attr_destroy(&attr);
203
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (err != 0) {
204 ✗ out = std::string("Could not start the Regex filter: ") + FPPstrerror(err);
205 ✗ LogErr(VB_GENERAL, "RecurringTasks: %s\n", out.c_str());
206 ✗ return false;
207 }
208
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 pthread_join(thread, nullptr);
209 7 out = std::move(search.out);
210 7 return search.ok;
211 7 }
212
213 // filterType == "json": walk a dot-path ("data.temperature") through the
214 // parsed raw response. Object-key traversal only, no array indices - covers
215 // the common "pluck one field out of a JSON API response" case without
216 // pulling in a real JSON-path library. filterType == "between": substring
217 // between two literal markers (either may be empty to mean start/end of raw
218 // instead). filterType == "regex": capture group 1 of the first match, or
219 // the whole match if the pattern has no group - listed last since it's the
220 // most powerful but least approachable option (regex syntax vs. a plain
221 // field name or literal markers). Anything else (empty/"none"/unrecognized)
222 // passes raw through unchanged.
223 21 bool RecurringTasks::ApplyResultFilter(const std::string& raw, const std::string& filterType,
224 const std::string& jsonPath, const std::string& betweenStart,
225 const std::string& betweenEnd, const std::string& regexPattern,
226 std::string& out) {
227
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 11 times.
21 if (filterType == "regex") {
228 10 return RegexFilter(raw, regexPattern, out);
229 }
230
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 out = FilterResult(raw, filterType, jsonPath, betweenStart, betweenEnd);
231 11 return true;
232 }
233
234 1890 void RecurringTasks::load() {
235 1890 std::vector<RecurringTask> tasks;
236
3/6
✓ Branch 0 taken 1890 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1890 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1890 times.
✗ Branch 5 not taken.
1890 std::string file = FPP_DIR_CONFIG("/recurringtasks.json");
237
3/4
✓ Branch 0 taken 1890 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1875 times.
✓ Branch 3 taken 15 times.
1890 if (!FileExists(file)) {
238
1/2
✓ Branch 0 taken 1875 times.
✗ Branch 1 not taken.
1875 std::unique_lock<std::mutex> l(m_tasksLock);
239 1875 m_tasks = std::move(tasks);
240 1875 return;
241 1875 }
242
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 Json::Value root = LoadJsonFromFile(file, JsonRoot::Array);
243 15 std::set<std::string> names;
244
7/12
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 20 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 35 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 20 times.
✓ Branch 11 taken 15 times.
35 for (auto const& j : root) {
245 // A task with a value of the wrong JSON type threw out of ParseTask()
246 // at startup, to std::terminate; skip that task instead.
247
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 RecurringTask task;
248 try {
249
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4 times.
20 task = ParseTask(j);
250
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 } catch (const std::exception& e) {
251
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
12 WarningHolder::InvalidConfigEntry("recurringtasks.json", e.what());
252 4 continue;
253 4 }
254
3/6
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 16 times.
16 if (task.name.empty() || task.intervalMS <= 0) {
255 ✗ LogWarn(VB_GENERAL, "RecurringTasks: skipping invalid task entry (missing name or non-positive intervalMS)\n");
256 ✗ continue;
257 }
258 // The admin UI's interval field has a 1-second floor (whole seconds,
259 // min='1'), but that's UI-only - config/recurringtasks.json can be
260 // hand-edited or written directly via the API. A task misconfigured
261 // with e.g. intervalMS=1 would fire near-continuously (unbounded
262 // concurrent URL fetches/script forks piling up faster than they can
263 // complete), so enforce the same floor server-side rather than
264 // trusting the UI to be the only writer.
265
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 15 times.
16 if (task.intervalMS < MIN_TASK_INTERVAL_MS) {
266
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_GENERAL, "RecurringTasks: task \"%s\" intervalMS %lld is below the %lldms floor, clamping\n",
267 task.name.c_str(), (long long)task.intervalMS, (long long)MIN_TASK_INTERVAL_MS);
268 1 task.intervalMS = MIN_TASK_INTERVAL_MS;
269 }
270 // The name keys the timer, the overlap guard and the status column,
271 // so a second task with the same name would replace the first one's
272 // timer and share its bookkeeping. Keep the first.
273
3/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 15 times.
16 if (!names.insert(task.name).second) {
274
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_GENERAL, "RecurringTasks: skipping task \"%s\" - another task already has that name\n",
275 task.name.c_str());
276 1 continue;
277 }
278
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 tasks.push_back(task);
279
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 5 times.
20 }
280
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 std::unique_lock<std::mutex> l(m_tasksLock);
281 15 m_tasks = std::move(tasks);
282
4/4
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 1875 times.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 1875 times.
3765 }
283
284 // Simple 1s-per-task stagger for a task's FIRST run - without this, every
285 // task sharing the same interval fires at exactly now+intervalMS on
286 // startup (Timers::addPeriodicTimer's default), and stays in lockstep
287 // forever after (each re-arm is relative to its own previous fire, so
288 // whatever phase the first run lands on persists indefinitely). Offset by
289 // config-file order (1s, 2s, 3s, ...), wrapped into [0, intervalMS) so it
290 // still means something once index*1000 would otherwise exceed a task's
291 // own interval.
292 1890 void RecurringTasks::startTimers() {
293
1/2
✓ Branch 0 taken 1890 times.
✗ Branch 1 not taken.
1890 std::unique_lock<std::mutex> l(m_tasksLock);
294 1890 long long index = 0;
295
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 1890 times.
1905 for (auto& task : m_tasks) {
296
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 14 times.
15 if (!task.enabled) {
297 1 continue;
298 }
299 14 long long stagger = (index * 1000LL) % task.intervalMS;
300
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
28 Timers::INSTANCE.addPeriodicTimer(
301
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
42 TaskTimerName(task.name), task.intervalMS, [this, task]() {
302 14 runTask(task);
303 14 },
304 stagger);
305 14 ++index;
306 }
307 1890 }
308
309 1885 void RecurringTasks::Init() {
310 1885 load();
311 1885 startTimers();
312 1885 }
313
314 1872 void RecurringTasks::Close() {
315
1/2
✓ Branch 0 taken 1872 times.
✗ Branch 1 not taken.
1872 std::unique_lock<std::mutex> l(m_tasksLock);
316
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 1872 times.
1887 for (auto& task : m_tasks) {
317
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 Timers::INSTANCE.stopPeriodicTimer(TaskTimerName(task.name));
318 }
319 1872 }
320
321 5 void RecurringTasks::Reload() {
322 5 Close();
323 5 load();
324 5 startTimers();
325 5 }
326
327 14 void RecurringTasks::runTask(const RecurringTask& task) {
328
3/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 13 times.
14 if (task.type == "preset") {
329
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!task.preset.empty()) {
330
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_COMMAND, "Recurring Task \"%s\" triggering preset \"%s\"\n", task.name.c_str(), task.preset.c_str());
331
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 CommandManager::INSTANCE.TriggerPreset(task.preset);
332 }
333
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 recordStatus(task.name, true, "");
334 1 return;
335 }
336
4/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 13 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 13 times.
13 if (task.type != "command" || task.command.empty()) {
337 ✗ return;
338 }
339
340 // Refuse to overlap: a slow command (URL fetch, script, ...) still
341 // in flight when the next tick fires would otherwise get a second
342 // (third, fourth, ...) concurrent run stacked on top of it, with
343 // nothing to stop that piling up indefinitely if the command
344 // consistently takes longer than the task's own interval. Skip this
345 // tick instead and say so loudly (log + Status column) - the fix is
346 // a longer interval or a faster command, not silently pretending it
347 // didn't happen.
348 {
349
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 std::unique_lock<std::mutex> l(m_pendingLock);
350
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 13 times.
14 for (auto const& p : m_pending) {
351
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (p.taskName == task.name) {
352 ✗ l.unlock();
353 ✗ LogErr(VB_GENERAL, "RecurringTasks: \"%s\" skipped this run - the previous run hasn't finished yet "
354 "(command is taking longer than the task's interval)\n",
355 task.name.c_str());
356 ✗ recordStatus(task.name, false, "Skipped - previous run still in progress");
357 ✗ return;
358 }
359 }
360
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 }
361
362
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 LogDebug(VB_COMMAND, "Recurring Task \"%s\" running command \"%s\"\n", task.name.c_str(), task.command.c_str());
363
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 std::unique_ptr<Command::Result> result = CommandManager::INSTANCE.run(task.command, task.args);
364
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (!result) {
365 ✗ recordStatus(task.name, false, "command returned no result");
366 ✗ return;
367 }
368
369 13 PendingResult pending;
370 13 pending.result = std::move(result);
371
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 pending.resultVariable = task.resultVariable;
372 13 pending.persistResult = task.persistResult;
373
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 pending.startTimeMS = GetMonotonicTimeMS();
374
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 pending.taskName = task.name;
375
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 pending.filterType = task.filterType;
376
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 pending.filterJsonPath = task.filterJsonPath;
377
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 pending.filterBetweenStart = task.filterBetweenStart;
378
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 pending.filterBetweenEnd = task.filterBetweenEnd;
379
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 pending.filterRegex = task.filterRegex;
380
381
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (pending.result->isDone()) {
382 ✗ finish(pending);
383 ✗ return;
384 }
385
386 {
387
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 std::unique_lock<std::mutex> l(m_pendingLock);
388
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 m_pending.push_back(std::move(pending));
389 13 }
390
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 armPollTimer();
391
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 }
392
393 // A one-shot (not periodic) timer that pollPending() re-arms itself while
394 // work remains: a periodic timer would have to stop itself from inside its
395 // own callback, and Timers::stopPeriodicTimer() deletes the TimerInfo -
396 // including the std::function currently executing - out from under it. A
397 // one-shot is removed from the timer list before its callback runs and
398 // deleted only after it returns, so re-adding under the same name from
399 // within the callback is safe. Every push into m_pending is followed by an
400 // arm on the same thread, so a poll can never be missed; at worst an arm
401 // races an in-progress poll and the extra fire finds nothing to do.
402 71 void RecurringTasks::armPollTimer() {
403
2/4
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71 times.
✗ Branch 3 not taken.
71 Timers::INSTANCE.addTimer(PENDING_POLL_TIMER_NAME, GetTimeMS() + PENDING_POLL_MS, [this]() {
404 70 pollPending();
405 70 });
406 71 }
407
408 13 void RecurringTasks::finish(PendingResult& pending) {
409
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 bool ok = !pending.result->isError();
410
3/12
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 13 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
26 std::string err = ok ? "" : pending.result->get();
411
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
13 if (!pending.resultVariable.empty() && ok) {
412 13 std::string filtered;
413
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 ok = ApplyResultFilter(pending.result->get(), pending.filterType, pending.filterJsonPath,
414
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
13 pending.filterBetweenStart, pending.filterBetweenEnd, pending.filterRegex, filtered);
415
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 2 times.
13 if (ok) {
416
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 Variables::INSTANCE.setVariable(pending.resultVariable, filtered, pending.persistResult);
417 } else {
418
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 err = filtered; // the Variable keeps its last value
419 }
420 13 }
421
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 recordStatus(pending.taskName, ok, err);
422 13 }
423
424 16 Json::Value RecurringTasks::TestRunTask(const Json::Value& taskJson) {
425
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 Json::Value result;
426
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 2 times.
16 RecurringTask task = ParseTask(taskJson);
427
428
3/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 13 times.
14 if (task.type == "preset") {
429
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!task.preset.empty()) {
430
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_COMMAND, "Recurring Task \"%s\" manual test-run triggering preset \"%s\"\n", task.name.c_str(), task.preset.c_str());
431
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 CommandManager::INSTANCE.TriggerPreset(task.preset);
432 }
433
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!task.name.empty()) {
434 ✗ recordStatus(task.name, true, "");
435 }
436
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 result["ok"] = true;
437
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 result["raw"] = "";
438
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 result["filtered"] = "";
439
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 result["error"] = "";
440
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 result["note"] = "Command Preset tasks have no output to filter.";
441 1 return result;
442 }
443
444
7/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 10 times.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 10 times.
13 if (task.type != "command" || task.command.empty()) {
445
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 result["ok"] = false;
446
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 result["raw"] = "";
447
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 result["filtered"] = "";
448
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 result["error"] = "No FPP Command selected";
449 3 return result;
450 }
451
452
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 LogDebug(VB_COMMAND, "Recurring Task \"%s\" manual test-run of command \"%s\"\n", task.name.c_str(), task.command.c_str());
453
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 std::unique_ptr<Command::Result> r = CommandManager::INSTANCE.run(task.command, task.args);
454
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (!r) {
455 ✗ result["ok"] = false;
456 ✗ result["raw"] = "";
457 ✗ result["filtered"] = "";
458 ✗ result["error"] = "command returned no result";
459 ✗ return result;
460 }
461
462 // Same ~1s busy-poll convention as CommandManager's own synchronous
463 // command execution (Commands.cpp) - this runs on an HTTP handler
464 // thread, not fppd's main loop, so blocking briefly here is safe.
465 10 int count = 0;
466
6/8
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 10 times.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 16 times.
✓ Branch 7 taken 10 times.
26 while (!r->isDone() && count < 1000) {
467
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 std::this_thread::sleep_for(std::chrono::milliseconds(1));
468 16 count++;
469 }
470
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
10 if (!r->isDone()) {
471 ✗ result["ok"] = false;
472 ✗ result["raw"] = "";
473 ✗ result["filtered"] = "";
474 ✗ result["error"] = "Timeout running command";
475 ✗ if (!task.name.empty()) {
476 ✗ recordStatus(task.name, false, "Timeout running command");
477 }
478 ✗ return result;
479 }
480
481
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 bool ok = !r->isError();
482
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 std::string raw = r->get();
483
6/10
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
18 std::string err = ok ? "" : raw;
484
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 result["raw"] = raw;
485
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 result["filtered"] = "";
486
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 if (ok) {
487 8 std::string filtered;
488
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 ok = ApplyResultFilter(raw, task.filterType, task.filterJsonPath,
489 task.filterBetweenStart, task.filterBetweenEnd, task.filterRegex, filtered);
490
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 if (!ok) {
491
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 err = filtered;
492 } else {
493
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 result["filtered"] = filtered;
494
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (!task.resultVariable.empty()) {
495
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 Variables::INSTANCE.setVariable(task.resultVariable, filtered, task.persistResult);
496 }
497 }
498 8 }
499
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 result["ok"] = ok;
500
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 result["error"] = err;
501
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 3 times.
10 if (!task.name.empty()) {
502
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 recordStatus(task.name, ok, err);
503 }
504 10 return result;
505 16 }
506
507 21 void RecurringTasks::recordStatus(const std::string& name, bool ok, const std::string& err) {
508
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (name.empty()) {
509 ✗ return;
510 }
511
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 std::unique_lock<std::mutex> l(m_statusLock);
512
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 TaskStatus& s = m_status[name];
513 21 s.hasRun = true;
514
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 s.lastRunMS = GetTimeMS();
515 21 s.lastOk = ok;
516
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 s.lastError = err;
517 21 }
518
519 55 void RecurringTasks::reportStatus(Json::Value& root) {
520
1/2
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
55 std::unique_lock<std::mutex> tl(m_tasksLock);
521
1/2
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
55 std::unique_lock<std::mutex> sl(m_statusLock);
522
2/2
✓ Branch 0 taken 207 times.
✓ Branch 1 taken 55 times.
262 for (auto const& task : m_tasks) {
523
1/2
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
207 Json::Value j;
524
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["name"] = task.name;
525
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["enabled"] = task.enabled;
526
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["intervalMS"] = (Json::Int64)task.intervalMS;
527
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["type"] = task.type;
528
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["preset"] = task.preset;
529
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["command"] = task.command;
530
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["args"] = task.args;
531
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["resultVariable"] = task.resultVariable;
532
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["persistResult"] = task.persistResult;
533
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["filterType"] = task.filterType;
534
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["filterJsonPath"] = task.filterJsonPath;
535
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["filterBetweenStart"] = task.filterBetweenStart;
536
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["filterBetweenEnd"] = task.filterBetweenEnd;
537
2/4
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 207 times.
✗ Branch 3 not taken.
207 j["filterRegex"] = task.filterRegex;
538
539
1/2
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
207 auto it = m_status.find(task.name);
540
2/2
✓ Branch 0 taken 134 times.
✓ Branch 1 taken 73 times.
207 if (it != m_status.end()) {
541
2/4
✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 134 times.
✗ Branch 3 not taken.
134 j["hasRun"] = it->second.hasRun;
542
2/4
✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 134 times.
✗ Branch 3 not taken.
134 j["lastRunMS"] = (Json::Int64)it->second.lastRunMS;
543
2/4
✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 134 times.
✗ Branch 3 not taken.
134 j["lastOk"] = it->second.lastOk;
544
2/4
✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 134 times.
✗ Branch 3 not taken.
134 j["lastError"] = it->second.lastError;
545 } else {
546
2/4
✓ Branch 0 taken 73 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 73 times.
✗ Branch 3 not taken.
73 j["hasRun"] = false;
547
2/4
✓ Branch 0 taken 73 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 73 times.
✗ Branch 3 not taken.
73 j["lastRunMS"] = 0;
548
2/4
✓ Branch 0 taken 73 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 73 times.
✗ Branch 3 not taken.
73 j["lastOk"] = true;
549
2/4
✓ Branch 0 taken 73 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 73 times.
✗ Branch 3 not taken.
73 j["lastError"] = "";
550 }
551
1/2
✓ Branch 0 taken 207 times.
✗ Branch 1 not taken.
207 root.append(j);
552 207 }
553 55 }
554
555 70 void RecurringTasks::pollPending() {
556 70 std::vector<PendingResult> toFinish;
557 bool morePending;
558 {
559
1/2
✓ Branch 0 taken 70 times.
✗ Branch 1 not taken.
70 std::unique_lock<std::mutex> l(m_pendingLock);
560 70 std::vector<PendingResult> stillPending;
561
2/2
✓ Branch 0 taken 71 times.
✓ Branch 1 taken 70 times.
141 for (auto& pending : m_pending) {
562
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 bool timedOut = (GetMonotonicTimeMS() - pending.startTimeMS) > PENDING_RESULT_MAX_MS;
563
6/8
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 58 times.
✓ Branch 3 taken 13 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 58 times.
✓ Branch 6 taken 13 times.
✓ Branch 7 taken 58 times.
71 if (pending.result->isDone() || timedOut) {
564
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 13 times.
13 if (timedOut && !pending.result->isDone()) {
565 ✗ LogWarn(VB_GENERAL, "RecurringTasks: a task's Result did not finish within %lldms, giving up\n", PENDING_RESULT_MAX_MS);
566 }
567
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 toFinish.push_back(std::move(pending));
568 } else {
569
1/2
✓ Branch 0 taken 58 times.
✗ Branch 1 not taken.
58 stillPending.push_back(std::move(pending));
570 }
571 }
572 70 m_pending = std::move(stillPending);
573 70 morePending = !m_pending.empty();
574 70 }
575
2/2
✓ Branch 0 taken 58 times.
✓ Branch 1 taken 12 times.
70 if (morePending) {
576
1/2
✓ Branch 0 taken 58 times.
✗ Branch 1 not taken.
58 armPollTimer();
577 }
578
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 70 times.
83 for (auto& pending : toFinish) {
579
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 if (pending.result->isDone()) {
580
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 finish(pending);
581 }
582 // else: timed out without finishing - drop without touching the
583 // Variable, already logged above.
584 }
585 70 }
586