GCC Code Coverage Report


Directory: ./
File: Scheduler.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 930 1031 90.2%
Functions: 46 55 83.6%
Branches: 1075 2002 53.7%

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
15 #include "fpp-json.h"
16
17 #include <climits>
18 #include <ctime>
19 #include <list>
20 #include <map>
21 #include <memory>
22 #include <mutex>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <string>
27 #include <thread>
28 #include <chrono>
29 #include <condition_variable>
30 #include <time.h>
31 #include <utility>
32 #include <vector>
33
34 #include "Player.h"
35 #include "Warnings.h"
36 #include "common.h"
37 #include "log.h"
38 #include "settings.h"
39 #include "commands/Commands.h"
40 #include "mediaoutput/mediaoutput.h"
41 #include "playlist/Playlist.h"
42
43 #include "Scheduler.h"
44
45 Scheduler* scheduler = NULL;
46
47 /////////////////////////////////////////////////////////////////////////////
48
49 // Test assumptions
50 static_assert(std::is_move_constructible_v<ScheduledItem>);
51
52 // Warning 23 is removed by an exact match on its text, so every site that adds
53 // or removes it for an entry has to produce the identical string.
54 // strftime() into a fixed buffer. The time and date formats come from
55 // settings; when a result doesn't fit, strftime() returns 0 and leaves the
56 // buffer's contents undefined, and the callers then read an unterminated
57 // string. Leave it empty instead.
58 151247 static void FormatTime(char* buf, size_t size, const char* fmt, const struct tm* t) {
59
2/2
✓ Branch 0 taken 64 times.
✓ Branch 1 taken 151183 times.
151247 if (strftime(buf, size, fmt, t) == 0)
60 64 buf[0] = '\0';
61 151247 }
62
63 671 static std::string missingEntryWarning(const ScheduleEntry& entry) {
64
2/6
✓ Branch 0 taken 671 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 671 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2013 return std::string("Scheduled ") + (entry.sequence ? "Sequence" : "Playlist") +
65
4/8
✗ Branch 0 not taken.
✓ Branch 1 taken 671 times.
✓ Branch 2 taken 671 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 671 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 671 times.
✗ Branch 7 not taken.
2013 " '" + entry.playlist + "' does not exist";
66 }
67
68 /////////////////////////////////////////////////////////////////////////////
69
70 // A schedule entry's args are stored as given, and the command layer accepts
71 // objects and arrays among them (an If command's condition), so asString()
72 // and operator[] can't be used on them blindly: either throws LogicError, and
73 // on the main loop nothing catches it - fppd aborted at every countdown to
74 // such an entry.
75 1909 static std::string ScheduleArgToString(const Json::Value& v) {
76
5/6
✓ Branch 0 taken 1897 times.
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1897 times.
✓ Branch 4 taken 12 times.
✓ Branch 5 taken 1897 times.
1909 if (v.isObject() || v.isArray()) {
77
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
24 return SaveJsonToString(v);
78 }
79
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1897 times.
1897 if (v.isNull()) {
80 ✗ return "";
81 }
82 1897 return v.asString();
83 }
84
85 // Whether two items carry the same first argument (how a ran item is matched).
86 43 static bool SameFirstScheduleArg(const Json::Value& a, const Json::Value& b) {
87
3/6
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 43 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 43 times.
43 if (!a.isArray() || !b.isArray()) {
88 ✗ return a == b;
89 }
90
3/6
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 43 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 43 times.
43 if (a.empty() || b.empty()) {
91 ✗ return a.empty() && b.empty();
92 }
93 43 return a[0] == b[0];
94 }
95
96 1888 Scheduler::Scheduler() :
97 1888 m_schedulerDisabled(false),
98 1888 m_loadSchedule(true),
99 1888 m_lastLoadDate(0),
100 1888 m_lastProcTime(0),
101 1888 m_lastCommandCheckTime(0),
102 1888 m_timeDelta(0),
103 1888 m_timeDeltaThreshold(0),
104 1888 m_forcedNextPlaylist(SCHEDULE_INDEX_INVALID),
105 1888 m_lastBlockedTime(0),
106 1888 m_schedulesExtendBeyondDistance(false) {
107
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 RegisterCommands();
108
109
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 m_schedulerDisabled = getSettingInt("DisableScheduler") == 1;
110
111
6/8
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1882 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 1882 times.
1888 if ((m_schedulerDisabled) && (getFPPmode() == PLAYER_MODE)) {
112
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
18 WarningHolder::AddWarning(19, "FPP Scheduler is disabled");
113
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogWarn(VB_SCHEDULE, "Scheduler is disabled!\n");
114 }
115
116 1888 m_lastProcTime = time(NULL);
117 // Seed the FPP-command catch-up window to now rather than leaving it at the
118 // epoch. CheckScheduledItems() fires any command whose second falls in
119 // (m_lastCommandCheckTime, now], which exists so a second this process
120 // MISSED -- a slow midnight reload, an NTP step -- still runs. A second
121 // that passed before this process existed was never missed by it, and the
122 // 60s catch-up cap is not a substitute: from 0 every start would replay the
123 // preceding minute of commands, so a scheduled "Outputs Off" or a restart
124 // would run a second time on every restart, and repeatedly in a restart
125 // loop.
126 1888 m_lastCommandCheckTime = m_lastProcTime;
127
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 LoadScheduleFromFile();
128
129 // Sun-relative times and the coordinates warning come from these, and
130 // are only worked out when the schedule is loaded.
131
2/2
✓ Branch 0 taken 3776 times.
✓ Branch 1 taken 1888 times.
5664 for (const char* s : { "Latitude", "Longitude" }) {
132
3/6
✓ Branch 0 taken 3776 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3776 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3776 times.
✗ Branch 5 not taken.
18560 registerSettingsListener("Scheduler", s, [this](const std::string&) {
133 2 ReloadScheduleFile();
134 2 });
135 }
136 1888 }
137
138 1867 Scheduler::~Scheduler() {
139 7468 unregisterSettingsListener("Scheduler", "Latitude");
140 5601 unregisterSettingsListener("Scheduler", "Longitude");
141 1867 }
142
143 464648 void Scheduler::ScheduleProc(void) {
144 464648 time_t procTime = time(NULL);
145
146 // Only check schedule once per second at most
147
2/2
✓ Branch 0 taken 454365 times.
✓ Branch 1 taken 10283 times.
464648 if (m_lastProcTime == procTime)
148 454365 return;
149
150 10283 int timeDelta = (int)(procTime - m_lastProcTime);
151 10283 m_lastProcTime = procTime;
152
153 // reload the schedule file once per day or if 5 seconds has elapsed
154 // since the last process time in case the time jumps
155
4/6
✓ Branch 0 taken 10283 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10283 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44 times.
✓ Branch 5 taken 10239 times.
20566 if ((m_lastLoadDate != GetCurrentDateInt()) ||
156
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 10239 times.
10283 (abs(timeDelta) > 5)) {
157 44 m_loadSchedule = true;
158
159 // Cleanup any ran items older than 2 days. SetItemRan mutates m_ranItems
160 // under m_scheduleLock from HTTP/MQTT/detached command threads, so this
161 // prune has to hold the lock too.
162 44 const time_t twoDaysAgo = time(NULL) - (2 * 24 * 60 * 60);
163
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
164
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44 times.
44 for (auto it = m_ranItems.begin(); it != m_ranItems.end(); /* in loop */) {
165 ✗ if (it->first >= twoDaysAgo)
166 ✗ break;
167 ✗ it = m_ranItems.erase(it);
168 }
169
170
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 23 times.
44 if (timeDelta < -SCHEDULE_COMMAND_CATCHUP_S) {
171 // The clock stepped back (an RTC that was ahead, corrected by
172 // NTP). Whatever "ran" at the wrong, later time hasn't really run
173 // yet: forget it so the reload below schedules it again, and pull
174 // the command check window back so commands in the stepped-over
175 // span fire when their second actually comes instead of being
176 // treated as already examined.
177
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 LogWarn(VB_SCHEDULE, "Clock stepped back %d seconds; re-arming schedule items after now\n", -timeDelta);
178
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
21 m_ranItems.erase(m_ranItems.upper_bound(procTime), m_ranItems.end());
179 21 m_lastCommandCheckTime = procTime;
180 }
181 44 }
182
183
2/2
✓ Branch 0 taken 76 times.
✓ Branch 1 taken 10207 times.
10283 if (m_loadSchedule)
184
1/2
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
76 LoadScheduleFromFile();
185
186
1/2
✓ Branch 0 taken 10283 times.
✗ Branch 1 not taken.
10283 CheckScheduledItems();
187
188 10283 return;
189 }
190
191 2402 void Scheduler::CheckIfShouldBePlayingNow(int ignoreRepeat, int forceStopped) {
192
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2396 times.
2402 if (m_schedulerDisabled)
193 6 return;
194
195 2396 LogDebug(VB_SCHEDULE, "CheckIfShouldBePlayingNow(%d, %d)\n", ignoreRepeat, forceStopped);
196
197 2396 std::time_t now = time(nullptr);
198 {
199
1/2
✓ Branch 0 taken 2396 times.
✗ Branch 1 not taken.
2396 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
200
201 // Idle in a switch is no stop (see Player::SwitchScope): re-armed
202 // then, the next pass pre-empts the playlist being switched to.
203 // Read after the status, as in the main loop.
204
1/2
✓ Branch 0 taken 2396 times.
✗ Branch 1 not taken.
2396 bool idle = Player::INSTANCE.GetStatus() == FPP_STATUS_IDLE;
205
1/2
✓ Branch 0 taken 2396 times.
✗ Branch 1 not taken.
2396 bool switching = Player::Switching();
206
207
2/2
✓ Branch 0 taken 228 times.
✓ Branch 1 taken 2288 times.
2516 for (auto& itemTime : m_scheduledItems) {
208
2/2
✓ Branch 0 taken 108 times.
✓ Branch 1 taken 120 times.
228 if (itemTime.first > now) {
209 108 break; // no need to look at items that are further in the future
210 }
211
212
1/2
✓ Branch 0 taken 120 times.
✗ Branch 1 not taken.
120 LogExcess(VB_SCHEDULE, "Checking scheduled items:\n");
213
2/2
✓ Branch 0 taken 126 times.
✓ Branch 1 taken 120 times.
246 for (auto& item : itemTime.second) {
214
2/4
✓ Branch 0 taken 126 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 126 times.
126 if (WillLog(LOG_EXCESSIVE, VB_SCHEDULE))
215 ✗ DumpScheduledItem(item.startTime, item);
216
2/2
✓ Branch 0 taken 122 times.
✓ Branch 1 taken 4 times.
126 if (item.isPlaylist) {
217
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 115 times.
122 if (item.endTime <= now) {
218
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 LogExcess(VB_SCHEDULE, "End time is in the past, skipping item\n");
219 7 continue;
220 }
221
222 115 bool ir = ignoreRepeat;
223
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 112 times.
115 if ((m_forcedNextPlaylist == item.entryIndex) &&
224
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 (m_forcedNextPlaylistStart == item.startTime)) {
225
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogExcess(VB_SCHEDULE, "This item entry index == m_forcedNextPlaylist\n");
226 3 ir = true;
227 }
228
229
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 115 times.
115 if (!switching &&
230
2/8
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 87 times.
✓ Branch 5 taken 28 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
115 (idle || (Player::INSTANCE.GetPlaylistName() != item.entry->playlist)) &&
231
2/2
✓ Branch 0 taken 102 times.
✓ Branch 1 taken 13 times.
115 (forceStopped != item.entryIndex) &&
232
5/6
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 85 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 15 times.
317 (item.entry->repeat || ir) &&
233
3/6
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 115 times.
202 !m_startsInFlight.count({ item.startTime, item.entryIndex })) {
234
1/2
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
87 LogExcess(VB_SCHEDULE, "Item run status reset\n");
235 87 item.ran = false;
236 }
237 }
238 }
239 }
240 2396 }
241 2396 LogExcess(VB_SCHEDULE, "Done checking list, proceeding to call CheckScheduledItems()\n");
242
243 2396 CheckScheduledItems(getSettingInt("restarted") == 1);
244 }
245
246 17 std::string Scheduler::GetPlaylistThatShouldBePlaying(int& repeat) {
247
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (m_schedulerDisabled)
248 ✗ return "";
249
250 17 std::time_t now = time(nullptr);
251
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
252
253
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 for (auto& itemTime : m_scheduledItems) {
254 ✗ if (itemTime.first > now) {
255 ✗ break; // no need to look at items that are further in the future
256 }
257
258 ✗ for (auto& item : itemTime.second) {
259 ✗ if (item.isPlaylist) {
260 // Skip things that should have ended in the past
261 ✗ if (item.endTime <= now) {
262 ✗ continue;
263 }
264
265 ✗ repeat = item.entry->repeat;
266 ✗ return item.entry->playlist;
267 }
268 }
269 }
270
271 17 repeat = 0;
272
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
34 return "";
273 17 }
274
275 249 void Scheduler::ReloadScheduleFile(void) {
276 249 m_loadSchedule = true;
277 249 }
278
279 344 void Scheduler::AddScheduledItems(ScheduleEntry* entry, int index, std::time_t loadTime) {
280
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 344 times.
344 if (!entry->enabled)
281 1 return;
282
283
2/2
✓ Branch 0 taken 327 times.
✓ Branch 1 taken 17 times.
344 if (!entry->playlist.empty()) {
284 327 std::string playlistFile;
285
1/2
✓ Branch 0 taken 327 times.
✗ Branch 1 not taken.
327 std::string warning = missingEntryWarning(*entry);
286
287
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 327 times.
327 if (entry->sequence) {
288 ✗ playlistFile = FPP_DIR_SEQUENCE("/" + entry->playlist);
289 } else {
290
5/10
✓ Branch 0 taken 327 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 327 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 327 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 327 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 327 times.
✗ Branch 9 not taken.
981 playlistFile = FPP_DIR_PLAYLIST("/" + entry->playlist + ".json");
291 }
292
293
3/4
✓ Branch 0 taken 327 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 326 times.
✓ Branch 3 taken 1 times.
327 if (FileExists(playlistFile)) {
294
2/4
✓ Branch 0 taken 326 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 326 times.
✗ Branch 3 not taken.
652 WarningHolder::RemoveWarning(23, warning);
295 } else {
296
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 ScheduleLoadWarning(23, warning);
297 1 entry->enabled = false;
298 1 return;
299 }
300
4/4
✓ Branch 0 taken 326 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 326 times.
✓ Branch 3 taken 1 times.
328 }
301
302 343 int dayIndex = entry->dayIndex;
303 343 std::time_t currTime = loadTime;
304 struct tm now;
305 343 localtime_r(&currTime, &now);
306
1/2
✓ Branch 0 taken 343 times.
✗ Branch 1 not taken.
343 int scheduleDistance = getSettingInt("ScheduleDistance");
307
308 // Check if this schedule's end date extends beyond the schedule distance
309 // Track this for warning display but respect the user's ScheduleDistance setting
310
3/4
✓ Branch 0 taken 343 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 112 times.
✓ Branch 3 taken 231 times.
343 if (entry->endDate > 0 && entry->endDate < 10000000) {
311 // This is a yearless date (< 10000000), calculate actual end date
312 112 int currentDate = (now.tm_year + 1900) * 10000 + (now.tm_mon + 1) * 100 + now.tm_mday;
313 112 int endMD = entry->endDate;
314
315 // Reconstruct the full end date for the current or next year
316 112 int endYear = now.tm_year + 1900;
317 112 int calculatedEndDate = endYear * 10000 + endMD;
318
319 // If calculated end date is before today, use next year
320
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 104 times.
112 if (calculatedEndDate < currentDate) {
321 8 calculatedEndDate = (endYear + 1) * 10000 + endMD;
322 }
323
324 // Calculate days from now to end date
325 112 int daysToEnd = (calculatedEndDate / 10000 - currentDate / 10000) * 365 +
326 112 ((calculatedEndDate / 100 % 100) - (currentDate / 100 % 100)) * 30 +
327 112 (calculatedEndDate % 100 - currentDate % 100);
328
329
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 103 times.
112 if (daysToEnd > scheduleDistance) {
330 9 m_schedulesExtendBeyondDistance = true;
331 }
332
1/2
✓ Branch 0 taken 231 times.
✗ Branch 1 not taken.
343 } else if (entry->endDate >= 10000000) {
333 // Full date specified
334 231 int currentDate = (now.tm_year + 1900) * 10000 + (now.tm_mon + 1) * 100 + now.tm_mday;
335 231 int daysDiff = ((entry->endDate / 10000) - (currentDate / 10000)) * 365 +
336 231 ((entry->endDate / 100 % 100) - (currentDate / 100 % 100)) * 30 +
337 231 (entry->endDate % 100 - currentDate % 100);
338
339
2/2
✓ Branch 0 taken 118 times.
✓ Branch 1 taken 113 times.
231 if (daysDiff > scheduleDistance) {
340 118 m_schedulesExtendBeyondDistance = true;
341 }
342 }
343
344 // Convert everything to a day mask to simplify code below. Odd/even is
345 // not a fixed set of weekdays, so it schedules its own occurrences here
346 // rather than through the mask; dayIndex is left as INX_ODD_DAY/EVEN_DAY.
347
4/4
✓ Branch 0 taken 337 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 332 times.
343 if ((dayIndex == INX_ODD_DAY) || (dayIndex == INX_EVEN_DAY)) {
348 // dayOffset is 0 when today already matches this entry's parity, 1
349 // when it does not (so the next - and previous - matching day is one
350 // day away, since odd/even parity alternates daily).
351
3/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 5 times.
11 int dayOffset = entry->IsOddEvenMatch(currTime) ? 0 : 1;
352
353 // Schedule yesterday if needed to handle midnight crossovers
354
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 6 times.
11 if (dayOffset)
355
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 entry->pushStartEndTimes(now.tm_wday - 1, m_timeDelta, m_timeDeltaThreshold, currTime);
356
357
2/2
✓ Branch 0 taken 140 times.
✓ Branch 1 taken 11 times.
151 for (int i = now.tm_wday + dayOffset; i <= scheduleDistance; i += 2) {
358
1/2
✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
140 entry->pushStartEndTimes(i, m_timeDelta, m_timeDeltaThreshold, currTime);
359 }
360 11 } else {
361
1/2
✓ Branch 0 taken 332 times.
✗ Branch 1 not taken.
332 dayIndex = ScheduleEntry::DayIndexToMask(dayIndex);
362 }
363
364 // Special case: if today is Sunday, a Saturday-night start that crosses
365 // midnight (e.g. 22:00->02:00) bleeds into Sunday morning. Push it only
366 // for entries that actually run on Saturday - hence the Saturday-mask
367 // test - so a Monday-only crossover entry no longer plays early Sunday.
368 // Odd/even entries are excluded because the block above already pushes
369 // yesterday (Saturday, when today is Sunday) whenever today's parity does
370 // not match; firing here too would double-push or push the wrong parity.
371
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 332 times.
343 if ((now.tm_wday == 0) &&
372
3/4
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
11 (entry->dayIndex != INX_ODD_DAY) && (entry->dayIndex != INX_EVEN_DAY) &&
373
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 (dayIndex & INX_DAY_MASK_SATURDAY))
374
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 entry->pushStartEndTimes(-1, m_timeDelta, m_timeDeltaThreshold, currTime);
375
376
4/4
✓ Branch 0 taken 337 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 332 times.
✓ Branch 3 taken 5 times.
343 if ((entry->dayIndex != INX_ODD_DAY) && (entry->dayIndex != INX_EVEN_DAY)) {
377
2/2
✓ Branch 0 taken 4300 times.
✓ Branch 1 taken 332 times.
4632 for (int weekOffset = 0; weekOffset <= scheduleDistance; weekOffset += 7) {
378
2/2
✓ Branch 0 taken 4140 times.
✓ Branch 1 taken 160 times.
4300 if (dayIndex & INX_DAY_MASK_SUNDAY)
379
1/2
✓ Branch 0 taken 4140 times.
✗ Branch 1 not taken.
4140 entry->pushStartEndTimes(INX_SUN + weekOffset, m_timeDelta, m_timeDeltaThreshold, currTime);
380
381
2/2
✓ Branch 0 taken 4165 times.
✓ Branch 1 taken 135 times.
4300 if (dayIndex & INX_DAY_MASK_MONDAY)
382
1/2
✓ Branch 0 taken 4165 times.
✗ Branch 1 not taken.
4165 entry->pushStartEndTimes(INX_MON + weekOffset, m_timeDelta, m_timeDeltaThreshold, currTime);
383
384
2/2
✓ Branch 0 taken 4185 times.
✓ Branch 1 taken 115 times.
4300 if (dayIndex & INX_DAY_MASK_TUESDAY)
385
1/2
✓ Branch 0 taken 4185 times.
✗ Branch 1 not taken.
4185 entry->pushStartEndTimes(INX_TUE + weekOffset, m_timeDelta, m_timeDeltaThreshold, currTime);
386
387
2/2
✓ Branch 0 taken 4185 times.
✓ Branch 1 taken 115 times.
4300 if (dayIndex & INX_DAY_MASK_WEDNESDAY)
388
1/2
✓ Branch 0 taken 4185 times.
✗ Branch 1 not taken.
4185 entry->pushStartEndTimes(INX_WED + weekOffset, m_timeDelta, m_timeDeltaThreshold, currTime);
389
390
2/2
✓ Branch 0 taken 4160 times.
✓ Branch 1 taken 140 times.
4300 if (dayIndex & INX_DAY_MASK_THURSDAY)
391
1/2
✓ Branch 0 taken 4160 times.
✗ Branch 1 not taken.
4160 entry->pushStartEndTimes(INX_THU + weekOffset, m_timeDelta, m_timeDeltaThreshold, currTime);
392
393
2/2
✓ Branch 0 taken 4160 times.
✓ Branch 1 taken 140 times.
4300 if (dayIndex & INX_DAY_MASK_FRIDAY)
394
1/2
✓ Branch 0 taken 4160 times.
✗ Branch 1 not taken.
4160 entry->pushStartEndTimes(INX_FRI + weekOffset, m_timeDelta, m_timeDeltaThreshold, currTime);
395
396
2/2
✓ Branch 0 taken 4165 times.
✓ Branch 1 taken 135 times.
4300 if (dayIndex & INX_DAY_MASK_SATURDAY)
397
1/2
✓ Branch 0 taken 4165 times.
✗ Branch 1 not taken.
4165 entry->pushStartEndTimes(INX_SAT + weekOffset, m_timeDelta, m_timeDeltaThreshold, currTime);
398 }
399 }
400
401 // loop through entry->startEndTimes and add to m_scheduledItems
402 343 time_t startTime = 0;
403 343 time_t endTime = 0;
404
2/2
✓ Branch 0 taken 3207 times.
✓ Branch 1 taken 343 times.
3550 for (auto& startEnd : entry->startEndTimes) {
405 3207 startTime = startEnd.first;
406 3207 endTime = startEnd.second;
407
408 // Check to see if this occurrence is within the date range
409 struct tm later;
410 3207 localtime_r(&startTime, &later);
411 3207 int dateInt = 0;
412 3207 dateInt += (later.tm_year + 1900) * 10000;
413 3207 dateInt += (later.tm_mon + 1) * 100;
414 3207 dateInt += (later.tm_mday);
415
416 // Skip if occurrence is outside the date range
417
2/4
✓ Branch 0 taken 3207 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3207 times.
3207 if (!DateInRange(dateInt, entry->startDate, entry->endDate)) {
418 ✗ continue;
419 }
420
421
1/2
✓ Branch 0 taken 3207 times.
✗ Branch 1 not taken.
3207 auto& vec = m_scheduledItems[startTime];
422 3207 vec.push_back({ .entry = entry,
423 .priority = index, // change this if we add a priority field
424 .entryIndex = index,
425 .ran = false,
426 .startTime = startTime,
427 .endTime = startTime });
428 3207 ScheduledItem& newItem = vec.back();
429
430
2/2
✓ Branch 0 taken 2317 times.
✓ Branch 1 taken 890 times.
3207 if (!entry->playlist.empty()) {
431 // Old style schedule entry without a FPP Command
432
1/2
✓ Branch 0 taken 2317 times.
✗ Branch 1 not taken.
2317 Json::Value args(Json::arrayValue);
433
2/4
✓ Branch 0 taken 2317 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2317 times.
✗ Branch 3 not taken.
2317 args.append(entry->playlist);
434
4/6
✓ Branch 0 taken 1080 times.
✓ Branch 1 taken 1237 times.
✓ Branch 2 taken 2317 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2317 times.
✗ Branch 5 not taken.
2317 args.append(entry->repeat ? "true" : "false");
435
2/4
✓ Branch 0 taken 2317 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2317 times.
✗ Branch 3 not taken.
2317 args.append("false");
436
437
1/2
✓ Branch 0 taken 2317 times.
✗ Branch 1 not taken.
2317 newItem.command = "Start Playlist";
438 2317 newItem.isPlaylist = true;
439 2317 newItem.args = std::move(args);
440 2317 newItem.endTime = endTime;
441
442 // Check to see if this item already ran
443
3/4
✓ Branch 0 taken 2317 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2316 times.
2317 if (auto it = m_ranItems.find(newItem.startTime); it != m_ranItems.end()) {
444
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto& item : it->second) {
445 1 if ((newItem.command == item.command) &&
446
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 (newItem.isPlaylist == item.isPlaylist) &&
447
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 (newItem.startTime == item.startTime) &&
448
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 (newItem.endTime == item.endTime) &&
449
2/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1 times.
2 (newItem.args.size() == item.args.size()) &&
450 ✗ SameFirstScheduleArg(newItem.args, item.args)) {
451 ✗ LogDebug(VB_SCHEDULE, "Marking playlist item as already ran:\n");
452 ✗ DumpScheduledItem(newItem.startTime, newItem);
453 ✗ newItem.ran = true;
454 }
455 }
456 }
457 2317 } else {
458 // New style schedule entry with a FPP Command
459
1/2
✓ Branch 0 taken 890 times.
✗ Branch 1 not taken.
890 newItem.command = entry->command;
460
1/2
✓ Branch 0 taken 890 times.
✗ Branch 1 not taken.
890 newItem.args = entry->args;
461 }
462 }
463
2/8
✓ Branch 0 taken 3207 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3207 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
3207 }
464
465 627 void Scheduler::DumpScheduledItem(const std::time_t itemTime, const ScheduledItem& item) {
466 char timeBuf[32];
467
1/2
✓ Branch 0 taken 627 times.
✗ Branch 1 not taken.
627 std::string timeStr = ctime_r(&itemTime, timeBuf);
468
469 627 std::string argStr;
470
2/4
✓ Branch 0 taken 627 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 627 times.
✗ Branch 3 not taken.
627 if (item.args.isArray()) {
471
7/12
✓ Branch 0 taken 627 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 627 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1909 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1909 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2536 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1909 times.
✓ Branch 11 taken 627 times.
2536 for (const auto& a : item.args) {
472
1/2
✓ Branch 0 taken 1909 times.
✗ Branch 1 not taken.
1909 argStr += "\"";
473
2/4
✓ Branch 0 taken 1909 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1909 times.
✗ Branch 3 not taken.
1909 argStr += ScheduleArgToString(a);
474
1/2
✓ Branch 0 taken 1909 times.
✗ Branch 1 not taken.
1909 argStr += "\" ";
475 }
476 } else {
477 ✗ argStr = ScheduleArgToString(item.args);
478 }
479
480
2/4
✓ Branch 0 taken 627 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 627 times.
✗ Branch 3 not taken.
627 LogDebug(VB_SCHEDULE, "%s: %2d '%s' - '%s'\n",
481 timeStr.substr(0, timeStr.length() - 1).c_str(),
482 item.priority,
483 item.command.c_str(),
484 argStr.c_str());
485 627 }
486
487 ✗ void Scheduler::DumpScheduledItems() {
488 ✗ LogDebug(VB_SCHEDULE, "DumpScheduledItems()\n");
489
490 ✗ for (const auto& itemTime : m_scheduledItems) {
491 ✗ for (const auto& item : itemTime.second) {
492 ✗ DumpScheduledItem(itemTime.first, item);
493 }
494 }
495 ✗ }
496
497 464 void Scheduler::doCountdown(const std::time_t now, const std::time_t itemTime, const std::vector<ScheduledItem>& items,
498 std::vector<CountdownPreset>& presets) {
499 // Check to see if we should be counting down to the next item
500 464 bool logItems = false;
501 464 int diff = itemTime - now;
502
503 // Print the countdown more frequently as we get closer
504
6/6
✓ Branch 0 taken 176 times.
✓ Branch 1 taken 288 times.
✓ Branch 2 taken 97 times.
✓ Branch 3 taken 79 times.
✓ Branch 4 taken 128 times.
✓ Branch 5 taken 257 times.
464 if (((diff > 300) && ((diff % 300) == 0)) ||
505
6/6
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 97 times.
✓ Branch 2 taken 27 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 256 times.
✓ Branch 5 taken 125 times.
385 ((diff > 60) && (diff <= 300) && ((diff % 60) == 0)) ||
506
6/6
✓ Branch 0 taken 132 times.
✓ Branch 1 taken 124 times.
✓ Branch 2 taken 115 times.
✓ Branch 3 taken 17 times.
✓ Branch 4 taken 125 times.
✓ Branch 5 taken 239 times.
381 ((diff > 10) && (diff <= 60) && ((diff % 10) == 0)) ||
507 ((diff <= 10))) {
508 225 logItems = true;
509 }
510
511
2/2
✓ Branch 0 taken 225 times.
✓ Branch 1 taken 239 times.
464 if (logItems) {
512
4/4
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 211 times.
✓ Branch 2 taken 224 times.
✓ Branch 3 taken 1 times.
225 LogDebug(VB_SCHEDULE, "Scheduled Item%s running in %d second%s:\n",
513 items.size() == 1 ? "" : "s",
514 diff,
515 diff == 1 ? "" : "s");
516
517
2/2
✓ Branch 0 taken 232 times.
✓ Branch 1 taken 225 times.
457 for (auto& item : items) {
518
4/4
✓ Branch 0 taken 172 times.
✓ Branch 1 taken 60 times.
✓ Branch 2 taken 94 times.
✓ Branch 3 taken 78 times.
232 if (item.isPlaylist &&
519 (diff < 1000)) {
520 char tmpStr[27];
521 94 snprintf(tmpStr, 26, "PLAYLIST_START_TMINUS_%03d", diff);
522 188 presets.push_back({ .name = tmpStr, .playlist = item.entry->playlist });
523 }
524
525
1/2
✓ Branch 0 taken 232 times.
✗ Branch 1 not taken.
232 DumpScheduledItem(itemTime, item);
526 }
527 }
528
3/8
✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 94 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 94 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
558 }
529
530 12766 void Scheduler::RunCountdownPresets(const std::vector<CountdownPreset>& presets) {
531
2/2
✓ Branch 0 taken 94 times.
✓ Branch 1 taken 12766 times.
12860 for (const auto& preset : presets) {
532 94 std::map<std::string, std::string> keywords;
533
3/6
✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 94 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 94 times.
✗ Branch 5 not taken.
188 keywords["PLAYLIST_NAME"] = preset.playlist;
534
2/4
✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 94 times.
✗ Branch 3 not taken.
94 CommandManager::INSTANCE.TriggerPreset(preset.name, keywords);
535 94 }
536 12766 }
537
538 12768 void Scheduler::RunPlayerActions(const std::vector<PlayerAction>& actions) {
539
2/2
✓ Branch 0 taken 93 times.
✓ Branch 1 taken 12768 times.
12861 for (const auto& action : actions) {
540
4/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 89 times.
✗ Branch 5 not taken.
93 switch (action.type) {
541 1 case PlayerAction::Type::StopUntilIdle:
542
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Player::INSTANCE.StopNowAllLevels(action.forceStop);
543 1 break;
544 1 case PlayerAction::Type::StopNow:
545
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Player::INSTANCE.StopNow(action.forceStop);
546 1 break;
547 2 case PlayerAction::Type::StopGracefully:
548
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Player::INSTANCE.StopGracefully(action.forceStop);
549 2 break;
550 ✗ case PlayerAction::Type::StopGracefullyAfterLoop:
551 ✗ Player::INSTANCE.StopGracefully(action.forceStop, 1);
552 ✗ break;
553 89 case PlayerAction::Type::StartPlaylist:
554 89 Player::INSTANCE.StartScheduledPlaylist(action.playlist, action.position,
555 89 action.repeat, action.entryIndex,
556 89 action.priority,
557
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 action.startTime, action.endTime, action.stopType);
558 89 break;
559 }
560 }
561 12768 }
562
563 static std::mutex s_commandThreadsLock;
564 static std::condition_variable s_commandThreadsDone;
565 static int s_commandThreads = 0;
566
567 1867 void Scheduler::WaitForCommandThreads(int timeoutMS) {
568
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 std::unique_lock<std::mutex> l(s_commandThreadsLock);
569
3/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1866 times.
3736 if (!s_commandThreadsDone.wait_for(l, std::chrono::milliseconds(timeoutMS), []() { return s_commandThreads == 0; })) {
570
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_SCHEDULE, "%d scheduled command(s) still running at shutdown\n", s_commandThreads);
571 }
572 1867 }
573
574 7 void Scheduler::doScheduledCommand(const std::time_t itemTime, const ScheduledItem& item) {
575
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 LogDebug(VB_SCHEDULE, "Running scheduled item:\n");
576
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 DumpScheduledItem(itemTime, item);
577
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 Json::Value cmd;
578
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 cmd["command"] = item.command;
579
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 cmd["args"] = item.args;
580
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 cmd["multisyncCommand"] = item.entry->multisyncCommand;
581
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 cmd["multisyncHosts"] = item.entry->multisyncHosts;
582
583 7 int entryIndex = item.entryIndex;
584 {
585
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::lock_guard<std::mutex> l(s_commandThreadsLock);
586 7 s_commandThreads++;
587 7 }
588 7 std::thread th([entryIndex](Json::Value cmd) {
589
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 SetThreadName("FPP-RunCmd");
590 // "id" rather than a row number: entryIndex is the 0-based index into
591 // the schedule vector, and that is exactly what the API reports as an
592 // entry's "id" (see BuildScheduleRangeItem / the scheduledItem["id"]
593 // below), so this is the value to cross-reference against.
594
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
7 LogDebug(VB_COMMAND, "Scheduler entry id %d running command \"%s\"\n", entryIndex, cmd["command"].asString().c_str());
595 // This thread is detached, so an exception escaping the command is not
596 // a failed command - it is std::terminate and the whole daemon goes
597 // down at whatever second the schedule fires. Commands parse
598 // user-supplied arguments (and plugins supply their own), so catch
599 // everything here and let the show keep running.
600 try {
601
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 CommandManager::INSTANCE.run(cmd);
602 ✗ } catch (const std::exception& e) {
603 ✗ LogErr(VB_COMMAND, "Scheduler entry id %d command \"%s\" threw: %s\n",
604 entryIndex, cmd["command"].asString().c_str(), e.what());
605 ✗ WarningHolder::AddWarningTimeout(60, 47, "Schedule: command '" + cmd["command"].asString() + "' failed: " + e.what());
606 ✗ } catch (...) {
607 ✗ LogErr(VB_COMMAND, "Scheduler entry id %d command \"%s\" threw an unknown exception\n",
608 entryIndex, cmd["command"].asString().c_str());
609 ✗ WarningHolder::AddWarningTimeout(60, 47, "Schedule: command '" + cmd["command"].asString() + "' failed");
610 ✗ }
611
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::lock_guard<std::mutex> l(s_commandThreadsLock);
612 7 s_commandThreads--;
613 7 s_commandThreadsDone.notify_all();
614 7 },
615
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::move(cmd));
616
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 th.detach();
617 7 }
618
619 299 bool Scheduler::doScheduledPlaylist(const std::time_t now, const std::time_t itemTime, ScheduledItem& item, bool restarted,
620 std::vector<PlayerAction>& actions) {
621 299 LogDebug(VB_SCHEDULE, "Checking scheduled 'Start Playlist'\n");
622 299 DumpScheduledItem(itemTime, item);
623
624
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 299 times.
299 if (item.endTime <= now) {
625 ✗ SetItemRan(item, true);
626
627 // don't try to start if scheduled end has passed
628 ✗ return false;
629 }
630
631 // Don't restart a playlist if it was just force stopped
632 938 if ((Player::INSTANCE.GetForceStopped()) &&
633
7/10
✓ Branch 0 taken 299 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 258 times.
✓ Branch 4 taken 41 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 40 times.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 40 times.
339 (Player::INSTANCE.GetOrigStartTime() == item.startTime) &&
634
4/8
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 40 times.
✓ Branch 3 taken 259 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 299 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
339 (Player::INSTANCE.GetForceStoppedPlaylist() == item.entry->playlist)) {
635 ✗ SetItemRan(item, true);
636 ✗ return false;
637 }
638
639
2/2
✓ Branch 0 taken 147 times.
✓ Branch 1 taken 152 times.
299 if (Player::INSTANCE.GetStatus() == FPP_STATUS_PLAYLIST_PLAYING) {
640 // If we are playing, check to see if we should be playing something else
641
642 // Check to see if we are already playing this item, for
643 // instance if we reloaded the schedule while a scheduled
644 // playlist was playing.
645
2/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 146 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
147 if ((Player::INSTANCE.GetPlaylistName() == item.entry->playlist) &&
646
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 (Player::INSTANCE.WasScheduled()) &&
647
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 (Player::INSTANCE.GetRepeat() == item.entry->repeat) &&
648
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 ((Player::INSTANCE.GetOrigStopTime() == item.endTime) ||
649
6/10
✓ Branch 0 taken 147 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 146 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 147 times.
✗ Branch 9 not taken.
149 (Player::INSTANCE.GetStopTime() == item.endTime)) &&
650
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 (Player::INSTANCE.GetStopMethod() == item.entry->stopType)) {
651 1 SetItemRan(item, true);
652 1 return false;
653 }
654
655
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 138 times.
146 if (!Player::INSTANCE.WasScheduled()) {
656 // Manually started playlist is running
657
658 // Check if playlist is protected from schedule override
659
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 5 times.
8 if (!Player::INSTANCE.GetAllowScheduleOverride()) {
660
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 LogInfo(VB_SCHEDULE, "Manual playlist '%s' is protected from schedule override, not starting scheduled playlist '%s'\n",
661 Player::INSTANCE.GetPlaylistName().c_str(), item.entry->playlist.c_str());
662 // Track this blocked scheduled item
663 3 m_lastBlockedPlaylist = item.entry->playlist;
664 3 m_lastBlockedTime = now;
665 3 return false;
666 }
667
668
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
5 if (item.startTime < Player::INSTANCE.GetStartTime()) {
669 // Due before the playlist was started by hand: waiting behind
670 // the scheduled one it replaced, or a repeating entry owed a
671 // restart. It plays once that playlist ends. Only an entry
672 // that comes due while it plays can pre-empt it.
673
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 LogDebug(VB_SCHEDULE, "Scheduled playlist '%s' was due before manual playlist '%s' started, waiting for it to end\n",
674 item.entry->playlist.c_str(), Player::INSTANCE.GetPlaylistName().c_str());
675 4 return false;
676 }
677
678 // Check priority
679
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (Player::INSTANCE.GetPriority() > item.priority) {
680 // Scheduled playlist has higher priority, stop the manual one
681
2/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
1 LogInfo(VB_SCHEDULE, "Stopping manual playlist '%s' (priority %d) for scheduled playlist '%s' (priority %d)\n",
682 Player::INSTANCE.GetPlaylistName().c_str(), Player::INSTANCE.GetPriority(),
683 item.entry->playlist.c_str(), item.priority);
684 1 actions.push_back({ .type = PlayerAction::Type::StopUntilIdle,
685 .forceStop = 1 });
686 } else {
687 // Manual playlist has higher or equal priority, let it continue
688 ✗ LogDebug(VB_SCHEDULE, "Manual playlist '%s' (priority %d) has higher priority than scheduled playlist '%s' (priority %d), not overriding\n",
689 Player::INSTANCE.GetPlaylistName().c_str(), Player::INSTANCE.GetPriority(),
690 item.entry->playlist.c_str(), item.priority);
691 ✗ return false;
692 }
693
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 136 times.
138 } else if (Player::INSTANCE.GetPriority() > item.priority) {
694 // Lower priority (higher number) playlist is running
695 // Reset the ran status on the schedule entry for the
696 // running playlist to false so it shows as 'next' again
697
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::time_t oldStartTime = Player::INSTANCE.GetOrigStartTime();
698
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string playlistName = Player::INSTANCE.GetPlaylistName();
699
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (auto it = m_scheduledItems.find(oldStartTime); it != m_scheduledItems.end()) {
700
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& oldItem : it->second) {
701
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
4 if (oldItem.isPlaylist &&
702 2 (oldItem.entry->playlist == playlistName)) {
703 2 oldItem.ran = false;
704 2 m_forcedNextPlaylist = oldItem.entryIndex;
705 2 m_forcedNextPlaylistStart = oldItem.startTime;
706 }
707 }
708 }
709
710 2 int forceStop = 0;
711
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
2 if (Player::INSTANCE.GetStopTime() < Player::INSTANCE.GetOrigStopTime()) {
712 ✗ forceStop = 1;
713 }
714
715 // Stop whatever is playing the way its entry says to, and start
716 // this item once the player is idle: a hard stop is idle by the
717 // rescan this pass does after its actions, a graceful one when the
718 // running playlist finishes. This item is deliberately not marked
719 // ran. It used to queue its start straight after the stop, and
720 // the start cut the stopping playlist off - every pre-emption was a
721 // hard stop whatever stop type the running entry had.
722
2/5
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
2 switch (Player::INSTANCE.GetStopMethod()) {
723 2 case 0:
724 2 actions.push_back({ .type = PlayerAction::Type::StopGracefully,
725 .forceStop = forceStop });
726 2 break;
727 ✗ case 2:
728 ✗ actions.push_back({ .type = PlayerAction::Type::StopGracefullyAfterLoop,
729 .forceStop = forceStop });
730 ✗ break;
731 ✗ case 1:
732 default:
733 ✗ actions.push_back({ .type = PlayerAction::Type::StopNow,
734 .forceStop = forceStop });
735 ✗ break;
736 }
737 2 return false;
738 2 } else {
739 // Need to wait for current Scheduled Higher-Priority
740 // item to end until we can play multiple playlists
741 // concurrently
742 136 return false;
743 }
744
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 86 times.
152 } else if (Player::INSTANCE.GetStatus() != FPP_STATUS_IDLE) {
745 // We are either paused or stopping already so do nothing
746 66 return false;
747 }
748
749 87 LogDebug(VB_SCHEDULE, "Starting Scheduled Playlist:\n");
750 87 DumpScheduledItem(itemTime, item);
751
752 87 int position = 0;
753
8/12
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 83 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✓ Branch 7 taken 83 times.
✓ Branch 8 taken 4 times.
✓ Branch 9 taken 83 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
87 if (restarted && (getSetting("resumePlaylist") == item.entry->playlist)) {
754 4 position = getSettingInt("resumePosition");
755
756
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.
16 SetSetting("resumePlaylist", "");
757
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 SetSetting("resumePosition", 0);
758 }
759
760
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 86 times.
87 if ((m_forcedNextPlaylist == item.entryIndex) &&
761
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 (m_forcedNextPlaylistStart == item.startTime)) {
762 // The pre-empted occurrence is getting its restart; that was the
763 // whole of what the force was for.
764 1 m_forcedNextPlaylist = SCHEDULE_INDEX_INVALID;
765 }
766
767
1/2
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
87 m_startsInFlight.insert({ item.startTime, item.entryIndex });
768 87 actions.push_back({ .type = PlayerAction::Type::StartPlaylist,
769 87 .playlist = item.entry->playlist,
770 .position = position,
771 87 .repeat = item.entry->repeat,
772 87 .entryIndex = item.entryIndex,
773 87 .priority = item.entryIndex, // priority is entry index for now
774 87 .startTime = item.startTime,
775 87 .endTime = item.endTime,
776 87 .stopType = item.entry->stopType });
777 87 return true;
778
4/22
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 87 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 87 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
90 }
779
780 // How far ahead of a scheduled playlist the audio output is brought up.
781 static constexpr std::time_t MEDIA_PREWARM_LEAD_S = 5;
782
783 12679 void Scheduler::CheckScheduledItems(bool restarted) {
784
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 12677 times.
12679 if (m_schedulerDisabled)
785 2 return;
786
787 12677 std::time_t now = time(nullptr);
788 12677 std::vector<PlayerAction> actions;
789 12677 std::vector<CountdownPreset> presets;
790 // How long to hold the audio output up for a playlist about to start (0 =
791 // none due). Opening it takes most of a second on slow boards, so do it
792 // ahead of time rather than when the first track starts.
793 12677 int prewarmMs = 0;
794
795 // Longest a scheduled FPP command may fire after its second passed. Bounds
796 // the (m_lastCommandCheckTime, now] catch-up window so a forward clock step
797 // does not replay a batch of stale commands; anything older is logged and
798 // marked ran instead of run.
799 12677 constexpr std::time_t maxCommandCatchup = SCHEDULE_COMMAND_CATCHUP_S;
800
801 // Each pass decides what to do while holding m_scheduleLock and runs it
802 // after releasing the lock, since starting or stopping a playlist takes
803 // m_playlistMutex in the opposite order (see Scheduler.h). A pass stops
804 // scanning as soon as it has an action so that the item after it is still
805 // judged against the player's real state rather than the state the player
806 // had before the action ran, which is what deciding for every item up
807 // front would do.
808 do {
809 12766 actions.clear();
810 12766 presets.clear();
811
812 {
813
1/2
✓ Branch 0 taken 12766 times.
✗ Branch 1 not taken.
12766 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
814
815 // With the player idle and several playlists due, start the
816 // highest-priority one (lowest entry index) rather than whichever
817 // started earliest. Starting the earlier, lower-priority one
818 // played the wrong show until the next pass pre-empted it; and
819 // after a graceful pre-emption the stopped entry is due again (it
820 // is owed a restart) and would win again, pre-empting forever.
821 12766 int bestIdlePriority = INT_MAX;
822
3/4
✓ Branch 0 taken 12766 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9576 times.
✓ Branch 3 taken 3190 times.
12766 if (Player::INSTANCE.GetStatus() == FPP_STATUS_IDLE) {
823
2/2
✓ Branch 0 taken 426 times.
✓ Branch 1 taken 9341 times.
9767 for (auto& due : m_scheduledItems) {
824
2/2
✓ Branch 0 taken 235 times.
✓ Branch 1 taken 191 times.
426 if (due.first > now)
825 235 break;
826
2/2
✓ Branch 0 taken 198 times.
✓ Branch 1 taken 191 times.
389 for (auto& c : due.second) {
827
3/4
✓ Branch 0 taken 101 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 101 times.
✗ Branch 3 not taken.
110 if (!c.ran && c.isPlaylist && (c.endTime > now) &&
828
4/4
✓ Branch 0 taken 110 times.
✓ Branch 1 taken 88 times.
✓ Branch 2 taken 95 times.
✓ Branch 3 taken 6 times.
403 (c.priority < bestIdlePriority) &&
829
4/6
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 95 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 95 times.
✓ Branch 5 taken 103 times.
293 !m_startsInFlight.count({ c.startTime, c.entryIndex })) {
830 95 bestIdlePriority = c.priority;
831 }
832 }
833 }
834 }
835
836
2/2
✓ Branch 0 taken 1391 times.
✓ Branch 1 taken 12213 times.
13604 for (auto& itemTime : m_scheduledItems) {
837
2/2
✓ Branch 0 taken 464 times.
✓ Branch 1 taken 927 times.
1391 if (itemTime.first > now) {
838
1/2
✓ Branch 0 taken 464 times.
✗ Branch 1 not taken.
464 doCountdown(now, itemTime.first, itemTime.second, presets);
839
2/2
✓ Branch 0 taken 72 times.
✓ Branch 1 taken 392 times.
464 if (itemTime.first - now <= MEDIA_PREWARM_LEAD_S) {
840
2/2
✓ Branch 0 taken 72 times.
✓ Branch 1 taken 31 times.
103 for (auto& item : itemTime.second) {
841
3/4
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 31 times.
72 if (!item.ran && item.isPlaylist) {
842 // Through the start, plus the usual linger in
843 // case the playlist opens with no media.
844 41 prewarmMs = (int)((itemTime.first - now + MEDIA_PREWARM_LEAD_S) * 1000);
845 41 break;
846 }
847 }
848 }
849 464 break; // no need to look at items that are further in the future
850 }
851
852
2/2
✓ Branch 0 taken 948 times.
✓ Branch 1 taken 840 times.
1788 for (auto& item : itemTime.second) {
853
2/2
✓ Branch 0 taken 630 times.
✓ Branch 1 taken 318 times.
948 if (item.ran)
854 630 continue; // skip over any items that ran already
855
856
2/2
✓ Branch 0 taken 308 times.
✓ Branch 1 taken 10 times.
318 if (item.isPlaylist) {
857 // While any scheduled start is still being carried
858 // out the player is busy, whatever its status says:
859 // it reads idle until that playlist has loaded.
860 // Guarding only the in-flight item let a concurrent
861 // pass start the next, lower-priority item into the
862 // same player and lose the higher one. Skipped items
863 // keep their ran flag clear, and the pass doing the
864 // start rescans once it has finished.
865
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 308 times.
308 if (!m_startsInFlight.empty())
866 ✗ continue;
867 // Nor while one playlist is being replaced by another
868 // (see Player::SwitchScope).
869
3/4
✓ Branch 0 taken 308 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 307 times.
308 if (Player::Switching())
870 1 continue;
871
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 299 times.
307 if (item.priority > bestIdlePriority)
872 8 continue; // a higher-priority playlist is due; it goes first
873
3/4
✓ Branch 0 taken 299 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 212 times.
✓ Branch 3 taken 87 times.
299 if (!doScheduledPlaylist(now, itemTime.first, item, restarted, actions)) {
874 212 continue;
875 }
876 } else {
877 // An FPP Command has no duration to catch up to, so a
878 // missed second would silently drop it. Fire any
879 // command whose second falls in the (lastCheck, now]
880 // window this run has not examined yet, but never one
881 // more than maxCommandCatchup seconds late so a large
882 // clock step cannot replay a batch of stale commands.
883
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
10 if (itemTime.first <= m_lastCommandCheckTime) {
884 // A prior run already had its chance at this second.
885 // A reload can reset item.ran (FPP-command ran-state
886 // is not restored from m_ranItems), so gate on the
887 // check window, not on item.ran, to avoid re-firing.
888
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 SetItemRan(item, true);
889 3 continue;
890 }
891
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if ((now - itemTime.first) > maxCommandCatchup) {
892 ✗ LogWarn(VB_SCHEDULE,
893 "Scheduled command '%s' skipped, %ld seconds late (catch-up cap is %ld s)\n",
894 item.command.c_str(), (long)(now - itemTime.first),
895 (long)maxCommandCatchup);
896 ✗ SetItemRan(item, true);
897 ✗ continue;
898 }
899
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 doScheduledCommand(itemTime.first, item);
900 }
901
902
1/2
✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
94 SetItemRan(item, true);
903
904
2/2
✓ Branch 0 taken 87 times.
✓ Branch 1 taken 7 times.
94 if (!actions.empty())
905 87 break;
906 }
907
908
2/2
✓ Branch 0 taken 89 times.
✓ Branch 1 taken 838 times.
927 if (!actions.empty())
909 89 break;
910 }
911
912 // A switch that began after the checks above read the player
913 // idle: a start now would replace the playlist being switched to.
914 // Drop this pass's actions, and leave its start to a later one.
915
5/8
✓ Branch 0 taken 89 times.
✓ Branch 1 taken 12677 times.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 89 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 12766 times.
12766 if (!actions.empty() && Player::Switching()) {
916 ✗ LogDebug(VB_SCHEDULE, "A playlist switch began, dropping this pass's scheduled actions\n");
917 ✗ for (const auto& action : actions) {
918 ✗ if (action.type != PlayerAction::Type::StartPlaylist)
919 ✗ continue;
920 ✗ m_startsInFlight.erase({ action.startTime, action.entryIndex });
921 ✗ if (auto it = m_scheduledItems.find(action.startTime); it != m_scheduledItems.end()) {
922 ✗ for (auto& item : it->second) {
923 ✗ if (item.isPlaylist && (item.entryIndex == action.entryIndex))
924 ✗ SetItemRan(item, false);
925 }
926 }
927 }
928 ✗ actions.clear();
929 }
930 12766 }
931
932 {
933 // Clear this pass's in-flight marks however RunPlayerActions()
934 // leaves: every scheduled playlist start waits while any mark is
935 // set, so one leaked by an exception would stop the schedule.
936 struct InFlightGuard {
937 Scheduler* s;
938 const std::vector<PlayerAction>& actions;
939 12766 ~InFlightGuard() {
940 12766 std::unique_lock<std::recursive_mutex> lock(s->m_scheduleLock);
941
2/2
✓ Branch 0 taken 90 times.
✓ Branch 1 taken 12766 times.
12856 for (const auto& action : actions) {
942
2/2
✓ Branch 0 taken 87 times.
✓ Branch 1 taken 3 times.
90 if (action.type == PlayerAction::Type::StartPlaylist)
943 87 s->m_startsInFlight.erase({ action.startTime, action.entryIndex });
944 }
945 12766 }
946 12766 } inFlightGuard{ this, actions };
947
1/2
✓ Branch 0 taken 12766 times.
✗ Branch 1 not taken.
12766 RunPlayerActions(actions);
948 12766 }
949
1/2
✓ Branch 0 taken 12766 times.
✗ Branch 1 not taken.
12766 RunCountdownPresets(presets);
950
2/2
✓ Branch 0 taken 89 times.
✓ Branch 1 taken 12677 times.
12766 } while (!actions.empty());
951
952
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 12636 times.
12677 if (prewarmMs > 0) {
953
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 PrewarmMediaOutput(prewarmMs);
954 }
955
956 // Advance the command catch-up window past every second this call examined.
957 // Done once, after all rescans, so each rescan judges commands against the
958 // same window; within a call the item.ran flag prevents a second firing.
959 // Left unchanged if the clock ran backwards so the window never moves ahead
960 // of the wall clock.
961 // Under the lock: this runs on the main loop and on API threads, and an
962 // unlocked read-compare-write could move the window backwards.
963
1/2
✓ Branch 0 taken 12677 times.
✗ Branch 1 not taken.
12677 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
964
2/2
✓ Branch 0 taken 10271 times.
✓ Branch 1 taken 2406 times.
12677 if (now > m_lastCommandCheckTime)
965 10271 m_lastCommandCheckTime = now;
966 12677 }
967
968 1964 void Scheduler::ClearScheduledItems() {
969 1964 m_scheduledItems.clear();
970 1964 }
971
972 98 void Scheduler::SetItemRan(ScheduledItem& item, bool ran) {
973 98 item.ran = ran;
974
975
1/2
✓ Branch 0 taken 98 times.
✗ Branch 1 not taken.
98 auto& vec = m_ranItems[item.startTime];
976
1/2
✓ Branch 0 taken 98 times.
✗ Branch 1 not taken.
98 auto it = std::find_if(vec.begin(), vec.end(), [&](const ScheduledItem& ranItem) {
977 44 return item.command == ranItem.command &&
978
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 item.isPlaylist == ranItem.isPlaylist &&
979
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 item.startTime == ranItem.startTime &&
980
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 1 times.
44 item.endTime == ranItem.endTime &&
981
2/4
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43 times.
✗ Branch 3 not taken.
131 item.args.size() == ranItem.args.size() &&
982
1/2
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
87 SameFirstScheduleArg(item.args, ranItem.args);
983 });
984
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 55 times.
98 if (it != vec.end()) {
985 43 it->ran = ran;
986 } else {
987 // Without the entry pointer: it points into m_Schedule, which the next
988 // reload frees, while ran-state outlives reloads.
989 55 vec.push_back({ .entry = nullptr,
990 55 .priority = item.priority,
991 55 .entryIndex = item.entryIndex,
992 .ran = ran,
993 55 .startTime = item.startTime,
994 55 .endTime = item.endTime,
995 55 .command = item.command,
996 55 .args = item.args,
997 55 .isPlaylist = item.isPlaylist });
998 }
999
3/10
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 55 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
153 }
1000
1001 1964 void Scheduler::LoadScheduleFromFile(void) {
1002
3/6
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1964 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1964 times.
✗ Branch 5 not taken.
1964 std::string SCHEDULE_FILE = FPP_DIR_CONFIG("/schedule.json");
1003
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 LogDebug(VB_SCHEDULE, "Loading Schedule from %s\n", SCHEDULE_FILE.c_str());
1004
1005 1964 m_loadSchedule = false;
1006 // One clock reading for the whole load: every entry and every occurrence
1007 // is placed relative to it, so a load that runs across midnight cannot
1008 // anchor some of them to the old day (or week) and some to the new one.
1009 // The load date comes from the same reading, so a load anchored to the
1010 // old day is redone once the new day is seen.
1011 1964 const std::time_t loadTime = std::time(nullptr);
1012 {
1013 struct tm lt;
1014 1964 localtime_r(&loadTime, &lt);
1015 1964 m_lastLoadDate = (lt.tm_year + 1900) * 10000 + (lt.tm_mon + 1) * 100 + lt.tm_mday;
1016 }
1017 // Under the lock, like the AddScheduledItems() writes below and the read
1018 // in GetSchedule(), which an API thread makes while this reloads.
1019
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1020 1964 m_schedulesExtendBeyondDistance = false;
1021 1964 m_Schedule.clear();
1022
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 ClearScheduledItems();
1023
1024 1964 std::string playlistFile;
1025
1026 // A player that has never had a schedule has no schedule.json, which is a
1027 // normal state and not worth reading a file for -- an absent file and an
1028 // empty schedule are the same thing to everything below.
1029
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 Json::Value sch;
1030
3/4
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 171 times.
✓ Branch 3 taken 1793 times.
1964 if (FileExists(SCHEDULE_FILE)) {
1031
1/2
✓ Branch 0 taken 171 times.
✗ Branch 1 not taken.
171 sch = LoadJsonFromFile(SCHEDULE_FILE, JsonRoot::Array);
1032 }
1033
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 ClearScheduleLoadWarnings();
1034
3/4
✓ Branch 0 taken 2310 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 346 times.
✓ Branch 3 taken 1964 times.
2310 for (int i = 0; i < sch.size(); i++) {
1035
1/2
✓ Branch 0 taken 346 times.
✗ Branch 1 not taken.
346 ScheduleEntry scheduleEntry;
1036 // jsoncpp throws on a wrong-typed field ("day":"7", an entry that is
1037 // not an object). This runs from the Scheduler constructor and from
1038 // the main loop's reloads with nothing above it to catch, so one bad
1039 // entry took fppd down -- and again on every restart. Skip the entry.
1040 try {
1041
4/6
✓ Branch 0 taken 346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 344 times.
346 if (!scheduleEntry.LoadFromJson(sch[i]))
1042 ✗ continue;
1043
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 } catch (const std::exception& ex) {
1044
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 std::string warning = "Schedule entry " + std::to_string(i + 1) + " is invalid and was skipped: " + ex.what();
1045
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_SCHEDULE, "%s\n", warning.c_str());
1046
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 ScheduleLoadWarning(47, warning);
1047 2 continue;
1048 2 }
1049
1050
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 std::string warning = missingEntryWarning(scheduleEntry);
1051
1052
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 344 times.
344 if (scheduleEntry.sequence) {
1053 ✗ playlistFile = FPP_DIR_SEQUENCE("/" + scheduleEntry.playlist);
1054 } else {
1055
5/10
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 344 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 344 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 344 times.
✗ Branch 9 not taken.
688 playlistFile = FPP_DIR_PLAYLIST("/" + scheduleEntry.playlist + ".json");
1056 }
1057
1058
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 if ((getFPPmode() != PLAYER_MODE) &&
1059
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 344 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 344 times.
344 (scheduleEntry.enabled) &&
1060 ✗ (!scheduleEntry.playlist.empty())) {
1061 ✗ std::string warning = std::string("Playlist '") + scheduleEntry.playlist +
1062 ✗ "' was scheduled but system is running in Remote mode.";
1063 ✗ LogWarn(VB_SCHEDULE, "%s\n", warning.c_str());
1064 ✗ ScheduleLoadWarning(29, warning);
1065 ✗ continue;
1066 ✗ }
1067
1068 1032 if ((scheduleEntry.enabled) &&
1069
4/6
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 327 times.
✓ Branch 3 taken 17 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 344 times.
671 (!scheduleEntry.playlist.empty()) &&
1070
2/4
✓ Branch 0 taken 327 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 327 times.
327 (!FileExists(playlistFile))) {
1071 ✗ LogErr(VB_SCHEDULE, "ERROR: Scheduled %s '%s' does not exist\n",
1072 scheduleEntry.sequence ? "Sequence" : "Playlist",
1073 scheduleEntry.playlist.c_str());
1074 ✗ ScheduleLoadWarning(23, warning);
1075 ✗ continue;
1076 } else {
1077
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
344 WarningHolder::RemoveWarning(23, warning);
1078 }
1079
1080
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 m_Schedule.push_back(scheduleEntry);
1081
3/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✓ Branch 3 taken 2 times.
346 }
1082
1083
2/2
✓ Branch 0 taken 344 times.
✓ Branch 1 taken 1964 times.
2308 for (int i = 0; i < m_Schedule.size(); i++) {
1084
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 AddScheduledItems(&m_Schedule[i], i, loadTime);
1085 }
1086
1087
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 SchedulePrint();
1088
1089
2/4
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1964 times.
1964 if (WillLog(LOG_EXCESSIVE, VB_SCHEDULE))
1090 ✗ DumpScheduledItems();
1091
1092
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 lock.unlock();
1093
1094 3928 return;
1095 1964 }
1096
1097 1964 void Scheduler::SchedulePrint(void) {
1098 1964 int i = 0;
1099 1964 char stopTypes[4] = "GHL";
1100 1964 std::string dayStr;
1101
1102
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1958 times.
1964 if (m_schedulerDisabled)
1103
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogInfo(VB_SCHEDULE, "WARNING: Scheduler is currently disabled\n");
1104
1105
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 LogInfo(VB_SCHEDULE, "Current Schedule: (Status: '+' = Enabled, '-' = Disabled, '!' = Outside Date Range, '*' = Repeat, Stop (G)raceful/(L)oop/(H)ard\n");
1106
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 LogInfo(VB_SCHEDULE, "Stat Start & End Dates Days Start & End Times Playlist/Command\n");
1107
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 LogInfo(VB_SCHEDULE, "---- ----------------------- ------------- ------------------- ---------------------------------------------\n");
1108
2/2
✓ Branch 0 taken 344 times.
✓ Branch 1 taken 1964 times.
2308 for (i = 0; i < m_Schedule.size(); i++) {
1109
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 dayStr = GetDayTextFromDayIndex(m_Schedule[i].dayIndex);
1110
14/16
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 327 times.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 327 times.
✓ Branch 4 taken 17 times.
✓ Branch 5 taken 327 times.
✓ Branch 6 taken 211 times.
✓ Branch 7 taken 133 times.
✓ Branch 8 taken 344 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 172 times.
✓ Branch 11 taken 172 times.
✓ Branch 12 taken 343 times.
✓ Branch 13 taken 1 times.
✓ Branch 14 taken 344 times.
✗ Branch 15 not taken.
344 LogInfo(VB_SCHEDULE, "%c%c%c%c %04d-%02d-%02d - %04d-%02d-%02d %-13.13s %-8.8s %c %-8.8s %s\n",
1111 m_Schedule[i].enabled ? '+' : '-',
1112 CurrentDateInRange(m_Schedule[i].startDate, m_Schedule[i].endDate) ? ' ' : '!',
1113 m_Schedule[i].repeat ? '*' : ' ',
1114 stopTypes[m_Schedule[i].stopType],
1115 (int)(m_Schedule[i].startDate / 10000),
1116 (int)(m_Schedule[i].startDate % 10000 / 100),
1117 (int)(m_Schedule[i].startDate % 100),
1118 (int)(m_Schedule[i].endDate / 10000),
1119 (int)(m_Schedule[i].endDate % 10000 / 100),
1120 (int)(m_Schedule[i].endDate % 100),
1121 dayStr.c_str(),
1122 m_Schedule[i].startTimeStr.c_str(),
1123 m_Schedule[i].playlist.empty() ? ' ' : '-',
1124 m_Schedule[i].playlist.empty() ? "" : m_Schedule[i].endTimeStr.c_str(),
1125 m_Schedule[i].playlist.empty() ? m_Schedule[i].command.c_str() : m_Schedule[i].playlist.c_str());
1126 }
1127
1128
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 LogDebug(VB_SCHEDULE, "//////////////////////////////////////////////////\n");
1129 1964 }
1130
1131 831910 ScheduledItem* Scheduler::GetNextScheduledPlaylist() {
1132 831910 std::time_t now = time(nullptr);
1133
1/2
✓ Branch 0 taken 831910 times.
✗ Branch 1 not taken.
831910 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1134
1135
2/2
✓ Branch 0 taken 43901 times.
✓ Branch 1 taken 830348 times.
874249 for (auto& itemTime : m_scheduledItems) {
1136
2/2
✓ Branch 0 taken 44186 times.
✓ Branch 1 taken 42339 times.
86525 for (auto& item : itemTime.second) {
1137
2/2
✓ Branch 0 taken 31904 times.
✓ Branch 1 taken 12282 times.
44186 if (item.ran)
1138 31904 continue; // skip over any items that ran already
1139
1140
2/2
✓ Branch 0 taken 1562 times.
✓ Branch 1 taken 10720 times.
12282 if (item.isPlaylist)
1141 1562 return &item;
1142 }
1143 }
1144
1145 830348 return nullptr;
1146 831910 }
1147
1148 356759 std::string Scheduler::GetNextPlaylistName() {
1149
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 356743 times.
356759 if (m_schedulerDisabled)
1150
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
32 return "Scheduler is disabled.";
1151
1152
1/2
✓ Branch 0 taken 356743 times.
✗ Branch 1 not taken.
356743 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1153
1/2
✓ Branch 0 taken 356743 times.
✗ Branch 1 not taken.
356743 ScheduledItem* item = GetNextScheduledPlaylist();
1154
1155
2/2
✓ Branch 0 taken 356119 times.
✓ Branch 1 taken 624 times.
356743 if (!item)
1156
1/2
✓ Branch 0 taken 356119 times.
✗ Branch 1 not taken.
712238 return "No playlist scheduled.";
1157
1158
1/2
✓ Branch 0 taken 624 times.
✗ Branch 1 not taken.
624 return item->entry->playlist;
1159 356743 }
1160
1161 356759 std::string Scheduler::GetNextPlaylistStartStr() {
1162
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 356743 times.
356759 if (m_schedulerDisabled)
1163
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
32 return "";
1164
1165
4/8
✓ Branch 0 taken 356743 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 356743 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 356743 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 356743 times.
✗ Branch 7 not taken.
356743 std::string timeFmt = getSetting("DateFormat") + " @ " + getSetting("TimeFormat");
1166 356743 std::string result;
1167
1/2
✓ Branch 0 taken 356743 times.
✗ Branch 1 not taken.
356743 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1168
1/2
✓ Branch 0 taken 356743 times.
✗ Branch 1 not taken.
356743 ScheduledItem* item = GetNextScheduledPlaylist();
1169
1170
2/2
✓ Branch 0 taken 356119 times.
✓ Branch 1 taken 624 times.
356743 if (!item)
1171 356119 return result;
1172
1173 char timeStr[32];
1174 624 std::time_t timeT = 0;
1175 struct tm timeStruct;
1176 624 time_t currTime = time(NULL);
1177
1178 624 localtime_r(&item->startTime, &timeStruct);
1179 624 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1180
1181 // Thursday @ 11:00:00 - (Everyday)
1182
1/2
✓ Branch 0 taken 624 times.
✗ Branch 1 not taken.
624 result = timeStr;
1183
1/2
✓ Branch 0 taken 624 times.
✗ Branch 1 not taken.
624 result += " - (";
1184
2/4
✓ Branch 0 taken 624 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 624 times.
✗ Branch 3 not taken.
624 result += GetDayTextFromDayIndex(item->entry->dayIndex);
1185
1/2
✓ Branch 0 taken 624 times.
✗ Branch 1 not taken.
624 result += ")";
1186
1187 624 return result;
1188 356743 }
1189
1190 36399 std::string Scheduler::GetDayTextFromDayIndex(const int index) {
1191
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 36390 times.
36399 if (index & INX_DAY_MASK) {
1192 char tmpStr[15];
1193 9 strcpy(tmpStr, "Mask: ");
1194
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 strcat(tmpStr, (index & INX_DAY_MASK_SUNDAY) ? "S" : "-");
1195
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 strcat(tmpStr, (index & INX_DAY_MASK_MONDAY) ? "M" : "-");
1196
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 strcat(tmpStr, (index & INX_DAY_MASK_TUESDAY) ? "T" : "-");
1197
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 strcat(tmpStr, (index & INX_DAY_MASK_WEDNESDAY) ? "W" : "-");
1198
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 strcat(tmpStr, (index & INX_DAY_MASK_THURSDAY) ? "T" : "-");
1199
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 strcat(tmpStr, (index & INX_DAY_MASK_FRIDAY) ? "F" : "-");
1200
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 strcat(tmpStr, (index & INX_DAY_MASK_SATURDAY) ? "S" : "-");
1201
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return tmpStr;
1202 }
1203
1204
13/17
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 69 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✓ Branch 7 taken 36225 times.
✓ Branch 8 taken 9 times.
✓ Branch 9 taken 9 times.
✓ Branch 10 taken 9 times.
✓ Branch 11 taken 9 times.
✓ Branch 12 taken 9 times.
✓ Branch 13 taken 9 times.
✓ Branch 14 taken 13 times.
✓ Branch 15 taken 9 times.
✗ Branch 16 not taken.
36390 switch (index) {
1205 4 case INX_SUN:
1206
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
8 return "Sunday";
1207 69 case INX_MON:
1208
1/2
✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
138 return "Monday";
1209 ✗ case INX_TUE:
1210 ✗ return "Tuesday";
1211 9 case INX_WED:
1212
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return "Wednesday";
1213 ✗ case INX_THU:
1214 ✗ return "Thursday";
1215 ✗ case INX_FRI:
1216 ✗ return "Friday";
1217 7 case INX_SAT:
1218
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
14 return "Saturday";
1219 36225 case INX_EVERYDAY:
1220
1/2
✓ Branch 0 taken 36225 times.
✗ Branch 1 not taken.
72450 return "Everyday";
1221 9 case INX_WKDAYS:
1222
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return "Weekdays";
1223 9 case INX_WKEND:
1224
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return "Weekends";
1225 9 case INX_M_W_F:
1226
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return "Mon/Wed/Fri";
1227 9 case INX_T_TH:
1228
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return "Tues-Thurs";
1229 9 case INX_SUN_TO_THURS:
1230
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return "Sun-Thurs";
1231 9 case INX_FRI_SAT:
1232
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return "Fri/Sat";
1233 13 case INX_ODD_DAY:
1234
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
26 return "Odd Days";
1235 9 case INX_EVEN_DAY:
1236
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
18 return "Even Days";
1237 }
1238
1239 ✗ return "Error";
1240 }
1241
1242 118422 Json::Value Scheduler::GetInfo(void) {
1243
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 Json::Value result;
1244
1245
4/8
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 118422 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 118422 times.
✗ Branch 7 not taken.
118422 std::string timeFmt = getSetting("DateFormat") + " @ " + getSetting("TimeFormat");
1246
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1247
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 Json::Value np;
1248
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 ScheduledItem* nextItem = GetNextScheduledPlaylist();
1249
3/6
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 118422 times.
✗ Branch 5 not taken.
118422 np["playlistName"] = GetNextPlaylistName();
1250
4/6
✓ Branch 0 taken 312 times.
✓ Branch 1 taken 118110 times.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 118422 times.
✗ Branch 5 not taken.
118422 np["scheduledStartTime"] = nextItem ? (Json::UInt64)(nextItem->startTime) : 0;
1251
3/6
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 118422 times.
✗ Branch 5 not taken.
118422 np["scheduledStartTimeStr"] = GetNextPlaylistStartStr();
1252
1253
4/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 118414 times.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 118422 times.
✗ Branch 5 not taken.
118422 result["enabled"] = m_schedulerDisabled ? 0 : 1;
1254
2/4
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
118422 result["nextPlaylist"] = np;
1255
1256
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 if ((Player::INSTANCE.GetStatus() == FPP_STATUS_PLAYLIST_PLAYING) ||
1257
7/8
✓ Branch 0 taken 43081 times.
✓ Branch 1 taken 75341 times.
✓ Branch 2 taken 43081 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 42943 times.
✓ Branch 5 taken 138 times.
✓ Branch 6 taken 75520 times.
✓ Branch 7 taken 42902 times.
161365 (Player::INSTANCE.GetStatus() == FPP_STATUS_STOPPING_GRACEFULLY) ||
1258
3/4
✓ Branch 0 taken 42943 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 42902 times.
42943 (Player::INSTANCE.GetStatus() == FPP_STATUS_STOPPING_GRACEFULLY_AFTER_LOOP)) {
1259
1/2
✓ Branch 0 taken 75520 times.
✗ Branch 1 not taken.
75520 Json::Value cp;
1260
1261
3/4
✓ Branch 0 taken 75520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✓ Branch 3 taken 73069 times.
75520 if (Player::INSTANCE.WasScheduled()) {
1262 char timeStr[32];
1263 2451 std::time_t timeT = 0;
1264 struct tm timeStruct;
1265 2451 time_t currTime = time(NULL);
1266
1267 2451 localtime_r(&currTime, &timeStruct);
1268 2451 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1269
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["currentTime"] = (Json::UInt64)currTime;
1270
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["currentTimeStr"] = timeStr;
1271
1272
1/2
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
2451 timeT = Player::INSTANCE.GetOrigStartTime();
1273 2451 localtime_r(&timeT, &timeStruct);
1274 2451 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1275
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["scheduledStartTime"] = (Json::UInt64)timeT;
1276
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["scheduledStartTimeStr"] = timeStr;
1277
1278
1/2
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
2451 timeT = Player::INSTANCE.GetStartTime();
1279 2451 localtime_r(&timeT, &timeStruct);
1280 2451 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1281
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["actualStartTime"] = (Json::UInt64)timeT;
1282
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["actualStartTimeStr"] = timeStr;
1283
1284
1/2
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
2451 timeT = Player::INSTANCE.GetOrigStopTime();
1285 2451 localtime_r(&timeT, &timeStruct);
1286 2451 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1287
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["scheduledEndTime"] = (Json::UInt64)timeT;
1288
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["scheduledEndTimeStr"] = timeStr;
1289
1290
1/2
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
2451 timeT = Player::INSTANCE.GetStopTime();
1291 2451 localtime_r(&timeT, &timeStruct);
1292 2451 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1293
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["actualEndTime"] = (Json::UInt64)timeT;
1294
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["actualEndTimeStr"] = timeStr;
1295
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["secondsRemaining"] = (int)(timeT - currTime);
1296
1297
1/2
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
2451 int stopType = Player::INSTANCE.GetStopMethod();
1298
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 cp["stopType"] = stopType;
1299
3/4
✓ Branch 0 taken 138 times.
✓ Branch 1 taken 2312 times.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
4901 cp["stopTypeStr"] = stopType == 2 ? "Graceful Loop" : stopType == 1 ? "Hard"
1300
3/4
✓ Branch 0 taken 2450 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
7352 : "Graceful";
1301
1302
2/4
✓ Branch 0 taken 2451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2451 times.
✗ Branch 3 not taken.
2451 result["status"] = "playing";
1303 } else {
1304
2/4
✓ Branch 0 taken 73069 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 73069 times.
✗ Branch 3 not taken.
73069 result["status"] = "manual";
1305 }
1306
1307
3/6
✓ Branch 0 taken 75520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75520 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 75520 times.
✗ Branch 5 not taken.
75520 cp["playlistName"] = Player::INSTANCE.GetPlaylistName();
1308
1309
2/4
✓ Branch 0 taken 75520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75520 times.
✗ Branch 3 not taken.
75520 result["currentPlaylist"] = cp;
1310 75520 } else {
1311
2/4
✓ Branch 0 taken 42902 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42902 times.
✗ Branch 3 not taken.
42902 result["status"] = "idle";
1312 }
1313
1314 // Add blocked schedule information
1315
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 118412 times.
118422 if (m_lastBlockedTime > 0) {
1316 10 time_t now = time(NULL);
1317 // Only report if blocked within last 5 minutes
1318
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if ((now - m_lastBlockedTime) < 300) {
1319
3/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
10 result["blockedSchedule"]["playlistName"] = m_lastBlockedPlaylist;
1320
3/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
10 result["blockedSchedule"]["blockedTime"] = (Json::UInt64)m_lastBlockedTime;
1321 }
1322 }
1323
1324 118422 return result;
1325 118422 }
1326
1327 36327 Json::Value Scheduler::GetSchedule() {
1328
4/8
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36327 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36327 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36327 times.
✗ Branch 7 not taken.
36327 std::string timeFmt = getSetting("DateFormat") + " @ " + getSetting("TimeFormat");
1329
1/2
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
36327 Json::Value result;
1330
1/2
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
36327 Json::Value entries(Json::arrayValue);
1331
1/2
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
36327 Json::Value entry;
1332
1/2
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
36327 Json::Value items(Json::arrayValue);
1333
1/2
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
36327 Json::Value scheduledItem;
1334 36327 std::time_t now = time(nullptr);
1335
1/2
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
36327 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1336
1337
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 36327 times.
✓ Branch 2 taken 36327 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36327 times.
✗ Branch 5 not taken.
36327 result["enabled"] = m_schedulerDisabled ? 0 : 1;
1338
1339
2/2
✓ Branch 0 taken 35315 times.
✓ Branch 1 taken 36327 times.
71642 for (int i = 0; i < m_Schedule.size(); i++) {
1340
1/2
✓ Branch 0 taken 35315 times.
✗ Branch 1 not taken.
35315 entry = m_Schedule[i].GetJson();
1341
2/4
✓ Branch 0 taken 35315 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35315 times.
✗ Branch 3 not taken.
35315 entry["id"] = i;
1342
4/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 35311 times.
✓ Branch 2 taken 35315 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35315 times.
✗ Branch 5 not taken.
35315 entry["type"] = m_Schedule[i].playlist.empty() ? "command" : "playlist";
1343
3/6
✓ Branch 0 taken 35315 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35315 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35315 times.
✗ Branch 5 not taken.
35315 entry["dayStr"] = GetDayTextFromDayIndex(m_Schedule[i].dayIndex);
1344
1345
1/2
✓ Branch 0 taken 35315 times.
✗ Branch 1 not taken.
35315 entries.append(entry);
1346 }
1347
2/4
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36327 times.
✗ Branch 3 not taken.
36327 result["entries"] = entries;
1348
1349 struct tm timeStruct;
1350 char timeStr[32];
1351 time_t tmpTime;
1352
1353
5/6
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4552 times.
✓ Branch 3 taken 31775 times.
✓ Branch 4 taken 2128 times.
✓ Branch 5 taken 34199 times.
40879 if ((Player::INSTANCE.GetStatus() != FPP_STATUS_IDLE) &&
1354
3/4
✓ Branch 0 taken 4552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✓ Branch 3 taken 2424 times.
4552 (Player::INSTANCE.WasScheduled())) {
1355 // If we are playing a scheduled playlist, then insert an item for
1356 // it since it is already marked as ran and will get skipped by.
1357 // We do this here instead of just using a more complicated if continue
1358 // statement below because the player may have more up to date info
1359 // on the end time than our old schedule entry if the end time has
1360 // been extended or shortened.
1361
3/6
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2128 times.
✗ Branch 5 not taken.
2128 scheduledItem["priority"] = Player::INSTANCE.GetPriority();
1362
3/6
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2128 times.
✗ Branch 5 not taken.
2128 scheduledItem["id"] = Player::INSTANCE.GetScheduleEntry();
1363
1364
1/2
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
2128 tmpTime = Player::INSTANCE.GetOrigStartTime();
1365 2128 localtime_r(&tmpTime, &timeStruct);
1366 2128 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1367
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["startTimeStr"] = timeStr;
1368
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["startTime"] = (Json::UInt64)tmpTime;
1369
1370 2128 FormatTime(timeStr, sizeof(timeStr), "%Y%m%d", &timeStruct);
1371
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["startDateStr"] = timeStr;
1372
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["startDateInt"] = atoi(timeStr);
1373
1374
1/2
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
2128 tmpTime = Player::INSTANCE.GetStopTime();
1375 2128 localtime_r(&tmpTime, &timeStruct);
1376 2128 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1377
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["endTimeStr"] = timeStr;
1378
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["endTime"] = (Json::UInt64)tmpTime;
1379
1380 2128 FormatTime(timeStr, sizeof(timeStr), "%Y%m%d", &timeStruct);
1381
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["endDateStr"] = timeStr;
1382
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["endDateInt"] = atoi(timeStr);
1383
1384
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["command"] = "Start Playlist";
1385
1386
1/2
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
2128 Json::Value args(Json::arrayValue);
1387
3/6
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2128 times.
✗ Branch 5 not taken.
2128 args.append(Player::INSTANCE.GetPlaylistName());
1388
2/4
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2128 times.
✗ Branch 3 not taken.
2128 scheduledItem["args"] = args;
1389
1390
1/2
✓ Branch 0 taken 2128 times.
✗ Branch 1 not taken.
2128 items.append(scheduledItem);
1391 2128 }
1392
1393
2/2
✓ Branch 0 taken 37881 times.
✓ Branch 1 taken 36327 times.
74208 for (auto& itemTime : m_scheduledItems) {
1394
2/2
✓ Branch 0 taken 37949 times.
✓ Branch 1 taken 37881 times.
75830 for (auto& item : itemTime.second) {
1395
2/2
✓ Branch 0 taken 5675 times.
✓ Branch 1 taken 32274 times.
37949 if (item.ran)
1396 5675 continue;
1397
1398
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["priority"] = item.priority;
1399
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["id"] = item.entryIndex;
1400
1401 32274 localtime_r(&item.startTime, &timeStruct);
1402 32274 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1403
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["startTimeStr"] = timeStr;
1404
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["startTime"] = (Json::UInt64)item.startTime;
1405
1406 32274 FormatTime(timeStr, sizeof(timeStr), "%Y%m%d", &timeStruct);
1407
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["startDateStr"] = timeStr;
1408
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["startDateInt"] = atoi(timeStr);
1409
1410 32274 localtime_r(&item.endTime, &timeStruct);
1411 32274 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1412
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["endTimeStr"] = timeStr;
1413
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["endTime"] = (Json::UInt64)item.endTime;
1414
1415 32274 FormatTime(timeStr, sizeof(timeStr), "%Y%m%d", &timeStruct);
1416
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["endDateStr"] = timeStr;
1417
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["endDateInt"] = atoi(timeStr);
1418
1419
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["command"] = item.command;
1420
1421
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["args"] = item.args;
1422
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["multisyncCommand"] = item.entry->multisyncCommand;
1423
2/4
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32274 times.
✗ Branch 3 not taken.
32274 scheduledItem["multisyncHosts"] = item.entry->multisyncHosts;
1424
1425
1/2
✓ Branch 0 taken 32274 times.
✗ Branch 1 not taken.
32274 items.append(scheduledItem);
1426 }
1427 }
1428
2/4
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36327 times.
✗ Branch 3 not taken.
36327 result["items"] = items;
1429
2/4
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36327 times.
✗ Branch 3 not taken.
36327 result["schedulesExtendBeyondDistance"] = m_schedulesExtendBeyondDistance;
1430
3/6
✓ Branch 0 taken 36327 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36327 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36327 times.
✗ Branch 5 not taken.
36327 result["scheduleDistance"] = getSettingInt("ScheduleDistance");
1431
1432 36327 return result;
1433 36327 }
1434
1435 /*
1436 * Expand the configured schedule across an arbitrary date range.
1437 *
1438 * GetSchedule() can only report the rolling window fppd actually schedules out
1439 * (ScheduleDistance days, minus anything already run), which is enough for the
1440 * list preview but not for a calendar the user can page through. This walks
1441 * the range a day at a time and asks each entry which occurrences land on it,
1442 * so any month - past or future - can be rendered from the real schedule rules.
1443 *
1444 * Occurrences are advisory: they describe what the schedule says should happen,
1445 * not what fppd has committed to running.
1446 */
1447 190 Json::Value Scheduler::BuildScheduleRangeItem(const ScheduleEntry& entry,
1448 const Occurrence& occurrence,
1449 const std::string& timeFmt) {
1450 struct tm timeStruct;
1451 char timeStr[32];
1452
1/2
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
190 Json::Value item;
1453
1454
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["id"] = occurrence.entryIndex;
1455
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["priority"] = occurrence.entryIndex; // priority is entry index for now
1456
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["enabled"] = entry.enabled;
1457
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["overridden"] = occurrence.overridden;
1458
5/8
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 152 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 152 times.
✓ Branch 4 taken 190 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 190 times.
✗ Branch 7 not taken.
190 item["type"] = entry.playlist.empty() ? "command" : (entry.sequence ? "sequence" : "playlist");
1459
1460
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["startTime"] = (Json::UInt64)occurrence.start;
1461
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["endTime"] = (Json::UInt64)occurrence.end;
1462
1463 190 localtime_r(&occurrence.start, &timeStruct);
1464 190 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1465
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["startTimeStr"] = timeStr;
1466 190 FormatTime(timeStr, sizeof(timeStr), "%Y%m%d", &timeStruct);
1467
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["startDateStr"] = timeStr;
1468
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["startDateInt"] = atoi(timeStr);
1469
1470 190 localtime_r(&occurrence.end, &timeStruct);
1471 190 FormatTime(timeStr, sizeof(timeStr), timeFmt.c_str(), &timeStruct);
1472
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["endTimeStr"] = timeStr;
1473 190 FormatTime(timeStr, sizeof(timeStr), "%Y%m%d", &timeStruct);
1474
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["endDateStr"] = timeStr;
1475
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["endDateInt"] = atoi(timeStr);
1476
1477
2/2
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 152 times.
190 if (entry.playlist.empty()) {
1478
2/4
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 38 times.
✗ Branch 3 not taken.
38 item["command"] = entry.command;
1479
2/4
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 38 times.
✗ Branch 3 not taken.
38 item["args"] = entry.args;
1480 } else {
1481
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 Json::Value args(Json::arrayValue);
1482
2/4
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 152 times.
✗ Branch 3 not taken.
152 args.append(entry.playlist);
1483
4/6
✓ Branch 0 taken 78 times.
✓ Branch 1 taken 74 times.
✓ Branch 2 taken 152 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 152 times.
✗ Branch 5 not taken.
152 args.append(entry.repeat ? "true" : "false");
1484
2/4
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 152 times.
✗ Branch 3 not taken.
152 args.append("false");
1485
1486
2/4
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 152 times.
✗ Branch 3 not taken.
152 item["command"] = "Start Playlist";
1487
2/4
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 152 times.
✗ Branch 3 not taken.
152 item["args"] = args;
1488
2/4
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 152 times.
✗ Branch 3 not taken.
152 item["playlist"] = entry.playlist;
1489 152 }
1490
1491
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["multisyncCommand"] = entry.multisyncCommand;
1492
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["multisyncHosts"] = entry.multisyncHosts;
1493
3/8
✗ Branch 0 not taken.
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 190 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 190 times.
✗ Branch 7 not taken.
190 item["stopTypeStr"] = entry.stopType == 0 ? "Graceful" : (entry.stopType == 1 ? "Hard" : "Graceful Loop");
1494
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["repeat"] = entry.repeat;
1495
2/4
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 item["repeatInterval"] = entry.repeatInterval;
1496
1497 190 return item;
1498 ✗ }
1499
1500 18 Json::Value Scheduler::GetScheduleRange(time_t rangeStart, time_t rangeEnd,
1501 bool includeDisabled, bool daySummary) {
1502
4/8
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 18 times.
✗ Branch 7 not taken.
18 std::string timeFmt = getSetting("DateFormat") + " @ " + getSetting("TimeFormat");
1503
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 Json::Value result;
1504
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 Json::Value entries(Json::arrayValue);
1505
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 Json::Value items(Json::arrayValue);
1506 18 std::vector<Occurrence> expanded;
1507 18 std::vector<Occurrence> playlistWindows;
1508
1509 // The walk below steps a day at a time from rangeStart; a start whose
1510 // year doesn't fit in a struct tm makes localtime_r fail and leaves the
1511 // cursor at -1, i.e. trillions of steps. Keep both ends in 1970..9999
1512 // here rather than trusting every caller to.
1513 static constexpr time_t MAX_RANGE_TIME = 253402300799; // 9999-12-31 23:59:59 UTC
1514
4/6
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 16 times.
18 if (rangeStart < 0 || rangeEnd < rangeStart || rangeEnd > MAX_RANGE_TIME) {
1515
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 result["entries"] = entries;
1516
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 result["items"] = items;
1517 2 return result;
1518 }
1519
1520 // Work from a copy of the schedule so the expansion below never holds the
1521 // scheduler lock. ScheduleProc() takes the same lock to start and stop
1522 // playlists, and expanding a large range takes long enough that holding it
1523 // throughout would delay a show. Copying a handful of entries is cheap.
1524 16 std::vector<ScheduleEntry> schedule;
1525 {
1526
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1527
1528
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
16 result["enabled"] = m_schedulerDisabled ? 0 : 1;
1529
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 schedule = m_Schedule;
1530 16 }
1531
1532
2/2
✓ Branch 0 taken 116 times.
✓ Branch 1 taken 16 times.
132 for (int i = 0; i < schedule.size(); i++) {
1533
1/2
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
116 Json::Value entry = schedule[i].GetJson();
1534
2/4
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
116 entry["id"] = i;
1535
4/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 112 times.
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 116 times.
✗ Branch 5 not taken.
116 entry["type"] = schedule[i].playlist.empty() ? "command" : "playlist";
1536
3/6
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 116 times.
✗ Branch 5 not taken.
116 entry["dayStr"] = GetDayTextFromDayIndex(schedule[i].dayIndex);
1537
1/2
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
116 entries.append(entry);
1538 116 }
1539
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 result["entries"] = entries;
1540
1541 struct tm timeStruct;
1542 char timeStr[32];
1543
1544 // An entry starting late in the day can run past midnight, so start a day
1545 // early to catch occurrences that overlap into the requested range. Walk
1546 // the range from midday to midday: a 23 or 25 hour DST day would otherwise
1547 // let a fixed 86400s step skip or repeat a calendar date.
1548
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (!localtime_r(&rangeStart, &timeStruct)) {
1549 ✗ result["items"] = items;
1550 ✗ return result;
1551 }
1552 16 timeStruct.tm_hour = 12;
1553 16 timeStruct.tm_min = 0;
1554 16 timeStruct.tm_sec = 0;
1555 16 timeStruct.tm_isdst = -1;
1556 16 timeStruct.tm_mday -= 1;
1557 16 time_t cursor = mktime(&timeStruct);
1558
1559 // This runs on an API thread for whatever range the calendar asks for;
1560 // what the expansion finds wrong (a year past a lunar table) belongs in
1561 // this response, not in the scheduler's load warnings.
1562
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 SchedulePreviewScope previewScope;
1563
2/2
✓ Branch 0 taken 2362 times.
✓ Branch 1 taken 16 times.
2378 for (; cursor <= (rangeEnd + SECONDS_PER_DAY); cursor += SECONDS_PER_DAY) {
1564 2362 localtime_r(&cursor, &timeStruct);
1565 2362 int year = timeStruct.tm_year + 1900;
1566 2362 int month = timeStruct.tm_mon + 1;
1567 2362 int mday = timeStruct.tm_mday;
1568
1569
2/2
✓ Branch 0 taken 23868 times.
✓ Branch 1 taken 2362 times.
26230 for (int i = 0; i < schedule.size(); i++) {
1570 23868 ScheduleEntry& entry = schedule[i];
1571
1572
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 23868 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
23868 if (!entry.enabled && !includeDisabled)
1573 ✗ continue;
1574
1575 23868 std::vector<std::pair<time_t, time_t>> occurrences;
1576
1/2
✓ Branch 0 taken 23868 times.
✗ Branch 1 not taken.
23868 entry.GetOccurrencesOnDate(year, month, mday, occurrences);
1577
1578 23868 int dayKey = (year * 10000) + (month * 100) + mday;
1579
1580
2/2
✓ Branch 0 taken 294 times.
✓ Branch 1 taken 23868 times.
24162 for (auto& occurrence : occurrences) {
1581 // A command fires at a point in time - only playlists occupy the
1582 // window up to their end time. AddScheduledItems() makes the
1583 // same distinction, so match it or the calendar would draw a
1584 // repeating command as a block instead of a marker.
1585
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 227 times.
294 if (entry.playlist.empty())
1586 67 occurrence.second = occurrence.first;
1587
1588 // A playlist's end is exclusive, as everywhere else: one that
1589 // ends at the start of the range (an entry ending at midnight)
1590 // belongs to the previous day, not this range.
1591 294 bool span = occurrence.second > occurrence.first;
1592 294 Occurrence o{ .entryIndex = i,
1593 .dayKey = dayKey,
1594 294 .start = occurrence.first,
1595 294 .end = occurrence.second,
1596 294 .overridden = false };
1597
1598 // Only playlists can override, and a schedule of repeating
1599 // commands can run to tens of thousands of occurrences, so
1600 // collect the playlist windows once rather than rescanning
1601 // everything for every occurrence. Collect them before the
1602 // range test: one from before the range (the day-early walk
1603 // finds it) can still override an occurrence inside it.
1604
2/2
✓ Branch 0 taken 227 times.
✓ Branch 1 taken 67 times.
294 if (!entry.playlist.empty())
1605
1/2
✓ Branch 0 taken 227 times.
✗ Branch 1 not taken.
227 playlistWindows.push_back(o);
1606
1607
6/6
✓ Branch 0 taken 227 times.
✓ Branch 1 taken 67 times.
✓ Branch 2 taken 192 times.
✓ Branch 3 taken 35 times.
✓ Branch 4 taken 54 times.
✓ Branch 5 taken 13 times.
294 if ((span ? (occurrence.second <= rangeStart) : (occurrence.second < rangeStart)) ||
1608
2/2
✓ Branch 0 taken 53 times.
✓ Branch 1 taken 193 times.
246 (occurrence.first > rangeEnd))
1609 101 continue;
1610
1611
1/2
✓ Branch 0 taken 193 times.
✗ Branch 1 not taken.
193 expanded.push_back(o);
1612 }
1613 23868 }
1614 }
1615
1616 // Flag playlist occurrences the scheduler would skip because a
1617 // higher-priority (lower index) playlist is already running at that point.
1618
1619
2/2
✓ Branch 0 taken 193 times.
✓ Branch 1 taken 16 times.
209 for (auto& occurrence : expanded) {
1620
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 152 times.
193 if (schedule[occurrence.entryIndex].playlist.empty())
1621 41 continue;
1622
1623
2/2
✓ Branch 0 taken 3830 times.
✓ Branch 1 taken 143 times.
3973 for (const auto& window : playlistWindows) {
1624
2/2
✓ Branch 0 taken 2174 times.
✓ Branch 1 taken 1656 times.
3830 if (window.entryIndex >= occurrence.entryIndex)
1625 2174 continue;
1626
1627
4/4
✓ Branch 0 taken 862 times.
✓ Branch 1 taken 794 times.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 853 times.
1656 if ((window.start <= occurrence.start) && (window.end > occurrence.start)) {
1628 9 occurrence.overridden = true;
1629 9 break;
1630 }
1631 }
1632 }
1633
1634
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 15 times.
16 if (daySummary) {
1635 // Coarse views cannot show hundreds of occurrences per cell and do not
1636 // need to: what matters there is which entries are active on a day.
1637 // Collapse to one item per entry per day and carry the occurrence count
1638 // so the caller can label it; the detail views ask for the full list.
1639 1 std::map<std::pair<int, int>, Occurrence> byDay;
1640 1 std::map<std::pair<int, int>, int> counts;
1641
1642
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 times.
13 for (const auto& occurrence : expanded) {
1643 12 auto key = std::make_pair(occurrence.dayKey, occurrence.entryIndex);
1644
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 counts[key]++;
1645
1646
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 auto it = byDay.find(key);
1647
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 3 times.
12 if (it == byDay.end()) {
1648
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 byDay[key] = occurrence;
1649 } else {
1650 // Span the whole day's activity for this entry
1651 3 it->second.start = std::min(it->second.start, occurrence.start);
1652 3 it->second.end = std::max(it->second.end, occurrence.end);
1653
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 it->second.overridden = it->second.overridden && occurrence.overridden;
1654 }
1655 }
1656
1657
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1 times.
10 for (const auto& [key, occurrence] : byDay) {
1658 9 Json::Value item = BuildScheduleRangeItem(schedule[occurrence.entryIndex],
1659
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 occurrence, timeFmt);
1660
3/6
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
9 item["count"] = counts[key];
1661
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 item["summary"] = true;
1662
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 items.append(item);
1663 9 }
1664 1 } else {
1665
2/2
✓ Branch 0 taken 181 times.
✓ Branch 1 taken 15 times.
196 for (const auto& occurrence : expanded) {
1666 181 Json::Value item = BuildScheduleRangeItem(schedule[occurrence.entryIndex],
1667
1/2
✓ Branch 0 taken 181 times.
✗ Branch 1 not taken.
181 occurrence, timeFmt);
1668
2/4
✓ Branch 0 taken 181 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 181 times.
✗ Branch 3 not taken.
181 item["count"] = 1;
1669
2/4
✓ Branch 0 taken 181 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 181 times.
✗ Branch 3 not taken.
181 item["summary"] = false;
1670
1/2
✓ Branch 0 taken 181 times.
✗ Branch 1 not taken.
181 items.append(item);
1671 181 }
1672 }
1673
1674
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 result["items"] = items;
1675
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (!previewScope.warnings().empty()) {
1676 ✗ Json::Value warnings(Json::arrayValue);
1677 ✗ for (const auto& w : previewScope.warnings())
1678 ✗ warnings.append(w.second);
1679 ✗ result["warnings"] = warnings;
1680 ✗ }
1681
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 result["rangeStart"] = (Json::UInt64)rangeStart;
1682
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 result["rangeEnd"] = (Json::UInt64)rangeEnd;
1683
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 result["daySummary"] = daySummary;
1684
3/6
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
16 result["scheduleDistance"] = getSettingInt("ScheduleDistance");
1685
1686 16 return result;
1687 18 }
1688
1689 1 void Scheduler::SetTimeDelta(int delta, int timeLimit) {
1690 // Under the schedule lock: the load path reads and resets m_timeDelta
1691 // there, and this runs on the command's thread.
1692
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1693 1 m_timeDelta = m_timeDelta + delta;
1694 1 m_timeDeltaThreshold = time(NULL) + timeLimit;
1695 // Trigger a reload the next time Scheduler::ScheduleProc() is called
1696 1 m_loadSchedule = true;
1697 1 }
1698
1699 2 bool Scheduler::StartNextScheduledItemNow() {
1700 // Decide under the lock and copy everything the player calls need, then
1701 // release it before calling the player. See Scheduler.h for why.
1702 2 std::vector<PlayerAction> actions;
1703
1704 {
1705
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::unique_lock<std::recursive_mutex> lock(m_scheduleLock);
1706
1707
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 ScheduledItem* item = GetNextScheduledPlaylist();
1708
1709
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (!item)
1710 ✗ return false;
1711
1712
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 if (Player::INSTANCE.GetStatus() != FPP_STATUS_IDLE) {
1713
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_SCHEDULE, "Force-Stopping running playlist\n");
1714 1 actions.push_back({ .type = PlayerAction::Type::StopNow,
1715 .forceStop = 1 });
1716 }
1717
1718
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogDebug(VB_SCHEDULE, "Force-Starting next scheduled playlist now:\n");
1719
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 DumpScheduledItem(item->startTime, *item);
1720
1721 2 actions.push_back({ .type = PlayerAction::Type::StartPlaylist,
1722 2 .playlist = item->entry->playlist,
1723 .position = 0,
1724 2 .repeat = item->entry->repeat,
1725 2 .entryIndex = item->entryIndex,
1726 2 .priority = item->entryIndex, // priority is entry index for now
1727 2 .startTime = time(NULL),
1728 2 .endTime = item->endTime,
1729 2 .stopType = item->entry->stopType });
1730
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 }
1731
1732
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 RunPlayerActions(actions);
1733
1734 2 return true;
1735
3/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
5 }
1736
1737 class ScheduleCommand : public Command {
1738 public:
1739 3776 ScheduleCommand(const std::string& str, Scheduler* s) :
1740 Command(str),
1741 3776 sched(s) {}
1742 7468 virtual ~ScheduleCommand() {}
1743
1744 Scheduler* sched;
1745 };
1746
1747 class StartNextItemCommand : public ScheduleCommand {
1748 public:
1749 1888 StartNextItemCommand(Scheduler* s) :
1750
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
3776 ScheduleCommand("Start Next Scheduled Item", s) {
1751 1888 }
1752
1753 2 virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
1754
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (sched->StartNextScheduledItemNow())
1755 2 return std::make_unique<Command::Result>("Next scheduled item started");
1756 else
1757 ✗ return std::make_unique<Command::Result>("Unable to start next scheduled item");
1758 }
1759 };
1760
1761 // Command arguments arrive as strings after %VAR% replacement, so an unset
1762 // variable hands us "" and a typo hands us anything at all. std::stoi() throws
1763 // on both, and these commands run on a detached scheduler thread.
1764 10 static bool ParseIntArg(const std::string& str, int& out) {
1765
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 9 times.
10 if (str.empty()) {
1766 1 return false;
1767 }
1768
1769 9 const char* start = str.c_str();
1770 9 char* end = nullptr;
1771 9 errno = 0;
1772 9 long v = strtol(start, &end, 10);
1773
1774 // "Consumed nothing" has to be decided BEFORE the trailing-whitespace skip
1775 // below moves `end`: strtol() skips leading whitespace itself, so an
1776 // all-whitespace argument leaves end == start with v == 0, and skipping
1777 // first walks end to the terminator and makes the test pass -- reporting a
1778 // successful parse of 0 for an argument that held no number at all.
1779
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 7 times.
9 if (end == start) {
1780 2 return false;
1781 }
1782
1783
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
7 while ((*end == ' ') || (*end == '\t')) {
1784 ✗ end++;
1785 }
1786
1787
4/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
7 if ((*end != '\0') || (errno == ERANGE) ||
1788
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 (v < INT_MIN) || (v > INT_MAX)) {
1789 1 return false;
1790 }
1791
1792 6 out = (int)v;
1793 6 return true;
1794 }
1795
1796 class ExtendScheduleCommand : public ScheduleCommand {
1797 public:
1798 1888 ExtendScheduleCommand(Scheduler* s) :
1799
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
3776 ScheduleCommand("Extend Schedule", s) {
1800
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("Seconds", "int", "Extend for Seconds").setRange(-12 * 60 * 60, 12 * 60 * 60).setDefaultValue("300").setAdjustable());
1801
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("Limit", "int", "For schedule events occurring in the next x Seconds").setRange(-12 * 60 * 60, 12 * 60 * 60).setDefaultValue("0").setAdjustable());
1802 1888 }
1803
1804 7 virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
1805
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (args.size() < 1) {
1806 ✗ return std::make_unique<Command::ErrorResult>("Command needs at least 1 argument, found " + std::to_string(args.size()));
1807 }
1808 7 int seconds = 0;
1809
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
7 if (!ParseIntArg(args[0], seconds)) {
1810
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.
4 return std::make_unique<Command::ErrorResult>("Seconds argument '" + args[0] + "' is not a number");
1811 }
1812
1813 3 int limit = 0;
1814
4/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
3 if ((args.size() > 1) && !args[1].empty() && !ParseIntArg(args[1], limit)) {
1815 ✗ return std::make_unique<Command::ErrorResult>("Limit argument '" + args[1] + "' is not a number");
1816 }
1817
1818
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (Player::INSTANCE.AdjustPlaylistStopTime(seconds)) {
1819
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (limit != 0) {
1820
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 scheduler->SetTimeDelta(seconds, limit);
1821 }
1822
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 return std::make_unique<Command::Result>("Schedule Updated");
1823 }
1824 ✗ return std::make_unique<Command::Result>("Error extending Schedule");
1825 }
1826 };
1827
1828 1888 void Scheduler::RegisterCommands() {
1829
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1728 times.
✓ Branch 7 taken 160 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1728 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 CommandManager::INSTANCE.addCategorizedCommand(new StartNextItemCommand(this), "Playlist", 1);
1830
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1728 times.
✓ Branch 7 taken 160 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1728 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
2208 CommandManager::INSTANCE.addCategorizedCommand(new ExtendScheduleCommand(this), "Playlist", 1);
1831 1888 }
1832