| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-2025 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 | // WebSocket push of the fppd status. | ||
| 14 | // | ||
| 15 | // Historically every open web page polled api/system/status on a timer | ||
| 16 | // (default 5s). That PHP endpoint fetches /fppd/status from fppd and then | ||
| 17 | // augments it with slow-moving host data (wifi, interfaces, advancedView, | ||
| 18 | // plugin indicators, ...). On a BeagleBone that augmentation is ~180ms of | ||
| 19 | // PHP per request; multiplied by every open tab every few seconds it is a | ||
| 20 | // meaningful, continuous load on the exact devices that can least afford it. | ||
| 21 | // | ||
| 22 | // This endpoint splits the fast-moving half off: fppd pushes its own | ||
| 23 | // /fppd/status payload over a WebSocket the instant it changes (and at most | ||
| 24 | // once per second while a playlist's clock ticks), so pages no longer poll | ||
| 25 | // for it at all. The slow host augmentation stays in PHP but can be polled | ||
| 26 | // far less often. fppd already has this data in hand, so producing it costs | ||
| 27 | // ~0.7ms and nothing when nobody is connected. | ||
| 28 | // | ||
| 29 | // Wire format (server -> client): | ||
| 30 | // {"type":"snapshot","data":{"status":{...}}} | ||
| 31 | // `data` carries the keys that changed this round: "status" every second, and | ||
| 32 | // "levels" (audio meters) ten times a second while anything is subscribed to | ||
| 33 | // them. The envelope leaves room to add more later without a protocol change. Each snapshot is complete for the | ||
| 34 | // keys it contains -- there are no deltas to miss, so there is no sequence | ||
| 35 | // number to compare: a client that drops the socket re-syncs by doing one full | ||
| 36 | // status poll when it reconnects, which it must do anyway to pick up the PHP | ||
| 37 | // augmentation. | ||
| 38 | // | ||
| 39 | // A client that only cares about state -- a monitor watching every box on the | ||
| 40 | // network, rather than a page showing this one -- can connect to | ||
| 41 | // /fppdws?quiet=1. An idle fppd's payload still changes every second, but only | ||
| 42 | // in its clock and uptime fields (and sensor readings, which jitter), so a | ||
| 43 | // plain client gets a frame a second from a box where nothing is happening. A | ||
| 44 | // quiet client gets a frame only when something else changed, plus one every | ||
| 45 | // QUIET_KEEPALIVE_MS so the clock fields stay roughly current and the client | ||
| 46 | // can still tell a live feed from a dead one. Playback and warnings count as | ||
| 47 | // changes, so a playing box still sends once a second. | ||
| 48 | // | ||
| 49 | // Client -> server: only "pong" is expected (drogon answers pings itself); | ||
| 50 | // anything else is ignored. The endpoint is read-only — it never accepts | ||
| 51 | // commands — so proxying it to the LAN carries no more authority than the | ||
| 52 | // existing status poll did. | ||
| 53 | |||
| 54 | #include "mediaoutput/AudioLevelMonitor.h" | ||
| 55 | |||
| 56 | #include <drogon/HttpAppFramework.h> | ||
| 57 | #include <drogon/WebSocketController.h> | ||
| 58 | // trantor (drogon's net layer) defines LOG_* macros that collide with FPP's | ||
| 59 | // LogLevel enum; drop them before pulling in the FPP headers, same as httpAPI.cpp. | ||
| 60 | #undef LOG_WARN | ||
| 61 | #undef LOG_INFO | ||
| 62 | #undef LOG_DEBUG | ||
| 63 | |||
| 64 | #include "fpp-pch.h" | ||
| 65 | |||
| 66 | #include "StatusWebSocket.h" | ||
| 67 | #include "Warnings.h" | ||
| 68 | #include "common.h" // GetCurrentFPPDStatus, SaveJsonToString | ||
| 69 | #include "fpp-json.h" | ||
| 70 | |||
| 71 | #include <mutex> | ||
| 72 | #include <set> | ||
| 73 | #include <string> | ||
| 74 | #include <vector> | ||
| 75 | |||
| 76 | using namespace drogon; | ||
| 77 | |||
| 78 | namespace { | ||
| 79 | |||
| 80 | std::mutex g_mutex; | ||
| 81 | std::set<WebSocketConnectionPtr> g_conns; | ||
| 82 | std::set<WebSocketConnectionPtr> g_quietConns; // the subset that asked for ?quiet=1 | ||
| 83 | std::string g_lastStatusJson; // last pushed status payload, for change detection | ||
| 84 | // The quiet clients' view: the payload minus kTickingKeys, and when they were | ||
| 85 | // last sent anything. | ||
| 86 | std::string g_lastQuietJson; | ||
| 87 | uint64_t g_lastQuietSendMs = 0; | ||
| 88 | constexpr uint64_t QUIET_KEEPALIVE_MS = 30000; | ||
| 89 | // Status keys that change on their own with nothing having happened. | ||
| 90 | const char* const kTickingKeys[] = { | ||
| 91 | "time", "timeStr", "timeStrFull", "dateStr", | ||
| 92 | "uptime", "uptimeStr", "uptimeTotalSeconds", "uptimeSeconds", | ||
| 93 | "uptimeMinutes", "uptimeHours", "uptimeDays", | ||
| 94 | "systemUptimeTotalSeconds", "sensors" | ||
| 95 | }; | ||
| 96 | // Set by StatusWebSocketShutdown() under g_mutex. Once set, no producer builds | ||
| 97 | // or sends a payload and no new connection is tracked, so a connection that | ||
| 98 | // arrives while fppd is tearing down can't re-populate g_conns after the clear. | ||
| 99 | bool g_shutdown = false; | ||
| 100 | |||
| 101 | // Build the current /fppd/status payload as a compact JSON string. This is | ||
| 102 | // the identical data the GET /fppd/status handler returns. | ||
| 103 | 206 | std::string buildStatusJson() { | |
| 104 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | Json::Value status; |
| 105 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | GetCurrentFPPDStatus(status); |
| 106 |
2/4✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 206 times.
✗ Branch 3 not taken.
|
412 | return SaveJsonToString(status, ""); // "" indentation == compact |
| 107 | 206 | } | |
| 108 | |||
| 109 | // Wrap an already-serialized data payload for one key in the snapshot | ||
| 110 | // envelope. dataJson is spliced in verbatim so we don't parse-then-reserialize. | ||
| 111 | 471 | std::string makeSnapshot(const char* key, const std::string& dataJson) { | |
| 112 | 471 | std::string out; | |
| 113 |
1/2✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
|
471 | out.reserve(dataJson.size() + 64); |
| 114 |
1/2✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
|
471 | out += "{\"type\":\"snapshot\",\"data\":{\""; |
| 115 |
1/2✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
|
471 | out += key; |
| 116 |
1/2✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
|
471 | out += "\":"; |
| 117 |
1/2✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
|
471 | out += dataJson; |
| 118 |
1/2✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
|
471 | out += "}}"; |
| 119 | 471 | return out; | |
| 120 | ✗ | } | |
| 121 | |||
| 122 | // Produce the current status, and if it differs from what was last sent (or a | ||
| 123 | // new client needs it), push it to all connected clients. Cheap and a no-op | ||
| 124 | // when nobody is connected, so the 1s timer costs nothing on an idle system. | ||
| 125 | 11969 | void broadcastStatusIfChanged() { | |
| 126 | 11969 | std::string msg; | |
| 127 | 11969 | std::vector<WebSocketConnectionPtr> targets; | |
| 128 | { | ||
| 129 |
1/2✓ Branch 0 taken 11969 times.
✗ Branch 1 not taken.
|
11969 | std::lock_guard<std::mutex> lk(g_mutex); |
| 130 | // Check for connections before doing any work: with nobody listening | ||
| 131 | // the 1s timer must cost nothing, so an idle system pays no price for | ||
| 132 | // this endpoint existing. (A client can appear right after this check; | ||
| 133 | // it gets its own snapshot in handleNewConnection, so a missed | ||
| 134 | // broadcast here is harmless.) | ||
| 135 |
5/6✓ Branch 0 taken 11969 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11870 times.
✓ Branch 3 taken 99 times.
✓ Branch 4 taken 11870 times.
✓ Branch 5 taken 99 times.
|
11969 | if (g_shutdown || g_conns.empty()) |
| 136 | 11870 | return; | |
| 137 | // The build is deliberately inside the lock. Three thread contexts | ||
| 138 | // reach this -- the drogon timer, the FPP-Warnings notify thread, and | ||
| 139 | // an IO thread via handleNewConnection -- and building outside it let a | ||
| 140 | // slower thread win the lock after a faster one and publish an older | ||
| 141 | // payload as the newest state (a warning could blink away for a second). | ||
| 142 | // g_mutex is file-static and only ever taken from those entry points, | ||
| 143 | // none of which hold any of the locks buildStatusJson() reaches, so the | ||
| 144 | // nesting is one-directional and this cannot deadlock. The hold is the | ||
| 145 | // ~0.7ms build. | ||
| 146 |
1/2✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
|
99 | Json::Value status; |
| 147 |
1/2✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
|
99 | GetCurrentFPPDStatus(status); |
| 148 |
2/4✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 99 times.
✗ Branch 3 not taken.
|
99 | std::string js = SaveJsonToString(status, ""); |
| 149 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 99 times.
|
99 | if (js == g_lastStatusJson) |
| 150 | ✗ | return; | |
| 151 | 99 | g_lastStatusJson = std::move(js); | |
| 152 |
1/2✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
|
99 | msg = makeSnapshot("status", g_lastStatusJson); |
| 153 | 99 | bool quietDue = false; | |
| 154 |
2/2✓ Branch 0 taken 35 times.
✓ Branch 1 taken 64 times.
|
99 | if (!g_quietConns.empty()) { |
| 155 |
2/2✓ Branch 0 taken 455 times.
✓ Branch 1 taken 35 times.
|
490 | for (const char* key : kTickingKeys) { |
| 156 |
1/2✓ Branch 0 taken 455 times.
✗ Branch 1 not taken.
|
455 | status.removeMember(key); |
| 157 | } | ||
| 158 |
2/4✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
|
35 | std::string stable = SaveJsonToString(status, ""); |
| 159 |
1/2✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
|
35 | uint64_t now = GetTimeMS(); |
| 160 |
6/6✓ Branch 0 taken 31 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 30 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 30 times.
|
35 | if (stable != g_lastQuietJson || now - g_lastQuietSendMs >= QUIET_KEEPALIVE_MS) { |
| 161 | 5 | g_lastQuietJson = std::move(stable); | |
| 162 | 5 | g_lastQuietSendMs = now; | |
| 163 | 5 | quietDue = true; | |
| 164 | } | ||
| 165 | 35 | } | |
| 166 |
2/2✓ Branch 0 taken 406 times.
✓ Branch 1 taken 99 times.
|
505 | for (const auto& c : g_conns) { |
| 167 |
7/8✓ Branch 0 taken 397 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 397 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 367 times.
✓ Branch 5 taken 30 times.
✓ Branch 6 taken 376 times.
✓ Branch 7 taken 30 times.
|
406 | if (quietDue || !g_quietConns.count(c)) { |
| 168 |
1/2✓ Branch 0 taken 376 times.
✗ Branch 1 not taken.
|
376 | targets.push_back(c); |
| 169 | } | ||
| 170 | } | ||
| 171 |
4/6✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 99 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 99 times.
✓ Branch 5 taken 11870 times.
|
11969 | } |
| 172 |
2/2✓ Branch 0 taken 376 times.
✓ Branch 1 taken 99 times.
|
475 | for (auto& c : targets) { |
| 173 |
2/4✓ Branch 0 taken 376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 376 times.
✗ Branch 3 not taken.
|
376 | if (c->connected()) |
| 174 |
1/2✓ Branch 0 taken 376 times.
✗ Branch 1 not taken.
|
376 | c->send(msg); |
| 175 | } | ||
| 176 |
4/4✓ Branch 0 taken 99 times.
✓ Branch 1 taken 11870 times.
✓ Branch 2 taken 99 times.
✓ Branch 3 taken 11870 times.
|
23839 | } |
| 177 | |||
| 178 | #ifdef HAS_AUDIO_LEVEL_MONITOR | ||
| 179 | // Push signal levels to anyone watching meters. | ||
| 180 | // | ||
| 181 | // Separate from the status broadcast and much faster: a meter that updates once | ||
| 182 | // a second does not read as a meter. It is also unconditional rather than | ||
| 183 | // change-gated -- levels differ on essentially every tick, so diffing them | ||
| 184 | // would cost more than it saved -- which is why this only runs while something | ||
| 185 | // is actually subscribed. With no meters up, AudioLevelMonitor has no pipelines | ||
| 186 | // and this returns immediately. | ||
| 187 | 102491 | void broadcastLevels() { | |
| 188 | 102491 | std::string msg; | |
| 189 | 102491 | std::vector<WebSocketConnectionPtr> targets; | |
| 190 | { | ||
| 191 |
1/2✓ Branch 0 taken 102491 times.
✗ Branch 1 not taken.
|
102491 | std::lock_guard<std::mutex> lk(g_mutex); |
| 192 |
8/8✓ Branch 0 taken 102486 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 959 times.
✓ Branch 3 taken 101527 times.
✓ Branch 4 taken 793 times.
✓ Branch 5 taken 166 times.
✓ Branch 6 taken 102325 times.
✓ Branch 7 taken 166 times.
|
102491 | if (g_shutdown || g_conns.empty() || !AudioLevelMonitor::INSTANCE.Active()) |
| 193 | 102325 | return; | |
| 194 |
4/8✓ Branch 0 taken 166 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 166 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 166 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 166 times.
✗ Branch 7 not taken.
|
166 | msg = makeSnapshot("levels", SaveJsonToString(AudioLevelMonitor::INSTANCE.GetLevels(), "")); |
| 195 |
1/2✓ Branch 0 taken 166 times.
✗ Branch 1 not taken.
|
166 | targets.assign(g_conns.begin(), g_conns.end()); |
| 196 |
2/2✓ Branch 0 taken 166 times.
✓ Branch 1 taken 102325 times.
|
102491 | } |
| 197 |
2/2✓ Branch 0 taken 264 times.
✓ Branch 1 taken 166 times.
|
430 | for (auto& c : targets) { |
| 198 |
2/4✓ Branch 0 taken 264 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 264 times.
✗ Branch 3 not taken.
|
264 | if (c->connected()) |
| 199 |
1/2✓ Branch 0 taken 264 times.
✗ Branch 1 not taken.
|
264 | c->send(msg); |
| 200 | } | ||
| 201 |
4/4✓ Branch 0 taken 166 times.
✓ Branch 1 taken 102325 times.
✓ Branch 2 taken 166 times.
✓ Branch 3 taken 102325 times.
|
204816 | } |
| 202 | #endif | ||
| 203 | |||
| 204 | // Warnings are part of the /fppd/status payload, so a new or cleared warning | ||
| 205 | // changes that payload. Rather than wait up to a second for the timer, push | ||
| 206 | // immediately when WarningHolder notifies us. | ||
| 207 | class StatusWarningListener : public WarningListener { | ||
| 208 | public: | ||
| 209 | 2303 | void handleWarnings(const std::list<FPPWarning>& /*warnings*/) override { | |
| 210 | 2303 | broadcastStatusIfChanged(); | |
| 211 | 2303 | } | |
| 212 | }; | ||
| 213 | StatusWarningListener* g_warningListener = nullptr; | ||
| 214 | |||
| 215 | } // namespace | ||
| 216 | |||
| 217 | // WebSocket controller bound to /fppdws. Instances are thin: all shared state | ||
| 218 | // lives in the file-static structures above so the timer and warning callbacks | ||
| 219 | // (which have no controller instance) can reach it too. | ||
| 220 | class StatusWebSocket : public drogon::WebSocketController<StatusWebSocket> { | ||
| 221 | public: | ||
| 222 | 206 | void handleNewConnection(const HttpRequestPtr& req, | |
| 223 | const WebSocketConnectionPtr& conn) override { | ||
| 224 | 206 | std::string msg; | |
| 225 | { | ||
| 226 | // A connection can still land after shutdown has begun (drogon's | ||
| 227 | // quit() only queues teardown). Building a payload then would read | ||
| 228 | // producers that are being destroyed, so answer nothing at all. | ||
| 229 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | std::lock_guard<std::mutex> lk(g_mutex); |
| 230 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 206 times.
|
206 | if (g_shutdown) |
| 231 | ✗ | return; | |
| 232 | // Send this client a full snapshot immediately so it doesn't have | ||
| 233 | // to wait for the next change. Force a build (bypass the change | ||
| 234 | // check), inside the lock for the same reason | ||
| 235 | // broadcastStatusIfChanged() builds inside it: this runs on an IO | ||
| 236 | // thread alongside the other two producers, and a build outside the | ||
| 237 | // lock could overwrite g_lastStatusJson with an older payload. | ||
| 238 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | std::string js = buildStatusJson(); |
| 239 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | g_conns.insert(conn); |
| 240 | // Its own first snapshot doesn't advance the quiet cache: the | ||
| 241 | // other quiet clients haven't seen this state yet. | ||
| 242 |
5/8✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 206 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 206 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 203 times.
|
618 | if (req->getParameter("quiet") == "1") { |
| 243 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | g_quietConns.insert(conn); |
| 244 | } | ||
| 245 | // Advance the shared cache so the timer doesn't immediately | ||
| 246 | // re-broadcast identical data to everyone. | ||
| 247 | 206 | g_lastStatusJson = std::move(js); | |
| 248 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | msg = makeSnapshot("status", g_lastStatusJson); |
| 249 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | } |
| 250 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | conn->send(msg); |
| 251 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | } |
| 252 | |||
| 253 | 32 | void handleNewMessage(const WebSocketConnectionPtr& /*conn*/, | |
| 254 | std::string&& /*message*/, | ||
| 255 | const WebSocketMessageType& /*type*/) override { | ||
| 256 | // Read-only endpoint: ignore anything the client sends. drogon | ||
| 257 | // handles ping/pong framing internally. | ||
| 258 | 32 | } | |
| 259 | |||
| 260 | 206 | void handleConnectionClosed(const WebSocketConnectionPtr& conn) override { | |
| 261 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | std::lock_guard<std::mutex> lk(g_mutex); |
| 262 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | g_conns.erase(conn); |
| 263 |
1/2✓ Branch 0 taken 206 times.
✗ Branch 1 not taken.
|
206 | g_quietConns.erase(conn); |
| 264 | 206 | } | |
| 265 | |||
| 266 | 2346 | WS_PATH_LIST_BEGIN | |
| 267 |
2/4✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
|
6646 | WS_PATH_ADD("/fppdws"); |
| 268 | 2346 | WS_PATH_LIST_END | |
| 269 | }; | ||
| 270 | |||
| 271 | 1885 | void StatusWebSocketInit() { | |
| 272 | static bool initialized = false; | ||
| 273 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
|
1885 | if (initialized) |
| 274 | ✗ | return; | |
| 275 | 1885 | initialized = true; | |
| 276 | |||
| 277 |
1/2✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
|
1885 | if (!g_warningListener) { |
| 278 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
|
1885 | g_warningListener = new StatusWarningListener(); |
| 279 | 1885 | WarningHolder::AddWarningListener(g_warningListener); | |
| 280 | } | ||
| 281 | |||
| 282 | // A once-per-second tick on drogon's own event loop -- independent of the | ||
| 283 | // fppd main loop, which is epoll-gated and doesn't tick reliably during | ||
| 284 | // playback. broadcastStatusIfChanged() is a no-op with no clients, so | ||
| 285 | // this is effectively free until a page connects. | ||
| 286 |
1/2✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
|
11551 | drogon::app().getLoop()->runEvery(1.0, []() { broadcastStatusIfChanged(); }); |
| 287 | |||
| 288 | #ifdef HAS_AUDIO_LEVEL_MONITOR | ||
| 289 | // Meters need a much faster tick than status. This costs nothing until | ||
| 290 | // someone subscribes -- see broadcastLevels(). | ||
| 291 |
1/2✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
|
104376 | drogon::app().getLoop()->runEvery(0.1, []() { broadcastLevels(); }); |
| 292 | #endif | ||
| 293 | } | ||
| 294 | |||
| 295 | 1867 | void StatusWebSocketShutdown() { | |
| 296 | static bool shutdownDone = false; | ||
| 297 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1867 times.
|
1867 | if (shutdownDone) |
| 298 | ✗ | return; | |
| 299 | 1867 | shutdownDone = true; | |
| 300 | |||
| 301 | // Order matters here. Removing the listener first is what makes the rest | ||
| 302 | // safe: WarningHolder holds its listener lock across the whole notify pass, | ||
| 303 | // so once RemoveWarningListener() returns, no handleWarnings() call on our | ||
| 304 | // listener is running or can start, and the warning thread can no longer | ||
| 305 | // drive a status build. The listener object itself is deliberately leaked -- | ||
| 306 | // the notify thread is the only user and it is done with it by now, and a | ||
| 307 | // delete buys nothing on a process that _exit()s. | ||
| 308 |
1/2✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
|
1867 | if (g_warningListener) { |
| 309 |
1/2✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
|
1867 | WarningHolder::RemoveWarningListener(g_warningListener); |
| 310 | } | ||
| 311 | |||
| 312 | // The remaining producer is the drogon timer, which is still ticking (this | ||
| 313 | // runs before drogon is stopped). g_shutdown stops it building, and clearing | ||
| 314 | // g_conns drops the connection references drogon's quit() is not guaranteed | ||
| 315 | // to release through the close callbacks. | ||
| 316 |
1/2✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
|
1867 | std::lock_guard<std::mutex> lk(g_mutex); |
| 317 | 1867 | g_shutdown = true; | |
| 318 | 1867 | g_conns.clear(); | |
| 319 | 1867 | g_quietConns.clear(); | |
| 320 | 1867 | } | |
| 321 |