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