Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
Search.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
6#include <sstream>
7
8#include "Stroika/Foundation/Containers/Collection.h"
10#include "Stroika/Foundation/Execution/Activity.h"
11#include "Stroika/Foundation/Execution/Exceptions.h"
16#include "Stroika/Foundation/IO/Network/ConnectionlessSocket.h"
21
22#include "Search.h"
23
24using std::byte;
25
26using namespace Stroika::Foundation;
29using namespace Stroika::Foundation::Execution;
30using namespace Stroika::Foundation::IO;
32
33using Memory::MakeSharedPtr;
34
35using namespace Stroika::Frameworks;
36using namespace Stroika::Frameworks::UPnP;
37using namespace Stroika::Frameworks::UPnP::SSDP;
38using namespace Stroika::Frameworks::UPnP::SSDP::Client;
39
40// Comment this in to turn on tracing in this module
41//#define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
42
43class Search::Rep_ final {
44public:
45 Rep_ (const Options& options)
46 : fInterfaceFilter_{options.fInterfaces}
47 {
48 static constexpr Activity kConstructingSSDPSearcher_{"constructing SSDP searcher"sv};
49 DeclareActivity activity{&kConstructingSSDPSearcher_};
50 if (InternetProtocol::IP::SupportIPV4 (options.fIPVersion)) {
51 ConnectionlessSocket::Ptr s = ConnectionlessSocket::New (SocketAddress::INET, Socket::DGRAM);
52 fSockets_.Add (s);
53 }
54 if (InternetProtocol::IP::SupportIPV6 (options.fIPVersion)) {
55 ConnectionlessSocket::Ptr s = ConnectionlessSocket::New (SocketAddress::INET6, Socket::DGRAM);
56 fSockets_.Add (s);
57 }
58 for (ConnectionlessSocket::Ptr cs : fSockets_) {
59 cs.SetReceivePacketInfo (true); // for Advertisement::fReceivedOn
60 cs.SetMulticastLoopMode (true); // possible should make this configurable
61 cs.SetMulticastTTL (options.fMulticastTTL);
62 }
63 if (options.fFollowNetworkChanges) {
64 fNetworkChanges_ = SSDP::Private_::FollowNetworkChanges ([this] () { SearchAgain_ (); });
65 }
66 }
67 ~Rep_ () = default;
68 CallbackID AddOnFoundCallback (const function<void (const SSDP::Advertisement& d)>& callOnFinds)
69 {
70 return fFoundCallbacks_.Add (callOnFinds);
71 }
72 void RemoveOnFoundCallback (CallbackID callOnFinds)
73 {
74 fFoundCallbacks_.Remove (callOnFinds);
75 }
76 void Start (const String& serviceType, const optional<Time::Duration>& autoRetryInterval)
77 {
78 [[maybe_unused]] lock_guard lifecycle{fLifecycleMutex_};
79 StartThread_ (serviceType, autoRetryInterval);
80 }
81 InterfacesByID GetNetworkInterfaces () const
82 {
83 return fSearchingOn_.load ();
84 }
85 void Stop ()
86 {
87 [[maybe_unused]] lock_guard lifecycle{fLifecycleMutex_};
88 if (fThread_ != nullptr) {
89 fThread_.AbortAndWaitForDone ();
90 }
91 fSearching_ = nullopt;
92 }
93 void StartThread_ (const String& serviceType, const optional<Time::Duration>& autoRetryInterval)
94 {
95 if (fThread_ != nullptr) {
96 fThread_.AbortAndWaitForDone ();
97 }
98 fSearching_ = make_pair (serviceType, autoRetryInterval);
99 fSearchAgain_ = false;
100 fThread_ = Thread::New ([this, serviceType, autoRetryInterval] () { DoRun_ (serviceType, autoRetryInterval); }, Thread::eAutoStart, "SSDP Searcher"sv);
101 }
102 // a network appeared: search there right away, rather than at the next retry - by starting the search over (it sends out
103 // of every interface as it starts). Without restarting the search thread, which may be in a slow callOnFinds: it starts
104 // over when it next waits for answers
105 void SearchAgain_ ()
106 {
107 fSearchAgain_ = true;
108 }
109 void DoRun_ (const String& serviceType, const optional<Time::Duration>& autoRetryInterval)
110 {
111 bool didFirstRetry = false; // because search unreliable/UDP, recommended to send two search requests
112 // a little bit apart even if addition to longer retry interval
113 // http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
114 Retry:
115 optional<Time::TimePointSeconds> retrySendAt;
116 if (not didFirstRetry) {
117 retrySendAt = Time::GetTickCount () + 2s;
118 didFirstRetry = true;
119 }
120 else if (autoRetryInterval.has_value ()) {
121 retrySendAt = Time::GetTickCount () + *autoRetryInterval;
122 }
123 const InterfacesByID candidates = SSDP::Private_::GetSSDPInterfaces (fInterfaceFilter_); // listed once for the round
124 InterfacesByID searchedOn;
125 for (ConnectionlessSocket::Ptr s : fSockets_) {
126#if USE_NOISY_TRACE_IN_THIS_MODULE_
127 Debug::TraceContextBumper ctx{"Sending M-SEARCH"sv};
128#endif
129 SocketAddress useSocketAddress = s.GetAddressFamily () == SocketAddress::INET ? SSDP::V4::kSocketAddress : SSDP::V6::kSocketAddress;
130 string request;
131 {
132 constexpr unsigned int kMX_ = 4; // how long a device may wait, at random, to answer: 1 to 5 seconds (UPnP Device Architecture 1.1, section 1.3.2)
133 stringstream requestBuf;
134 requestBuf << "M-SEARCH * HTTP/1.1\r\n"sv;
136 switch (s.GetAddressFamily ()) {
137 case SocketAddress::FamilyType::INET: {
138 return UniformResourceIdentification::Authority{SSDP::V4::kSocketAddress.GetInternetAddress (), useSocketAddress.GetPort ()};
139 } break;
140 case SocketAddress::FamilyType::INET6: {
141 return UniformResourceIdentification::Authority{SSDP::V6::kSocketAddress.GetInternetAddress (), useSocketAddress.GetPort ()};
142 } break;
143 default:
146 }
147 }();
148 requestBuf << "Host: "sv << hostAuthority.As<String> ().AsUTF8<string> () << "\r\n";
149 requestBuf << "Man: \"ssdp:discover\"\r\n"sv;
150 requestBuf << "ST: "sv << serviceType.AsUTF8<string> ().c_str () << "\r\n";
151 requestBuf << "MX: "sv << kMX_ << "\r\n";
152 requestBuf << "\r\n"sv;
153 request = requestBuf.str ();
154 }
155#if USE_NOISY_TRACE_IN_THIS_MODULE_
156 DbgTrace ("DETAILS: {}"_f, request);
157#endif
158 // out of each interface with an address of this socket's family - so devices on every network hear it (the answers
159 // all come back to this one socket)
160 const span<const byte> data{reinterpret_cast<const byte*> (request.c_str ()), request.length ()};
162 for (const Interface& i : candidates) {
163 if (not i.fBindings.fAddresses.Any ([&] (const InternetAddress& a) { return a.GetAddressFamily () == family; })) {
164 continue;
165 }
166 try {
168 s.SendTo (data, useSocketAddress);
169 searchedOn.Add (i);
170 }
171 catch (const Thread::AbortException&) {
172 ReThrow ();
173 }
174 catch (...) {
175 DbgTrace ("SSDP Search: could not send M-SEARCH on {}: {}"_f, i.fInterfaceID, current_exception ());
176 }
177 }
178 }
179
180 fSearchingOn_.store (searchedOn);
181
182 // only stopped by thread abort (which we PROBALY SHOULD FIX - ONLY SEARCH FOR CONFIRABLE TIMEOUT???)
183 WaitForIOReady<ConnectionlessSocket::Ptr> readyChecker{fSockets_};
184 while (1) {
185 if (fSearchAgain_.exchange (false)) {
186 didFirstRetry = false; // as at the start: two M-SEARCHes, a little apart
187 goto Retry;
188 }
189 Time::TimePointSeconds wakeAt = Time::GetTickCount () + kCheckForSearchAgainEvery_;
190 if (retrySendAt and *retrySendAt < wakeAt) {
191 wakeAt = *retrySendAt;
192 }
193 for (ConnectionlessSocket::Ptr s : readyChecker.WaitQuietlyUntil (wakeAt)) {
194 try {
195 byte buf[8 * 1024]; // not sure of max packet size
196 SocketAddress from;
197 optional<ConnectionlessSocket::PacketInfo> arrived;
198 size_t nBytesRead = s.ReceiveFrom (buf, 0, &from, &arrived).size ();
199 Assert (nBytesRead <= std::size (buf));
200 ReadPacketAndNotifyCallbacks_ (span{buf, nBytesRead}, SSDP::Private_::ReceivedOn (arrived, fSearchingOn_.load ()));
201 }
202 catch (const Thread::AbortException&) {
203 ReThrow ();
204 }
205 catch (...) {
206 // ignore errors - and keep on trucking
207 // but avoid wasting too much time if we get into an error storm
208 Execution::Sleep (1s);
209 }
210 }
211 if (retrySendAt and *retrySendAt < Time::GetTickCount ()) {
212 goto Retry;
213 }
214 }
215 }
216 void ReadPacketAndNotifyCallbacks_ (span<const byte> packet, const optional<Interface::SystemIDType>& receivedOn)
217 {
218 String headLine;
219 SSDP::Advertisement d;
220 SSDP::DeSerialize (Memory::BLOB{packet}, &headLine, &d);
221 d.fReceivedOn = receivedOn;
222#if USE_NOISY_TRACE_IN_THIS_MODULE_
223 Debug::TraceContextBumper ctx{"Read Reply"};
224 DbgTrace ("headLine: {}"_f, headLine);
225#endif
226 if (headLine.StartsWith ("HTTP/1.1 200"sv)) {
227 fFoundCallbacks_.Call (d);
228 }
229 }
230
231private:
232 Execution::CallbackRegistry<void (const SSDP::Advertisement&)> fFoundCallbacks_;
233 InterfaceFilter fInterfaceFilter_;
234 static constexpr Time::DurationSeconds kCheckForSearchAgainEvery_{1.0}; // how soon the search thread acts on SearchAgain_
235
236 mutex fLifecycleMutex_; // Start and Stop
237 atomic<bool> fSearchAgain_{false}; // set by SearchAgain_ (on its network-change thread), for the search thread
238 optional<pair<String, optional<Time::Duration>>> fSearching_; // the search started, and not stopped: serviceType, autoRetryInterval
240 Synchronized<InterfacesByID> fSearchingOn_; // what the last M-SEARCH went out of
241 Thread::CleanupPtr fThread_{Thread::CleanupPtr::eAbortBeforeWaiting};
242 optional<SSDP::Private_::NetworkChangeFollower> fNetworkChanges_; // last, so destroyed first: no SearchAgain_ while the rest goes away
243};
244
245/*
246 ********************************************************************************
247 ********************************** Search **************************************
248 ********************************************************************************
249 */
250const String Search::kSSDPAny = SSDP::kTarget_SSDPAll;
251const String Search::kRootDevice = "upnp:rootdevice"sv;
252
253Search::Search (const Options& options)
254 : fRep_{MakeSharedPtr<Rep_> (options)}
255{
256}
257
258Search::Search (const function<void (const SSDP::Advertisement& d)>& callOnFinds, const Options& options)
259 : Search{options}
260{
261 AddOnFoundCallback (callOnFinds);
262}
263
264Search::Search (const function<void (const SSDP::Advertisement& d)>& callOnFinds, const String& initialSearch, const Options& options)
265 : Search{callOnFinds, options}
266{
267 Start (initialSearch);
268}
269
270Search::Search (const function<void (const SSDP::Advertisement& d)>& callOnFinds, const String& initialSearch,
271 const optional<Time::Duration>& autoRetryInterval, const Options& options)
272 : Search{callOnFinds, options}
273{
274 Start (initialSearch, autoRetryInterval);
275}
276
277InterfacesByID Search::GetNetworkInterfaces () const
278{
279 return fRep_->GetNetworkInterfaces ();
280}
281
282Search::~Search ()
283{
284 if (fRep_ != nullptr) { // (moved from, it has none)
285 IgnoreExceptionsForCall (fRep_->Stop ());
286 }
287}
288
289auto Search::AddOnFoundCallback (const function<void (const SSDP::Advertisement& d)>& callOnFinds) -> CallbackID
290{
291 return fRep_->AddOnFoundCallback (callOnFinds);
292}
293
294void Search::RemoveOnFoundCallback (CallbackID callOnFinds)
295{
296 fRep_->RemoveOnFoundCallback (callOnFinds);
297}
298
299void Search::Start (const String& serviceType, const optional<Time::Duration>& autoRetryInterval)
300{
301 fRep_->Start (serviceType, autoRetryInterval);
302}
303
304void Search::Stop ()
305{
306 fRep_->Stop ();
307}
#define AssertNotReached()
Definition Assertions.h:358
auto MakeSharedPtr(ARGS_TYPE &&... args) -> shared_ptr< T >
same as make_shared, but if type T has block allocation, then use block allocation for the 'shared pa...
function< bool(const Foundation::IO::Network::Interface &i)> InterfaceFilter
Which network interfaces SSDP is to talk on - asked of each interface every time SSDP lists them,...
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
#define DbgTrace
Definition Trace.h:327
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual bool StartsWith(const Character &c, CompareOptions co=eWithCase) const
Definition String.cpp:1118
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>
nonvirtual bool Add(ArgByValueType< value_type > item)
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,...
nonvirtual void SendTo(span< const byte > data, const SocketAddress &sockAddr) const
nonvirtual void SetMulticastInterface(const Interface &i) const
nonvirtual span< byte > ReceiveFrom(span< byte > into, int flag, SocketAddress *fromAddress, Time::DurationSeconds timeout=Time::kInfinity) const
nonvirtual constexpr AddressFamily GetAddressFamily() const
nonvirtual SocketAddress::FamilyType GetAddressFamily() const
Definition Socket.inl:62
nonvirtual InternetAddress GetInternetAddress() const
Authority is roughly the part of a URL where you say the hostname (and portnumber etc) - part just af...
nonvirtual T As(optional< StringPCTEncodedFlag > pctEncode={}) const
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 ...
Ptr New(const function< void()> &fun2CallOnce, const optional< Characters::String > &name, const optional< Configuration > &configuration)
Definition Thread.cpp:887
void Sleep(Time::Duration seconds2Wait)
Definition Sleep.inl:101
ConnectionlessSocket::Ptr New(SocketAddress::FamilyType family, Type socketKind, const optional< IPPROTO > &protocol=nullopt)
bool SupportIPV4(IPVersionSupport flag)
Definition IP.inl:8