Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
Search.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. 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"
15#include "Stroika/Foundation/IO/Network/ConnectionlessSocket.h"
19
20#include "Search.h"
21
22using std::byte;
23
24using namespace Stroika::Foundation;
27using namespace Stroika::Foundation::IO;
29
30using namespace Stroika::Frameworks;
31using namespace Stroika::Frameworks::UPnP;
32using namespace Stroika::Frameworks::UPnP::SSDP;
33using namespace Stroika::Frameworks::UPnP::SSDP::Client;
34
35// Comment this in to turn on tracing in this module
36//#define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
37
38class Search::Rep_ {
39public:
41 {
42 static constexpr Execution::Activity kConstructingSSDPSearcher_{"constructing SSDP searcher"sv};
43 Execution::DeclareActivity activity{&kConstructingSSDPSearcher_};
44 if (InternetProtocol::IP::SupportIPV4 (ipVersion)) {
45 ConnectionlessSocket::Ptr s = ConnectionlessSocket::New (SocketAddress::INET, Socket::DGRAM);
46 fSockets_.Add (s);
47 }
48 if (InternetProtocol::IP::SupportIPV6 (ipVersion)) {
49 ConnectionlessSocket::Ptr s = ConnectionlessSocket::New (SocketAddress::INET6, Socket::DGRAM);
50 fSockets_.Add (s);
51 }
52 for (ConnectionlessSocket::Ptr cs : fSockets_) {
53 cs.SetMulticastLoopMode (true); // possible should make this configurable
54 }
55 }
56 ~Rep_ () = default;
57 void AddOnFoundCallback (const function<void (const SSDP::Advertisement& d)>& callOnFinds)
58 {
59 [[maybe_unused]] lock_guard critSec{fCritSection_};
60 fFoundCallbacks_.push_back (callOnFinds);
61 }
62 void Start (const String& serviceType, const optional<Time::Duration>& autoRetryInterval)
63 {
64 if (fThread_ != nullptr) {
65 fThread_.AbortAndWaitForDone ();
66 }
67 fThread_ = Execution::Thread::New ([this, serviceType, autoRetryInterval] () { DoRun_ (serviceType, autoRetryInterval); },
68 Execution::Thread::eAutoStart, "SSDP Searcher"sv);
69 }
70 void Stop ()
71 {
72 if (fThread_ != nullptr) {
73 fThread_.AbortAndWaitForDone ();
74 }
75 }
76 void DoRun_ (const String& serviceType, const optional<Time::Duration>& autoRetryInterval)
77 {
78 bool didFirstRetry = false; // because search unreliable/UDP, recommended to send two search requests
79 // a little bit apart even if addition to longer retry interval
80 // http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
81 Retry:
82 optional<Time::TimePointSeconds> retrySendAt;
83 if (not didFirstRetry) {
84 retrySendAt = Time::GetTickCount () + 2s;
85 didFirstRetry = true;
86 }
87 else if (autoRetryInterval.has_value ()) {
88 retrySendAt = Time::GetTickCount () + *autoRetryInterval;
89 }
90 for (ConnectionlessSocket::Ptr s : fSockets_) {
91#if USE_NOISY_TRACE_IN_THIS_MODULE_
92 Debug::TraceContextBumper ctx{"Sending M-SEARCH"sv};
93#endif
94 SocketAddress useSocketAddress = s.GetAddressFamily () == SocketAddress::INET ? SSDP::V4::kSocketAddress : SSDP::V6::kSocketAddress;
95 string request;
96 {
97 /*
98 * From http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0-20080424.pdf:
99 * To limit network congestion, the time-to-live (TTL) of each IP packet for each multicast
100 * message should default to 4 and should be configurable.
101 */
102 const unsigned int kMaxHops_ = 4;
103 stringstream requestBuf;
104 requestBuf << "M-SEARCH * HTTP/1.1\r\n"sv;
106 switch (s.GetAddressFamily ()) {
107 case SocketAddress::FamilyType::INET: {
108 return UniformResourceIdentification::Authority{SSDP::V4::kSocketAddress.GetInternetAddress (), useSocketAddress.GetPort ()};
109 } break;
110 case SocketAddress::FamilyType::INET6: {
111 return UniformResourceIdentification::Authority{SSDP::V6::kSocketAddress.GetInternetAddress (), useSocketAddress.GetPort ()};
112 } break;
113 default:
116 }
117 }();
118 requestBuf << "Host: "sv << hostAuthority.As<String> ().AsUTF8<string> () << "\r\n";
119 requestBuf << "Man: \"ssdp:discover\"\r\n"sv;
120 requestBuf << "ST: "sv << serviceType.AsUTF8<string> ().c_str () << "\r\n";
121 requestBuf << "MX: "sv << kMaxHops_ << "\r\n";
122 requestBuf << "\r\n"sv;
123 request = requestBuf.str ();
124 s.SetMulticastTTL (kMaxHops_);
125 }
126#if USE_NOISY_TRACE_IN_THIS_MODULE_
127 DbgTrace ("DETAILS: {}"_f, request);
128#endif
129 s.SendTo (span{reinterpret_cast<const byte*> (request.c_str ()), request.length ()}, useSocketAddress);
130 }
131
132 // only stopped by thread abort (which we PROBALY SHOULD FIX - ONLY SEARCH FOR CONFIRABLE TIMEOUT???)
134 while (1) {
135 for (ConnectionlessSocket::Ptr s : readyChecker.WaitQuietlyUntil (retrySendAt.value_or (Time::TimePointSeconds{Time::kInfinity}))) {
136 try {
137 byte buf[8 * 1024]; // not sure of max packet size
138 SocketAddress from;
139 size_t nBytesRead = s.ReceiveFrom (buf, 0, &from).size ();
140 Assert (nBytesRead <= Memory::NEltsOf (buf));
141 using namespace Streams;
142 ReadPacketAndNotifyCallbacks_ (BinaryToText::Reader::New (ExternallyOwnedSpanInputStream::New<byte> (span{buf, nBytesRead})));
143 }
144 catch (const Execution::Thread::AbortException&) {
146 }
147 catch (...) {
148 // ignore errors - and keep on trucking
149 // but avoid wasting too much time if we get into an error storm
150 Execution::Sleep (1s);
151 }
152 }
153 if (retrySendAt and *retrySendAt < Time::GetTickCount ()) {
154 goto Retry;
155 }
156 }
157 }
158 void ReadPacketAndNotifyCallbacks_ (const Streams::InputStream::Ptr<Character>& in)
159 {
160 String firstLine = in.ReadLine ().Trim ();
161
162#if USE_NOISY_TRACE_IN_THIS_MODULE_
163 Debug::TraceContextBumper ctx{"Read Reply"};
164 DbgTrace ("firstLine: {}"_f, firstLine);
165#endif
166
167 static const String kOKRESPONSELEAD_ = "HTTP/1.1 200"sv;
168 if (firstLine.length () >= kOKRESPONSELEAD_.length () and firstLine.SubString (0, kOKRESPONSELEAD_.length ()) == kOKRESPONSELEAD_) {
169 SSDP::Advertisement d;
170 while (true) {
171 String line = in.ReadLine ().Trim ();
172#if USE_NOISY_TRACE_IN_THIS_MODULE_
173 DbgTrace (L"reply-line: {}"_f, line);
174#endif
175 if (line.empty ()) {
176 break;
177 }
178
179 // Need to simplify this code (stroika string util)
180 if (optional<size_t> n = line.Find (':')) {
181 String label = line.SubString (0, *n);
182 String value = line.SubString (*n + 1).Trim ();
183 if (String::ThreeWayComparer{eCaseInsensitive}(label, "Location"sv) == 0) {
184 d.fLocation = IO::Network::URI{value};
185 }
186 else if (String::ThreeWayComparer{eCaseInsensitive}(label, "ST"sv) == 0) {
187 d.fTarget = value;
188 }
189 else if (String::ThreeWayComparer{eCaseInsensitive}(label, "USN"sv) == 0) {
190 d.fUSN = value;
191 }
192 else if (String::ThreeWayComparer{eCaseInsensitive}(label, "Server"sv) == 0) {
193 d.fServer = value;
194 }
195 }
196 }
197 {
198 // bad practice to keep mutex lock here - DEADLOCK CITY - find nice CLEAN way todo this...
199 [[maybe_unused]] lock_guard critSec{fCritSection_};
200 for (const auto& i : fFoundCallbacks_) {
201 i (d);
202 }
203 }
204 }
205 }
206
207private:
208 recursive_mutex fCritSection_;
209 vector<function<void (const SSDP::Advertisement& d)>> fFoundCallbacks_;
211 Execution::Thread::CleanupPtr fThread_{Execution::Thread::CleanupPtr::eAbortBeforeWaiting};
212};
213
214/*
215 ********************************************************************************
216 ********************************** Search **************************************
217 ********************************************************************************
218 */
219const String Search::kSSDPAny = "ssdp:any"sv;
220const String Search::kRootDevice = "upnp:rootdevice"sv;
221
223 : fRep_{make_shared<Rep_> (ipVersion)}
224{
225}
226
227Search::Search (const function<void (const SSDP::Advertisement& d)>& callOnFinds, IO::Network::InternetProtocol::IP::IPVersionSupport ipVersion)
228 : Search{ipVersion}
229{
230 AddOnFoundCallback (callOnFinds);
231}
232
233Search::Search (const function<void (const SSDP::Advertisement& d)>& callOnFinds, const String& initialSearch,
235 : Search{callOnFinds, ipVersion}
236{
237 Start (initialSearch);
238}
239
240Search::Search (const function<void (const SSDP::Advertisement& d)>& callOnFinds, const String& initialSearch,
241 const optional<Time::Duration>& autoRetryInterval, IO::Network::InternetProtocol::IP::IPVersionSupport ipVersion)
242 : Search{callOnFinds, ipVersion}
243{
244 Start (initialSearch, autoRetryInterval);
245}
246
247Search::~Search ()
248{
249 IgnoreExceptionsForCall (fRep_->Stop ());
250}
251
252void Search::AddOnFoundCallback (const function<void (const SSDP::Advertisement& d)>& callOnFinds)
253{
254 fRep_->AddOnFoundCallback (callOnFinds);
255}
256
257void Search::Start (const String& serviceType, const optional<Time::Duration>& autoRetryInterval)
258{
259 fRep_->Start (serviceType, autoRetryInterval);
260}
261
262void Search::Stop ()
263{
264 fRep_->Stop ();
265}
#define AssertNotReached()
Definition Assertions.h:355
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
constexpr DurationSeconds kInfinity
Definition Realtime.h:104
#define DbgTrace
Definition Trace.h:309
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual size_t length() const noexcept
Definition String.inl:1045
nonvirtual String SubString(SZ from) const
nonvirtual String Trim(bool(*shouldBeTrimmed)(Character)=Character::IsWhitespace) const
Definition String.cpp:1592
nonvirtual optional< size_t > Find(Character c, CompareOptions co=eWithCase) const
Definition String.inl:681
A Collection<T> is a container to manage an un-ordered collection of items, without equality defined ...
Definition Collection.h:102
nonvirtual void AbortAndWaitForDone(Time::DurationSeconds timeout=Time::kInfinity) const
Abort () the thread, and then WaitForDone () - but if doesn't finish fast enough, send extra aborts (...
Definition Thread.inl:291
nonvirtual void SendTo(span< const byte > data, const SocketAddress &sockAddr) const
nonvirtual span< byte > ReceiveFrom(span< byte > into, int flag, SocketAddress *fromAddress, Time::DurationSeconds timeout=Time::kInfinity) const
nonvirtual SocketAddress::FamilyType GetAddressFamily() const
Definition Socket.inl:62
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
InputStream<>::Ptr is Smart pointer (with abstract Rep) class defining the interface to reading from ...
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:955
void Sleep(Time::Duration seconds2Wait)
Definition Sleep.cpp:18
ConnectionlessSocket::Ptr New(SocketAddress::FamilyType family, Type socketKind, const optional< IPPROTO > &protocol=nullopt)
bool SupportIPV4(IPVersionSupport flag)
Definition IP.inl:8