Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
CachingListener.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
6#include <algorithm>
7#include <memory>
8#include <mutex>
9
10#include "Stroika/Foundation/Containers/Mapping.h"
11#include "Stroika/Foundation/Containers/Sequence.h"
12#include "Stroika/Foundation/Execution/Exceptions.h"
17
19
20#include "CachingListener.h"
21
22using namespace Stroika::Foundation;
24using namespace Stroika::Foundation::Execution;
26
27using Memory::MakeSharedPtr;
28
29using namespace Stroika::Frameworks;
30using namespace Stroika::Frameworks::UPnP;
31using namespace Stroika::Frameworks::UPnP::SSDP;
32using namespace Stroika::Frameworks::UPnP::SSDP::Client;
33
34/*
35 ********************************************************************************
36 *************************** CachingListener::Rep_ ******************************
37 ********************************************************************************
38 */
39class CachingListener::Rep_ {
40private:
41 // where it is in force: each network it was heard on (nullopt: one not known), until its max-age runs out there
43 struct Entry_ {
44 SSDP::Advertisement fAdvertisement; // the last heard - fAlive true
45 HeardOn_ fHeardOn; // never empty
46 };
47 using Locations_ = Mapping<URI, Entry_>; // one USN's advertisements, by LOCATION
48
49public:
50 Rep_ (const Options& options)
51 : fOptions_{options}
52 , fListener_{[this] (const SSDP::Advertisement& a) { Heard_ (a, false); }, options.fListenerOptions}
53 {
54 if (options.fSearchFor) {
55 fSearch_.emplace ([this] (const SSDP::Advertisement& a) { Heard_ (a, true); },
56 Search::Options{.fIPVersion = options.fListenerOptions.fIPVersion,
57 .fMulticastTTL = options.fMulticastTTL,
58 .fInterfaces = options.fListenerOptions.fInterfaces,
59 .fFollowNetworkChanges = options.fListenerOptions.fFollowNetworkChanges});
60 }
61 }
62 ~Rep_ ()
63 {
64 IgnoreExceptionsForCall (Stop ()); // first: its expiry checks call Expire_, on this
65 }
66 CallbackID AddOnChangeCallback (const function<void (const SSDP::Advertisement& d)>& callOnChanges)
67 {
68 return fCallbacks_.Add (callOnChanges);
69 }
70 void RemoveOnChangeCallback (CallbackID callOnChanges)
71 {
72 fCallbacks_.Remove (callOnChanges);
73 }
74 InterfacesByID GetNetworkInterfaces () const
75 {
76 return fListener_.GetNetworkInterfaces ();
77 }
78 Collection<SSDP::Advertisement> GetAdvertisements () const
79 {
81 for (const KeyValuePair<String, Locations_>& usn : fCache_.load ()) {
82 for (const KeyValuePair<URI, Entry_>& i : usn.fValue) {
83 result += i.fValue.fAdvertisement;
84 }
85 }
86 return result;
87 }
88 void Start ()
89 {
90 [[maybe_unused]] lock_guard lifecycle{fLifecycleMutex_};
91 {
92 [[maybe_unused]] lock_guard noting{fNoting_};
93 Require (not fStarted_); // not already started
94 fStarted_ = true;
95 CheckExpiriesWhenDue_ (); // what it kept from before a Stop
96 }
97 fListener_.Start ();
98 if (fSearch_) {
99 fSearch_->Start (*fOptions_.fSearchFor, fOptions_.fSearchRepeatInterval);
100 }
101 }
102 void Stop ()
103 {
104 [[maybe_unused]] lock_guard lifecycle{fLifecycleMutex_};
105 if (fSearch_) {
106 fSearch_->Stop ();
107 }
108 fListener_.Stop ();
109 // last, so after any Heard_: the expiry checks - removed not holding fNoting_, as removing one waits for its call under
110 // way, an Expire_ that may be waiting for fNoting_ (then doing nothing, as stopped)
112 {
113 [[maybe_unused]] lock_guard noting{fNoting_};
114 fStarted_ = false;
115 checks = exchange (fChecks_, {});
116 fCheckAt_ = nullopt;
117 }
118 for (IntervalTimer::TimerID c : checks) {
119 IntervalTimer::Manager::sThe.Remove (c);
120 }
121 }
122
123private:
124 // on the listener's thread, or the searcher's: an ssdp:alive, or a search answer, adds its advertisement - or keeps it
125 // longer, on that network; an ssdp:byebye removes its device's every advertisement from that network
126 void Heard_ (const SSDP::Advertisement& heard, bool searchAnswer)
127 {
128 const String& usn = heard.fUSN;
129 if (usn.empty ()) {
130 return; // nothing to know it by
131 }
132 [[maybe_unused]] lock_guard noting{fNoting_};
133 if (searchAnswer or heard.fAlive == true) {
134 if (searchAnswer and MaybeSentBeforeByebye_ (heard)) {
135 return;
136 }
137 SSDP::Advertisement a = heard;
138 a.fAlive = true; // as a search answer does not say
139 Time::TimePointSeconds expiresAt = Time::GetTickCount () + a.fMaxAge.value_or (kDefaultMaxAge);
140 fSoonestExpiry_ = fSoonestExpiry_ ? min (*fSoonestExpiry_, expiresAt) : expiresAt;
141 CheckExpiriesWhenDue_ ();
142 bool added = false;
143 {
144 auto cache = fCache_.rwget ();
145 Locations_ locations = cache->LookupValue (usn);
146 optional<Entry_> e = locations.Lookup (a.fLocation);
147 added = not e.has_value ();
148 if (added) {
149 e.emplace ();
150 }
151 e->fAdvertisement = a;
152 e->fHeardOn.Add (a.fReceivedOn, expiresAt);
153 locations.Add (a.fLocation, *e);
154 cache->Add (usn, locations);
155 }
156 if (added) {
157 fCallbacks_.Call (a);
158 }
159 }
160 else if (heard.fAlive == false) {
161 // the whole device is gone: a device cannot withdraw one of its advertisements alone (UPnP Device Architecture 1.1,
162 // sections 1.2.2 and 2), so whichever its ssdp:byebye names, it withdraws them all. But only from the network it came on:
163 // a device on several networks can leave one, and stay on the rest (section 1.2.3). So it is removed from there - and
164 // from where it was heard on a network not known, which may be that one - or, the byebye's network not known, from all
165 const String device = DeviceOf_ (usn);
166 fByebyes_.Add (device, Time::GetTickCount () + kAnswerMaybeStaleFor_); // @see MaybeSentBeforeByebye_
168 {
169 auto cache = fCache_.rwget ();
171 cache->Where ([&] (const KeyValuePair<String, Locations_>& i) { return DeviceOf_ (i.fKey) == device; })) {
172 Locations_ keptHere;
173 for (const KeyValuePair<URI, Entry_>& l : u.fValue) {
174 Entry_ e = l.fValue;
175 if (heard.fReceivedOn) {
176 e.fHeardOn.RemoveIf (heard.fReceivedOn);
177 e.fHeardOn.RemoveIf (nullopt);
178 }
179 else {
180 e.fHeardOn.RemoveAll ();
181 }
182 if (e.fHeardOn.empty ()) {
183 gone += Gone_ (e);
184 }
185 else {
186 keptHere.Add (l.fKey, e);
187 }
188 }
189 if (keptHere.empty ()) {
190 cache->Remove (u.fKey);
191 }
192 else {
193 cache->Add (u.fKey, keptHere);
194 }
195 }
196 }
197 for (const SSDP::Advertisement& a : gone) {
198 fCallbacks_.Call (a);
199 }
200 }
201 // else an ssdp:update (UPnP 1.1), which neither adds nor removes
202 }
203 // on IntervalTimer's thread, as the soonest expiry is due (@see CheckExpiriesWhenDue_): removes what has expired
204 void Expire_ ()
205 {
206 [[maybe_unused]] lock_guard noting{fNoting_};
207 if (not fStarted_) {
208 return; // stopping: Stop removes the checks
209 }
210 // this check is done, and any other to come is no longer needed - CheckExpiriesWhenDue_ adds the next: removed, which here,
211 // on the timer thread, never waits
212 for (IntervalTimer::TimerID c : fChecks_) {
213 IntervalTimer::Manager::sThe.Remove (c);
214 }
215 fChecks_.RemoveAll ();
216 fCheckAt_ = nullopt;
217 Time::TimePointSeconds now = Time::GetTickCount ();
219 if (fSoonestExpiry_ and *fSoonestExpiry_ <= now) { // else nothing can have expired yet
221 fSoonestExpiry_ = nullopt; // what was soonest may have been heard again since: recompute
222 for (const KeyValuePair<String, Locations_>& usn : fCache_.load ()) {
223 Locations_ keptHere;
224 for (const KeyValuePair<URI, Entry_>& i : usn.fValue) {
225 Entry_ e = i.fValue;
226 e.fHeardOn.RemoveAll (
227 [&] (const KeyValuePair<optional<Interface::SystemIDType>, Time::TimePointSeconds>& h) { return h.fValue <= now; });
228 if (e.fHeardOn.empty ()) {
229 expired += Gone_ (e); // expired on every network
230 }
231 else {
232 keptHere.Add (i.fKey, e);
233 for (const KeyValuePair<optional<Interface::SystemIDType>, Time::TimePointSeconds>& h : e.fHeardOn) {
234 fSoonestExpiry_ = fSoonestExpiry_ ? min (*fSoonestExpiry_, h.fValue) : h.fValue;
235 }
236 }
237 }
238 if (not keptHere.empty ()) {
239 kept.Add (usn.fKey, keptHere);
240 }
241 }
242 fCache_.store (kept); // (also what expired on one network, kept for another)
243 }
244 CheckExpiriesWhenDue_ ();
245 for (const SSDP::Advertisement& a : expired) {
246 fCallbacks_.Call (a);
247 }
248 }
249 // holding fNoting_: that Expire_ is called as the soonest expiry is due - adding a one-shot timer, unless one is due by then.
250 // It never removes one: that waits for its call under way - an Expire_ that may be waiting for fNoting_ - so Expire_ removes
251 // those no longer needed (on the timer thread, where that never waits), and Stop the rest.
252 void CheckExpiriesWhenDue_ ()
253 {
254 if (fStarted_ and fSoonestExpiry_ and (not fCheckAt_ or *fSoonestExpiry_ < *fCheckAt_)) {
255 fCheckAt_ = *fSoonestExpiry_;
256 fChecks_ += IntervalTimer::Manager::sThe.AddOneShot ([this] () { Expire_ (); },
257 max (*fSoonestExpiry_ - Time::GetTickCount (), Time::DurationSeconds{0}));
258 }
259 }
260 // How long after a device's ssdp:byebye a search answer from it may have been sent before it. The scenario: we search; the
261 // device waits at random, up to the M-SEARCH's MX (Search's is 4 seconds), to answer - and before or after it does, shuts
262 // down and multicasts its byebye. Its answer comes in on the Search's socket and thread, the byebye on the Listener's, in no
263 // set order: UDP keeps none, and the Search's thread can fall behind reading on a busy network (seen by Tests/55 searching
264 // ssdp:all). So the answer can come after the byebye, and would add back a device that is gone - for its max-age, often
265 // 1800 seconds. Generous, as being wrong costs little: a device back within it, found only by a search answer, is added
266 // at its next ssdp:alive or search - and one coming back sends an ssdp:alive first. UPnP 1.1's BOOTID.UPNP.ORG would tell
267 // the two apart exactly, where both carry it: @see https://github.com/SophistSolutions/Stroika/issues/1211
268 static constexpr Time::DurationSeconds kAnswerMaybeStaleFor_{10.0};
269 // holding fNoting_: is this search answer one its device may have sent before an ssdp:byebye since (@see
270 // kAnswerMaybeStaleFor_)? Then it may only keep what is still held - the same USN, at that LOCATION, heard on that
271 // network - longer: a device on several networks that left one is still kept on the rest; nothing is added back
272 bool MaybeSentBeforeByebye_ (const SSDP::Advertisement& answer)
273 {
274 Time::TimePointSeconds now = Time::GetTickCount ();
275 fByebyes_.RemoveAll ([&] (const KeyValuePair<String, Time::TimePointSeconds>& b) { return b.fValue <= now; });
276 if (not fByebyes_.ContainsKey (DeviceOf_ (answer.fUSN))) {
277 return false;
278 }
279 optional<Entry_> held = fCache_.cget ()->LookupValue (answer.fUSN).Lookup (answer.fLocation);
280 return not held or not held->fHeardOn.ContainsKey (answer.fReceivedOn);
281 }
282 // the device a USN names - its "uuid:device-UUID", before any "::" (UPnP Device Architecture 1.1, section 1.2.2) - so an
283 // embedded device, with a UUID of its own, is a device of its own
284 static String DeviceOf_ (const String& usn)
285 {
286 optional<size_t> i = usn.Find ("::"sv);
287 return i ? usn.SubString (0, *i) : usn;
288 }
289 // what the callbacks are told of an advertisement removed: the last heard, fAlive false
290 static SSDP::Advertisement Gone_ (const Entry_& e)
291 {
292 SSDP::Advertisement a = e.fAdvertisement;
293 a.fAlive = false;
294 return a;
295 }
296
297private:
298 const Options fOptions_;
299 mutex fLifecycleMutex_; // Start and Stop
300 // one change at a time - made and told - on whichever thread sees it: so the callbacks are told of the changes in order
301 mutex fNoting_;
302 CallbackRegistry<void (const SSDP::Advertisement&)> fCallbacks_;
303 Synchronized<Mapping<String, Locations_>> fCache_; // by USN; changed only holding fNoting_
304 optional<Time::TimePointSeconds> fSoonestExpiry_; // (fNoting_) no entry expires before it - nullopt: none to
305 Sequence<IntervalTimer::TimerID> fChecks_; // (fNoting_) the one-shot timers still to call Expire_ - Stop removes them
306 optional<Time::TimePointSeconds> fCheckAt_; // (fNoting_) when the soonest of them is due - nullopt: none
307 Mapping<String, Time::TimePointSeconds> fByebyes_; // (fNoting_) by device: until when a search answer from it may predate its byebye
308 bool fStarted_{false}; // (fNoting_)
309 // last, so stopped first: they call Heard_, which uses the rest
310 Listener fListener_;
311 optional<Search> fSearch_;
312};
313
314/*
315 ********************************************************************************
316 ******************************** CachingListener *******************************
317 ********************************************************************************
318 */
319CachingListener::CachingListener (const Options& options)
320 : fRep_{MakeSharedPtr<Rep_> (options)}
321{
322}
323
324CachingListener::CachingListener (const Options& options, AutoStart)
325 : CachingListener{options}
326{
327 Start ();
328}
329
330CachingListener::CachingListener (const function<void (const SSDP::Advertisement& d)>& callOnChanges, const Options& options)
331 : CachingListener{options}
332{
333 AddOnChangeCallback (callOnChanges);
334}
335
336CachingListener::CachingListener (const function<void (const SSDP::Advertisement& d)>& callOnChanges, const Options& options, AutoStart)
337 : CachingListener{callOnChanges, options}
338{
339 Start ();
340}
341
342CachingListener::CachingListener (const function<void (const SSDP::Advertisement& d)>& callOnChanges, AutoStart)
343 : CachingListener{callOnChanges}
344{
345 Start ();
346}
347
348CachingListener::~CachingListener () = default; // Rep_ stops as it goes
349
350auto CachingListener::AddOnChangeCallback (const function<void (const SSDP::Advertisement& d)>& callOnChanges) -> CallbackID
351{
352 return fRep_->AddOnChangeCallback (callOnChanges);
353}
354
356{
357 fRep_->RemoveOnChangeCallback (callOnChanges);
358}
359
361{
362 return fRep_->GetNetworkInterfaces ();
363}
364
366{
367 return fRep_->GetAdvertisements ();
368}
369
371{
372 fRep_->Start ();
373}
374
376{
377 fRep_->Stop ();
378}
time_point< RealtimeClock, DurationSeconds > TimePointSeconds
TimePointSeconds is a simpler approach to chrono::time_point, which doesn't require using templates e...
Definition Realtime.h:82
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual String SubString(SZ from) const
nonvirtual optional< size_t > Find(Character c, CompareOptions co=eWithCase) const
Definition String.inl:687
A Collection<T> is a container to manage an un-ordered collection of items, without equality defined ...
a cross between Mapping<KEY, T> and Collection<T> and Set<T>
A Mapping uniquely associates two elements: a key and a value (use Assocation to allow multiple value...
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:186
A generalization of a vector: a container whose elements are keyed by the natural numbers.
The callbacks an object calls when something happens - each added, and removed by the ID Add gave it ...
Wrap any object with Synchronized<> and it can be used similarly to the base type,...
A Listener that remembers: the SSDP advertisements in force around it - each kept from its ssdp:alive...
nonvirtual CallbackID AddOnChangeCallback(const function< void(const SSDP::Advertisement &d)> &callOnChanges)
Call callOnChanges too, with each change from now on - what it holds already, GetAdvertisements () re...
CachingListener(const Options &options=kDefaultOptions)
nonvirtual Containers::Collection< SSDP::Advertisement > GetAdvertisements() const
The advertisements in force: each added, and not yet removed - fAlive true. Safe to call from any thr...
nonvirtual IO::Network::InterfacesByID GetNetworkInterfaces() const
The network interfaces it is listening on now (.
nonvirtual void RemoveOnChangeCallback(CallbackID callOnChanges)
Once this returns, that callback is not running, and is never called again - except that,...
Listener::CallbackID CallbackID
Names a callback AddOnChangeCallback added, for RemoveOnChangeCallback - Listener's.
CONTAINER::value_type * Start(CONTAINER &c)
For a contiguous container (such as a vector or basic_string) - find the pointer to the start of the ...
IO::Network::InternetProtocol::IP::IPVersionSupport fIPVersion
Definition Search.h:40