Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
CachingListener.h
Go to the documentation of this file.
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#ifndef _Stroika_Frameworks_UPnP_SSDP_Client_CachingListener_h_
5#define _Stroika_Frameworks_UPnP_SSDP_Client_CachingListener_h_ 1
6
7#include "Stroika/Frameworks/StroikaPreComp.h"
8
9#include <functional>
10#include <optional>
11
12#include "Stroika/Foundation/Containers/Collection.h"
14
17
18/**
19 * \file
20 *
21 * \note Code-Status: <a href="Code-Status.md#Alpha">Alpha</a>
22 */
23
24namespace Stroika::Frameworks::UPnP::SSDP::Client {
25
26 /**
27 * \brief A Listener that remembers: the SSDP advertisements in force around it - each kept from its ssdp:alive (or answer to
28 * its search) until its max-age runs out, or an ssdp:byebye withdraws its device.
29 *
30 * Its callbacks are told of each change to what it holds - not of each advertisement heard, as a Listener's are - with the
31 * Advertisement changed: fAlive true when it is added - heard for the first time, or the first since it was removed; false
32 * when it is removed - by an ssdp:byebye, or its max-age running out (so also for a device that went without saying so) -
33 * as last heard, LOCATION and all (a byebye has none). One it holds, heard again, calls nothing: it is only kept longer.
34 *
35 * So it says what is around in two ways - callbacks, and GetAdvertisements () - for different uses:
36 * o callbacks alone, to keep a model of one's own: fetching each device's description once, say, as it is added.
37 * o GetAdvertisements () alone, to ask what is there when wanted: after a search (the second example), or to show a
38 * list now and then.
39 * o both, for a part attaching once it is running - what is there now, then each change (the third example) - or for
40 * a callback that needs the whole picture, not just the change ("was that the last device of its type?"): from
41 * within a callback, GetAdvertisements () has the change it is told of made.
42 *
43 * It listens (a Listener) and - given Options::fSearchFor - searches (a Search), so what is already there is found at once,
44 * not just at its next NOTIFY: a device need re-announce only within half its max-age, which the UPnP Device Architecture
45 * says SHOULD be at least 1800 seconds.
46 *
47 * An advertisement is a USN at a LOCATION: a device on several networks advertises each USN with each network's own
48 * LOCATION (as BasicServer does), and each of those is added, and expires, on its own. An ssdp:byebye removes its whole
49 * device - every USN with its "uuid:device-UUID" (not an embedded device's: that has a UUID of its own), at every LOCATION -
50 * whichever USN it names: a device cannot withdraw one of its advertisements alone (UPnP Device Architecture 1.1, sections
51 * 1.2.2 and 2). But only on the network it arrived on (Advertisement::fReceivedOn): a device on several networks can leave
52 * one and stay on the others (section 1.2.3). So an advertisement heard on several networks is kept while it is in force on
53 * any - heard there within its max-age, and not withdrawn there. Every advertisement heard is kept, whatever
54 * Options::fSearchFor - filter in the callback, or what GetAdvertisements () returns.
55 *
56 * \note THREADS: callbacks run on whichever thread sees the change - the listener's, the searcher's, or IntervalTimer's for
57 * an expiry - one at a time, in the order of the changes they report. A callback may call GetAdvertisements (),
58 * AddOnChangeCallback () and RemoveOnChangeCallback (). An exception from one is logged and ignored. Keep them quick:
59 * while one runs, what is heard waits, and so do Stop () and destruction - and IntervalTimer's thread is shared with
60 * every other timer. Hand slow work - fetching the device description, say - to another thread.
61 *
62 * \note Needs an IntervalTimer::Manager::Activator, as BasicServer does: while started, it looks for what has expired
63 * as the soonest expiry is due - not polling - on IntervalTimer's thread.
64 *
65 * \note A search answer can come in after the ssdp:byebye its device sent later: a device waits up to the M-SEARCH's MX
66 * to answer, and the answer and the byebye come in on different sockets and threads - the Search's and the
67 * Listener's - in no set order (UDP keeps none, and the Search's thread can fall behind on a busy network). So for a
68 * while after a device's byebye, a search answer from it may keep what is still held of it longer, but adds nothing
69 * back - which it otherwise would, until its max-age runs out. An ssdp:alive always adds: a device coming back says so.
70 * @see https://github.com/SophistSolutions/Stroika/issues/1211 (telling them apart exactly, by BOOTID.UPNP.ORG)
71 *
72 * \par Example Usage
73 * \code
74 * CachingListener devices{[] (const SSDP::Advertisement& a) {
75 * // a.fAlive: added (true) or removed (false) - a.fUSN, a.fLocation ... say what
76 * },
77 * CachingListener::eAutoStart};
78 * \endcode
79 *
80 * \par Example Usage
81 * \code
82 * // a search that returns what it found: the M-SEARCH's MX is 4, so devices answer within 4 seconds
83 * CachingListener found{CachingListener::Options{.fSearchFor = SSDP::kTarget_UPNPRootDevice}, CachingListener::eAutoStart};
84 * Execution::Sleep (5s);
85 * for (const SSDP::Advertisement& a : found.GetAdvertisements ()) {
86 * ...
87 * }
88 * \endcode
89 *
90 * \par Example Usage
91 * \code
92 * // attaching once it is running: what is there now, then each change. In this order: the other way round, a change
93 * // between the two is missed. And holding the lock the callback takes, so it applies each change after what
94 * // GetAdvertisements () returned - else that older list could add back what a removal just removed. A change may
95 * // repeat that list - one added that it has, or removed that it lacks - so Apply_ must make a repeat change nothing:
96 * // by USN and LOCATION, add or replace, or remove if there.
97 * lock_guard l{fMutex_};
98 * fCallbackID = cache.AddOnChangeCallback ([this] (const SSDP::Advertisement& a) {
99 * lock_guard l{fMutex_};
100 * Apply_ (a);
101 * });
102 * for (const SSDP::Advertisement& a : cache.GetAdvertisements ()) {
103 * Apply_ (a);
104 * }
105 * ...
106 * cache.RemoveOnChangeCallback (fCallbackID); // later - not holding fMutex_: this waits for a running callback
107 * \endcode
108 */
110 public:
111 using AutoStart = Listener::AutoStart;
112 static constexpr AutoStart eAutoStart{Listener::eAutoStart};
113
114 public:
115 /**
116 * \par Example Usage
117 * \code
118 * CachingListener cache{callOnChanges, CachingListener::Options{.fListenerOptions = {.fIPVersion = IPVersionSupport::eIPV4Only}, .fSearchFor = nullopt}, CachingListener::eAutoStart};
119 * \endcode
120 */
121 struct Options {
122 /**
123 * How it listens - and searches, given fSearchFor: the same SSDP channels, network interfaces, and following of
124 * network changes (with its searches' TTL, fMulticastTTL).
125 */
127
128 /**
129 * What to search for, as it starts (and when a network appears): by default ssdp:all, which devices answer with each
130 * advertisement they have - so what is cached soon matches what their NOTIFYs would give. nullopt: only listen.
131 */
132 optional<String> fSearchFor{"ssdp:all"sv};
133
134 /**
135 * Search again this often, too (@see Search::Start's autoRetryInterval). nullopt (the default): search only as it
136 * starts - twice, 2 seconds apart, as UDP can lose one - and when a network appears. An interval under a device's
137 * max-age keeps one that sends no NOTIFYs (some only answer searches) cached.
138 */
139 optional<Foundation::Time::Duration> fSearchRepeatInterval;
140
141 /**
142 * The TTL (IPv6: hop limit) of each M-SEARCH, given fSearchFor: how many routers it may cross (@see
143 * Search::Options::fMulticastTTL).
144 */
146 };
147 static const Options kDefaultOptions;
148
149 public:
150 /**
151 * Listen - and search, given options.fSearchFor - calling callOnChanges, and any callback added with AddOnChangeCallback,
152 * with each change. Listening starts with Start (), or right away given eAutoStart.
153 *
154 * \note THREADS: callOnChanges is called on another thread, not the caller's (@see CachingListener).
155 */
156 CachingListener (const Options& options = kDefaultOptions);
157 CachingListener (const Options& options, AutoStart); ///< \brief ... and Start () - for GetAdvertisements (), with no callback
158 CachingListener (const function<void (const SSDP::Advertisement& d)>& callOnChanges, const Options& options = kDefaultOptions); ///< \brief callOnChanges: called on another thread
159 CachingListener (const function<void (const SSDP::Advertisement& d)>& callOnChanges, const Options& options,
160 AutoStart); ///< \brief ... and Start (); callOnChanges: called on another thread
161 CachingListener (const function<void (const SSDP::Advertisement& d)>& callOnChanges, AutoStart); ///< \brief ... and Start (); callOnChanges: called on another thread
162 CachingListener (CachingListener&&) = default;
163 CachingListener (const CachingListener&) = delete;
164
165 public:
166 /**
167 * Its OK to destroy it while running: it stops - waiting for a callback running.
168 *
169 * \pre not from within a callback (that would wait for itself)
170 */
172
173 public:
174 nonvirtual CachingListener& operator= (const CachingListener&) = delete;
175
176 public:
177 /**
178 * \brief Names a callback AddOnChangeCallback added, for RemoveOnChangeCallback - Listener's.
179 */
181
182 public:
183 /**
184 * \brief Call callOnChanges too, with each change from now on - what it holds already, GetAdvertisements () returns (both,
185 * missing nothing: @see CachingListener's third example). From any thread, also once started - and from within a
186 * callback; it never waits for a running one.
187 *
188 * \note THREADS: callOnChanges is called on another thread, not the caller's (@see CachingListener) - so it must be thread-safe.
189 */
190 nonvirtual CallbackID AddOnChangeCallback (const function<void (const SSDP::Advertisement& d)>& callOnChanges);
191
192 public:
193 /**
194 * \brief Once this returns, that callback is not running, and is never called again - except that, called from within it,
195 * the call already under way finishes. So it waits for that callback, running on another thread: do not call it
196 * holding a lock that callback takes. Removing one not added (or already removed) does nothing.
197 */
198 nonvirtual void RemoveOnChangeCallback (CallbackID callOnChanges);
199
200 public:
201 /**
202 * \brief The network interfaces it is listening on now (@see Listener::GetNetworkInterfaces). Safe to call from any thread.
203 */
205
206 public:
207 /**
208 * \brief The advertisements in force: each added, and not yet removed - fAlive true. Safe to call from any thread.
209 */
211
212 public:
213 /**
214 * Starts listening (and searching) - on the listener's thread (and the searcher's) - and looking, on IntervalTimer's
215 * thread as each expiry is due, for what has expired.
216 *
217 * \pre not already started.
218 * \pre an IntervalTimer::Manager::Activator exists - until it is stopped
219 */
220 nonvirtual void Start ();
221
222 public:
223 /**
224 * Stops listening, searching, and looking for what has expired. What it has kept, it keeps - and removes, if it has
225 * expired by then, once started again.
226 * Not an error to call if not started (just does nothing). This waits for a callback running.
227 *
228 * \pre not from within a callback (that would wait for itself)
229 */
230 nonvirtual void Stop ();
231
232 private:
233 class Rep_;
234 shared_ptr<Rep_> fRep_;
235 };
236
237}
238
239/*
240 ********************************************************************************
241 ***************************** Implementation Details ***************************
242 ********************************************************************************
243 */
244#include "CachingListener.inl"
245
246#endif /*_Stroika_Frameworks_UPnP_SSDP_Client_CachingListener_h_*/
constexpr uint8_t kDefaultMulticastTTL
The TTL (IPv6: hop limit) multicast SSDP packets - NOTIFYs and M-SEARCHes - go out with,...
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 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...
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.
Execution::CallbackRegistry< void(const SSDP::Advertisement &)>::ID CallbackID
Names a callback AddOnFoundCallback added, for RemoveOnFoundCallback.