| 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 "Warnings.h" // WarningHolder -- needed directly for NOPCH builds | ||
| 18 | |||
| 19 | #include <atomic> | ||
| 20 | #include <chrono> | ||
| 21 | #include <fstream> | ||
| 22 | #include <thread> | ||
| 23 | |||
| 24 | #include "../CurlManager.h" | ||
| 25 | #include "../common.h" | ||
| 26 | #include "../log.h" | ||
| 27 | |||
| 28 | #include "Playlist.h" | ||
| 29 | #include "PlaylistEntryBoth.h" | ||
| 30 | #include "PlaylistEntryCommand.h" | ||
| 31 | #include "PlaylistEntryDynamic.h" | ||
| 32 | #include "PlaylistEntryMedia.h" | ||
| 33 | #include "PlaylistEntryPause.h" | ||
| 34 | #include "PlaylistEntryRemap.h" | ||
| 35 | #include "PlaylistEntryScript.h" | ||
| 36 | #include "PlaylistEntrySequence.h" | ||
| 37 | #include "PlaylistEntryURL.h" | ||
| 38 | |||
| 39 | /* | ||
| 40 | * | ||
| 41 | */ | ||
| 42 | 15 | PlaylistEntryDynamic::PlaylistEntryDynamic(Playlist* playlist, PlaylistEntryBase* parent) : | |
| 43 | PlaylistEntryBase(playlist, parent), | ||
| 44 | 15 | m_drainQueue(0), | |
| 45 | 15 | m_currentEntry(-1) { | |
| 46 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::PlaylistEntryDynamic()\n"); |
| 47 | |||
| 48 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | m_type = "dynamic"; |
| 49 | |||
| 50 | static std::atomic<uint64_t> s_dynamicCounter{ 0 }; | ||
| 51 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | m_curlToken = "PlaylistEntryDynamic-" + std::to_string(s_dynamicCounter.fetch_add(1)); |
| 52 | 15 | } | |
| 53 | |||
| 54 | /* | ||
| 55 | * | ||
| 56 | */ | ||
| 57 | // Longest a plugin or URL source may take to answer before the entry gives up. | ||
| 58 | static constexpr long long DYNAMIC_FETCH_TIMEOUT_MS = 15000; | ||
| 59 | |||
| 60 | 60 | PlaylistEntryDynamic::~PlaylistEntryDynamic() { | |
| 61 | // Ordering matters for UAF/lifecycle safety: | ||
| 62 | // 1) cancelRequests() removes this entry's in-flight handles from | ||
| 63 | // CurlManager and destroys their callbacks (each captures `this`) | ||
| 64 | // WITHOUT invoking them, so no callback can run against a half-torn-down | ||
| 65 | // object. Safe from the dtor because processCurls() and this dtor both | ||
| 66 | // run on the main loop, so no callback is executing right now. | ||
| 67 | // 2) only then release the cookie session: once the requests are gone, no | ||
| 68 | // easy handle still points CURLOPT_SHARE at the share, so | ||
| 69 | // curl_share_cleanup() is safe. | ||
| 70 | 30 | CurlManager::INSTANCE.cancelRequests(m_curlToken); | |
| 71 | 30 | CurlManager::INSTANCE.releaseCookieSession(m_curlToken); | |
| 72 | |||
| 73 | 30 | ClearPlaylistEntries(); | |
| 74 | 60 | } | |
| 75 | |||
| 76 | /* | ||
| 77 | * | ||
| 78 | */ | ||
| 79 | 15 | int PlaylistEntryDynamic::Init(Json::Value& config) { | |
| 80 | 15 | LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::Init()\n"); | |
| 81 | |||
| 82 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | m_subType = config["subType"].asString(); |
| 83 | |||
| 84 | 15 | m_drainQueue = config["drainQueue"].asInt(); | |
| 85 | |||
| 86 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 14 times.
|
15 | if (config.isMember("pluginHost")) |
| 87 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | m_pluginHost = config["pluginHost"].asString(); |
| 88 | |||
| 89 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
|
15 | if (m_subType == "file") |
| 90 |
2/4✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
|
10 | m_data = config["dataFile"].asString(); |
| 91 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
|
5 | else if (m_subType == "plugin") |
| 92 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | m_data = config["pluginName"].asString(); |
| 93 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | else if (m_subType == "url") |
| 94 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | m_data = config["url"].asString(); |
| 95 | |||
| 96 | // The plugin/url HTTP requests go through CurlManager (see PrepPlugin()/ | ||
| 97 | // FetchURL()); there is no per-instance curl handle to set up. | ||
| 98 | // Cookie persistence across the plugin's prep/load/started calls is provided | ||
| 99 | // by CurlManager's cookie session keyed on m_curlToken. | ||
| 100 | |||
| 101 | 15 | return PlaylistEntryBase::Init(config); | |
| 102 | } | ||
| 103 | |||
| 104 | /* | ||
| 105 | * | ||
| 106 | */ | ||
| 107 | 17 | int PlaylistEntryDynamic::StartPlaying(void) { | |
| 108 | 17 | LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::StartPlaying()\n"); | |
| 109 | |||
| 110 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
|
17 | if (!CanPlay()) { |
| 111 | ✗ | FinishPlay(); | |
| 112 | ✗ | return 0; | |
| 113 | } | ||
| 114 | |||
| 115 |
6/6✓ Branch 0 taken 14 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 10 times.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 10 times.
|
17 | if ((m_subType == "plugin") || (m_subType == "url")) { |
| 116 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
|
7 | int started = (m_subType == "plugin") ? PrepPlugin() : FetchURL(); |
| 117 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
|
7 | if (!started) { |
| 118 | ✗ | FinishPlay(); | |
| 119 | ✗ | return 0; | |
| 120 | } | ||
| 121 | 7 | m_loading = true; | |
| 122 | 7 | m_loadDeadline = GetMonotonicTimeMS() + DYNAMIC_FETCH_TIMEOUT_MS; | |
| 123 | 7 | return PlaylistEntryBase::StartPlaying(); | |
| 124 | } | ||
| 125 | |||
| 126 | 10 | int res = 0; | |
| 127 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
|
10 | if (m_subType == "command") |
| 128 | ✗ | res = ReadFromCommand(); | |
| 129 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
10 | else if (m_subType == "file") |
| 130 | 10 | res = ReadFromFile(); | |
| 131 | |||
| 132 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
|
10 | if (!BeginEntries(res)) { |
| 133 | 6 | FinishPlay(); | |
| 134 | 6 | return 0; | |
| 135 | } | ||
| 136 | |||
| 137 | 4 | return PlaylistEntryBase::StartPlaying(); | |
| 138 | } | ||
| 139 | |||
| 140 | 15 | bool PlaylistEntryDynamic::BeginEntries(int loaded) { | |
| 141 |
3/4✓ Branch 0 taken 8 times.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
|
15 | if (!loaded || (m_currentEntry < 0)) |
| 142 | 7 | return false; | |
| 143 | |||
| 144 | 8 | m_isPrepped = 0; | |
| 145 | 8 | m_playlistEntries[m_currentEntry]->StartPlayingOrFinish(); | |
| 146 | 8 | Started(); | |
| 147 | 8 | return true; | |
| 148 | } | ||
| 149 | |||
| 150 | /* | ||
| 151 | * | ||
| 152 | */ | ||
| 153 | 437 | int PlaylistEntryDynamic::Process(void) { | |
| 154 |
2/2✓ Branch 0 taken 424 times.
✓ Branch 1 taken 13 times.
|
437 | if (m_loading) { |
| 155 | PluginPrep state; | ||
| 156 | 424 | std::string item; | |
| 157 | { | ||
| 158 |
1/2✓ Branch 0 taken 424 times.
✗ Branch 1 not taken.
|
424 | std::lock_guard<std::mutex> g(m_pluginMutex); |
| 159 | 424 | state = m_pluginPrep; | |
| 160 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 419 times.
|
424 | if (state == PluginPrep::Ready) { |
| 161 | 5 | item = std::move(m_pluginItem); | |
| 162 | 5 | m_pluginItem.clear(); | |
| 163 | 5 | m_pluginPrep = PluginPrep::Idle; // consumed; the next start re-fetches | |
| 164 | } | ||
| 165 | 424 | } | |
| 166 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 419 times.
|
424 | if (state == PluginPrep::Ready) { |
| 167 | 5 | m_loading = false; | |
| 168 |
5/8✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 4 times.
|
5 | if (!BeginEntries(ReadFromString(item))) |
| 169 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | FinishPlay(); |
| 170 |
6/8✓ Branch 0 taken 417 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 417 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 417 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 417 times.
|
419 | } else if ((state == PluginPrep::Failed) || (GetMonotonicTimeMS() >= m_loadDeadline)) { |
| 171 | 2 | m_loading = false; | |
| 172 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (state != PluginPrep::Failed) { |
| 173 | ✗ | LogErr(VB_PLAYLIST, "Dynamic playlist %s request did not complete in time\n", m_subType.c_str()); | |
| 174 | ✗ | WarningHolder::AddWarningTimeout(60, 32, "Dynamic playlist " + m_subType + " request timed out"); | |
| 175 | } | ||
| 176 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | FinishPlay(); |
| 177 | } | ||
| 178 |
1/2✓ Branch 0 taken 424 times.
✗ Branch 1 not taken.
|
424 | return PlaylistEntryBase::Process(); |
| 179 | 424 | } | |
| 180 | |||
| 181 |
3/6✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
|
13 | if ((m_currentEntry >= 0) && (m_playlistEntries[m_currentEntry])) { |
| 182 | // Only a child that is playing gets Process(), as in | ||
| 183 | // Playlist::Process(). One that finished inside StartPlaying() (a | ||
| 184 | // disabled entry, a failed start) was never set up for it: a disabled | ||
| 185 | // command entry has no result, and processing it dereferenced null. | ||
| 186 |
2/2✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 times.
|
13 | if (m_playlistEntries[m_currentEntry]->IsPlaying()) |
| 187 | 12 | m_playlistEntries[m_currentEntry]->Process(); | |
| 188 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | if (m_playlistEntries[m_currentEntry]->IsFinished()) { |
| 189 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
|
26 | if ((m_parentPlaylist->getPlaylistStatus() == FPP_STATUS_STOPPING_GRACEFULLY) || |
| 190 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
|
13 | (m_parentPlaylist->getPlaylistStatus() == FPP_STATUS_STOPPING_GRACEFULLY_AFTER_LOOP)) { |
| 191 | ✗ | FinishPlay(); | |
| 192 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 8 times.
|
13 | } else if (m_currentEntry < (m_playlistEntries.size() - 1)) { |
| 193 | 5 | m_currentEntry++; | |
| 194 | 5 | m_playlistEntries[m_currentEntry]->StartPlayingOrFinish(); | |
| 195 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
|
8 | } else if (m_drainQueue) { |
| 196 | // Check for more entries to play, if there are none, | ||
| 197 | // then StartPlaying() will call FinishPlay(). | ||
| 198 | 2 | StartPlaying(); | |
| 199 | } else { | ||
| 200 | 6 | FinishPlay(); | |
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | 13 | return 1; | |
| 205 | } | ||
| 206 | |||
| 207 | ✗ | return 0; | |
| 208 | } | ||
| 209 | |||
| 210 | /* | ||
| 211 | * | ||
| 212 | */ | ||
| 213 | ✗ | int PlaylistEntryDynamic::Stop(void) { | |
| 214 | ✗ | LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::Stop()\n"); | |
| 215 | ✗ | m_loading = false; | |
| 216 | |||
| 217 | ✗ | if ((m_currentEntry >= 0) && (m_playlistEntries[m_currentEntry])) { | |
| 218 | ✗ | m_playlistEntries[m_currentEntry]->Stop(); | |
| 219 | ✗ | return 1; | |
| 220 | } | ||
| 221 | |||
| 222 | ✗ | return PlaylistEntryBase::Stop(); | |
| 223 | } | ||
| 224 | |||
| 225 | /* | ||
| 226 | * | ||
| 227 | */ | ||
| 228 | ✗ | void PlaylistEntryDynamic::Dump(void) { | |
| 229 | ✗ | LogDebug(VB_PLAYLIST, "SubType : %s\n", m_subType.c_str()); | |
| 230 | ✗ | LogDebug(VB_PLAYLIST, "Data : %s\n", m_data.c_str()); | |
| 231 | ✗ | LogDebug(VB_PLAYLIST, "Drain Queue: %d\n", m_drainQueue); | |
| 232 | ✗ | LogDebug(VB_PLAYLIST, "Plugin Host: %s\n", m_pluginHost.c_str()); | |
| 233 | ✗ | } | |
| 234 | |||
| 235 | /* | ||
| 236 | * | ||
| 237 | */ | ||
| 238 | 30 | Json::Value PlaylistEntryDynamic::GetConfig(void) { | |
| 239 | 30 | Json::Value result = PlaylistEntryBase::GetConfig(); | |
| 240 | |||
| 241 |
2/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
|
30 | result["subType"] = m_subType; |
| 242 |
2/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
|
30 | result["data"] = m_data; |
| 243 | |||
| 244 |
3/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 10 times.
|
30 | if (m_subType == "file") { |
| 245 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | result["dataFile"] = m_data; |
| 246 |
3/4✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 8 times.
|
10 | } else if (m_subType == "plugin") { |
| 247 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | result["pluginName"] = m_data; |
| 248 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | result["pluginHost"] = m_pluginHost; |
| 249 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
8 | } else if (m_subType == "url") { |
| 250 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
8 | result["url"] = m_data; |
| 251 | } | ||
| 252 | |||
| 253 |
2/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
|
30 | result["drainQueue"] = m_drainQueue; |
| 254 |
5/6✓ Branch 0 taken 10 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✓ Branch 5 taken 20 times.
|
30 | if ((m_currentEntry >= 0) && (m_playlistEntries[m_currentEntry])) |
| 255 |
2/4✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
|
10 | result["dynamic"] = m_playlistEntries[m_currentEntry]->GetConfig(); |
| 256 | |||
| 257 | 30 | return result; | |
| 258 | ✗ | } | |
| 259 | 378 | PlaylistEntryBase* PlaylistEntryDynamic::GetCurrentEntry() { | |
| 260 |
6/8✓ Branch 0 taken 337 times.
✓ Branch 1 taken 41 times.
✓ Branch 2 taken 337 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 337 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 337 times.
✓ Branch 7 taken 41 times.
|
378 | if ((m_currentEntry >= 0) && (m_currentEntry < m_playlistEntries.size()) && (m_playlistEntries[m_currentEntry])) { |
| 261 | 337 | return m_playlistEntries[m_currentEntry]; | |
| 262 | } | ||
| 263 | 41 | return nullptr; | |
| 264 | } | ||
| 265 | |||
| 266 | /* | ||
| 267 | * | ||
| 268 | */ | ||
| 269 | ✗ | int PlaylistEntryDynamic::ReadFromCommand(void) { | |
| 270 | ✗ | LogDebug(VB_PLAYLIST, "ReadFromCommand: %s\n", m_data.c_str()); | |
| 271 | |||
| 272 | // FIXME, implement this and change return to 1 | ||
| 273 | ✗ | return 0; | |
| 274 | } | ||
| 275 | |||
| 276 | /* | ||
| 277 | * | ||
| 278 | */ | ||
| 279 | 10 | int PlaylistEntryDynamic::ReadFromFile(void) { | |
| 280 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
10 | LogDebug(VB_PLAYLIST, "ReadFromFile: %s\n", m_data.c_str()); |
| 281 | |||
| 282 |
3/4✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 9 times.
|
10 | if (!FileExists(m_data.c_str())) { |
| 283 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogErr(VB_PLAYLIST, "Filename %s does not exist\n", m_data.c_str()); |
| 284 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
1 | WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist source file does not exist: " + m_data); |
| 285 | 1 | return 0; | |
| 286 | } | ||
| 287 | |||
| 288 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | std::ifstream t(m_data.c_str()); |
| 289 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | std::stringstream buffer; |
| 290 | |||
| 291 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | buffer << t.rdbuf(); |
| 292 | |||
| 293 |
2/4✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
|
9 | return ReadFromString(buffer.str()); |
| 294 | 9 | } | |
| 295 | |||
| 296 | /* | ||
| 297 | * The "url" source: one async GET whose response is consumed by Process(). | ||
| 298 | * rc==0 means the transfer never got an HTTP response (connect/timeout). | ||
| 299 | */ | ||
| 300 | 4 | int PlaylistEntryDynamic::FetchURL(void) { | |
| 301 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | LogDebug(VB_PLAYLIST, "FetchURL: %s\n", m_data.c_str()); |
| 302 | { | ||
| 303 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | std::lock_guard<std::mutex> g(m_pluginMutex); |
| 304 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (m_pluginPrep == PluginPrep::Running) { |
| 305 | ✗ | return 1; // the previous start's request is still out; use it | |
| 306 | } | ||
| 307 | 4 | m_pluginPrep = PluginPrep::Running; | |
| 308 | 4 | m_pluginItem.clear(); | |
| 309 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | } |
| 310 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | std::string url = m_data; |
| 311 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | CurlManager::INSTANCE.addGet( |
| 312 | url, | ||
| 313 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
4 | [this, url](int rc, const std::string& resp) { |
| 314 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | std::lock_guard<std::mutex> g(m_pluginMutex); |
| 315 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | if (rc == 0) { |
| 316 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | LogErr(VB_PLAYLIST, "Dynamic playlist URL request failed (%s): %s\n", url.c_str(), resp.c_str()); |
| 317 |
5/10✓ 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.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
|
2 | WarningHolder::AddWarningTimeout(60, 32, "Dynamic playlist URL request failed (" + url + "): " + resp); |
| 318 | 2 | m_pluginPrep = PluginPrep::Failed; | |
| 319 | } else { | ||
| 320 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | m_pluginItem = resp; |
| 321 | 2 | m_pluginPrep = PluginPrep::Ready; | |
| 322 | } | ||
| 323 | 4 | }, | |
| 324 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | m_curlToken, m_curlToken); |
| 325 | 4 | return 1; | |
| 326 | 4 | } | |
| 327 | |||
| 328 | /* | ||
| 329 | * | ||
| 330 | */ | ||
| 331 | 14 | int PlaylistEntryDynamic::ReadFromString(std::string jsonStr) { | |
| 332 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | Json::Value root; |
| 333 | 14 | PlaylistEntryBase* playlistEntry = NULL; | |
| 334 | |||
| 335 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | LogDebug(VB_PLAYLIST, "ReadFromString(): String:\n%s\n", jsonStr.c_str()); |
| 336 | |||
| 337 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
|
14 | if (jsonStr.empty()) { |
| 338 | ✗ | LogDebug(VB_PLAYLIST, "Empty string in ReadFromString()\n"); | |
| 339 | ✗ | return 0; | |
| 340 | } | ||
| 341 | |||
| 342 |
3/4✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 11 times.
|
14 | if (!LoadJsonFromString(jsonStr, root, JsonRoot::Object)) { |
| 343 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | LogErr(VB_PLAYLIST, "Error parsing JSON: %s\n", jsonStr.c_str()); |
| 344 |
3/6✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
|
9 | WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist data is not valid JSON"); |
| 345 | 3 | return 0; | |
| 346 | } | ||
| 347 | |||
| 348 |
1/2✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
|
11 | ClearPlaylistEntries(); |
| 349 | |||
| 350 |
1/2✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
|
11 | Json::Value entries; |
| 351 |
3/6✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
|
22 | if (JsonHas(root, "mainPlaylist")) |
| 352 |
2/4✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
|
11 | entries = root["mainPlaylist"]; |
| 353 | ✗ | else if (JsonHas(root, "leadIn")) | |
| 354 | ✗ | entries = root["leadIn"]; | |
| 355 | ✗ | else if (JsonHas(root, "leadOut")) | |
| 356 | ✗ | entries = root["leadOut"]; | |
| 357 | ✗ | else if (JsonHas(root, "playlistEntries")) | |
| 358 | ✗ | entries = root["playlistEntries"]; | |
| 359 | else | ||
| 360 | ✗ | return 0; | |
| 361 | |||
| 362 |
2/4✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
|
11 | if (!entries.isArray()) { |
| 363 | ✗ | LogErr(VB_PLAYLIST, "Expected a JSON array of dynamic playlist entries, got something else\n"); | |
| 364 | ✗ | WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist data is not valid JSON"); | |
| 365 | ✗ | return 0; | |
| 366 | } | ||
| 367 | |||
| 368 |
3/4✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 9 times.
|
26 | for (int i = 0; i < entries.size(); i++) { |
| 369 |
2/4✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
|
17 | Json::Value pe = entries[i]; |
| 370 |
3/4✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 15 times.
|
17 | if (!pe.isObject()) { |
| 371 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | LogErr(VB_PLAYLIST, "Invalid dynamic playlist entry: expected a JSON object\n"); |
| 372 |
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.
|
6 | WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist data is not valid JSON"); |
| 373 | 2 | continue; | |
| 374 | } | ||
| 375 | 15 | playlistEntry = NULL; | |
| 376 | |||
| 377 | // Plugins, URLs and files supply this data. jsoncpp throws on a | ||
| 378 | // wrong-typed value ("type" an object, "enabled":"1", ...) and nothing | ||
| 379 | // above StartPlaying() catches, so an escape terminated fppd. Treat a | ||
| 380 | // throw exactly like a failed Init(). | ||
| 381 | try { | ||
| 382 |
3/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 1 times.
|
15 | std::string type = pe["type"].asString(); |
| 383 |
2/4✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
|
14 | if (type == "both") |
| 384 | ✗ | playlistEntry = new PlaylistEntryBoth(m_parentPlaylist); | |
| 385 |
2/4✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
|
14 | else if (type == "command") |
| 386 |
3/8✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 14 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
14 | playlistEntry = new PlaylistEntryCommand(m_parentPlaylist); |
| 387 | ✗ | else if (type == "media") | |
| 388 | ✗ | playlistEntry = new PlaylistEntryMedia(m_parentPlaylist); | |
| 389 | ✗ | else if (type == "pause") | |
| 390 | ✗ | playlistEntry = new PlaylistEntryPause(m_parentPlaylist); | |
| 391 | ✗ | else if (type == "remap") | |
| 392 | ✗ | playlistEntry = new PlaylistEntryRemap(m_parentPlaylist); | |
| 393 | ✗ | else if (type == "script") | |
| 394 | ✗ | playlistEntry = new PlaylistEntryScript(m_parentPlaylist); | |
| 395 | ✗ | else if (type == "sequence") | |
| 396 | ✗ | playlistEntry = new PlaylistEntrySequence(m_parentPlaylist); | |
| 397 | ✗ | else if (type == "url") | |
| 398 | ✗ | playlistEntry = new PlaylistEntryURL(m_parentPlaylist); | |
| 399 | else { | ||
| 400 | ✗ | LogErr(VB_PLAYLIST, "Invalid Playlist Entry Type: %s\n", pe["type"].asString().c_str()); | |
| 401 | ✗ | WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist contains an invalid entry type: " + pe["type"].asString()); | |
| 402 | ✗ | ClearPlaylistEntries(); | |
| 403 | ✗ | return 0; | |
| 404 | } | ||
| 405 | |||
| 406 |
3/4✓ Branch 0 taken 13 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
|
14 | if (!playlistEntry->Init(pe)) { |
| 407 | ✗ | LogErr(VB_PLAYLIST, "Error initializing %s Playlist Entry\n", pe["type"].asString().c_str()); | |
| 408 | ✗ | WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist entry failed to initialize: " + pe["type"].asString()); | |
| 409 | // Not yet pushed into m_playlistEntries (that happens only after a | ||
| 410 | // successful Init below), so ClearPlaylistEntries() won't free it. | ||
| 411 | ✗ | delete playlistEntry; | |
| 412 | ✗ | ClearPlaylistEntries(); | |
| 413 | ✗ | return 0; | |
| 414 | } | ||
| 415 |
2/4✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
16 | } catch (const std::exception& ex) { |
| 416 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | LogErr(VB_PLAYLIST, "Invalid dynamic playlist entry: %s\n", ex.what()); |
| 417 |
4/8✓ 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.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
|
6 | WarningHolder::AddWarningTimeout(60, 33, std::string("Dynamic playlist entry is invalid: ") + ex.what()); |
| 418 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
|
2 | delete playlistEntry; |
| 419 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | ClearPlaylistEntries(); |
| 420 | 2 | return 0; | |
| 421 | 2 | } | |
| 422 | |||
| 423 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | m_playlistEntries.push_back(playlistEntry); |
| 424 |
3/3✓ Branch 0 taken 13 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
|
17 | } |
| 425 | |||
| 426 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 8 times.
|
9 | if (!m_playlistEntries.size()) { |
| 427 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogErr(VB_PLAYLIST, "Error, no valid playlistEntries in dynamic data!\n"); |
| 428 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
3 | WarningHolder::AddWarningTimeout(60, 33, "Dynamic playlist data contained no valid entries"); |
| 429 | 1 | return 0; | |
| 430 | } | ||
| 431 | |||
| 432 | 8 | m_currentEntry = 0; | |
| 433 | |||
| 434 | 8 | return 1; | |
| 435 | 14 | } | |
| 436 | |||
| 437 | /* | ||
| 438 | * | ||
| 439 | */ | ||
| 440 | 8 | int PlaylistEntryDynamic::Started(void) { | |
| 441 | 8 | LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::Started()\n"); | |
| 442 | |||
| 443 | 8 | int res = 1; | |
| 444 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
|
8 | if (m_subType == "plugin") |
| 445 | 2 | res = StartedPlugin(); | |
| 446 | |||
| 447 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (!res) { |
| 448 | ✗ | return 0; | |
| 449 | } | ||
| 450 | |||
| 451 | 8 | return res; | |
| 452 | } | ||
| 453 | |||
| 454 | /* | ||
| 455 | * | ||
| 456 | */ | ||
| 457 | 2 | int PlaylistEntryDynamic::StartedPlugin(void) { | |
| 458 | // Fire-and-forget notification. The response is discarded and the entry does | ||
| 459 | // not wait on it, so it runs async through CurlManager. It uses the same | ||
| 460 | // cookie session (m_curlToken) so the plugin sees one PHP session, and it is | ||
| 461 | // ordered after loadNextItem because we only get here once the buffered item | ||
| 462 | // has been consumed and started. The callback captures nothing (no `this`), | ||
| 463 | // so it is safe even if this entry is destroyed before it completes; the | ||
| 464 | // owner tag still lets the dtor's cancelRequests() reclaim it. | ||
| 465 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | std::string url = PluginCommandURL("startedNextItem"); |
| 466 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | CurlManager::INSTANCE.addGet( |
| 467 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
2 | url, [](int rc, const std::string& resp) {}, m_curlToken, m_curlToken); |
| 468 | 2 | return 1; | |
| 469 | 2 | } | |
| 470 | |||
| 471 | /* | ||
| 472 | * | ||
| 473 | */ | ||
| 474 | ✗ | int PlaylistEntryDynamic::Prep(void) { | |
| 475 | ✗ | LogDebug(VB_PLAYLIST, "PlaylistEntryDynamic::Prep()\n"); | |
| 476 | |||
| 477 | ✗ | int res = 1; | |
| 478 | ✗ | if (m_subType == "plugin") | |
| 479 | ✗ | res = PrepPlugin(); | |
| 480 | |||
| 481 | ✗ | if (!res) { | |
| 482 | ✗ | return 0; | |
| 483 | } | ||
| 484 | |||
| 485 | ✗ | return PlaylistEntryBase::Prep(); | |
| 486 | ; | ||
| 487 | } | ||
| 488 | |||
| 489 | /* | ||
| 490 | * | ||
| 491 | */ | ||
| 492 | 3 | int PlaylistEntryDynamic::PrepPlugin(void) { | |
| 493 | // Idempotent: if a prep/load chain is already running or has already buffered | ||
| 494 | // an item (an earlier Prep() primed it), leave it be. Only fire when | ||
| 495 | // Idle or after a prior Failed attempt (an inline-prep retry). | ||
| 496 | { | ||
| 497 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | std::lock_guard<std::mutex> g(m_pluginMutex); |
| 498 |
2/4✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
|
3 | if (m_pluginPrep == PluginPrep::Running || m_pluginPrep == PluginPrep::Ready) { |
| 499 | ✗ | return 1; | |
| 500 | } | ||
| 501 | 3 | m_pluginPrep = PluginPrep::Running; | |
| 502 | 3 | m_pluginItem.clear(); | |
| 503 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | } |
| 504 | |||
| 505 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | std::string url = PluginCommandURL("prepNextItem"); |
| 506 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | LogDebug(VB_PLAYLIST, "PrepPlugin URL: %s\n", url.c_str()); |
| 507 | |||
| 508 | // prepNextItem's response is a notification (discarded). Its completion | ||
| 509 | // callback chains loadNextItem, guaranteeing the stateful prep->load order | ||
| 510 | // over the shared cookie session. rc==0 means the transfer never reached the | ||
| 511 | // host (connect/timeout) -> abort the chain and mark Failed. | ||
| 512 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | CurlManager::INSTANCE.addGet( |
| 513 | url, | ||
| 514 | ✗ | [this](int rc, const std::string& resp) { | |
| 515 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
|
3 | if (rc == 0) { |
| 516 | ✗ | std::lock_guard<std::mutex> g(m_pluginMutex); | |
| 517 | ✗ | m_pluginPrep = PluginPrep::Failed; | |
| 518 | ✗ | return; | |
| 519 | ✗ | } | |
| 520 | 3 | FireLoadNextItem(); | |
| 521 | }, | ||
| 522 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
3 | m_curlToken, m_curlToken); |
| 523 | |||
| 524 | 3 | return 1; | |
| 525 | 3 | } | |
| 526 | |||
| 527 | /* | ||
| 528 | * | ||
| 529 | */ | ||
| 530 | 3 | void PlaylistEntryDynamic::FireLoadNextItem(void) { | |
| 531 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | std::string url = PluginCommandURL("loadNextItem"); |
| 532 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | LogDebug(VB_PLAYLIST, "FireLoadNextItem URL: %s\n", url.c_str()); |
| 533 | |||
| 534 | // loadNextItem's response IS the next item's JSON; buffer it for | ||
| 535 | // Process() to consume. Runs on the main-loop thread (processCurls()), | ||
| 536 | // same as the consumer, so the lock only guards against the defensive case | ||
| 537 | // of a callback landing on another processCurls() call site. | ||
| 538 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | CurlManager::INSTANCE.addGet( |
| 539 | url, | ||
| 540 | ✗ | [this](int rc, const std::string& resp) { | |
| 541 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | std::lock_guard<std::mutex> g(m_pluginMutex); |
| 542 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
|
3 | if (rc == 0) { |
| 543 | ✗ | m_pluginPrep = PluginPrep::Failed; | |
| 544 | } else { | ||
| 545 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | m_pluginItem = resp; |
| 546 | 3 | m_pluginPrep = PluginPrep::Ready; | |
| 547 | } | ||
| 548 | 3 | }, | |
| 549 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
3 | m_curlToken, m_curlToken); |
| 550 | 3 | } | |
| 551 | |||
| 552 | /* | ||
| 553 | * | ||
| 554 | */ | ||
| 555 | 8 | std::string PlaylistEntryDynamic::PluginCommandURL(const char* command) const { | |
| 556 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | std::string url = "http://"; |
| 557 |
5/14✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 8 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
|
8 | url += (m_pluginHost != "") ? m_pluginHost : "127.0.0.1"; |
| 558 |
3/6✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
|
8 | url += "/plugin.php?plugin=" + m_data + "&page=playlistCallback.php&nopage=1&command="; |
| 559 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | url += command; |
| 560 | 8 | return url; | |
| 561 | ✗ | } | |
| 562 | |||
| 563 | /* | ||
| 564 | * | ||
| 565 | */ | ||
| 566 | 28 | void PlaylistEntryDynamic::ClearPlaylistEntries(void) { | |
| 567 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 28 times.
|
41 | while (!m_playlistEntries.empty()) { |
| 568 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | delete m_playlistEntries.back(); |
| 569 | 13 | m_playlistEntries.pop_back(); | |
| 570 | } | ||
| 571 | |||
| 572 | 28 | m_currentEntry = -1; | |
| 573 | 28 | } | |
| 574 |