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