Xclox
C++11 header-only cross-platform date and time library with an asynchronous NTP client
query.hpp
1 /*
2  * Copyright (c) 2024 Abdullatif Kalla.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE.txt file in the root directory of this source tree.
6  */
7 
8 #ifndef XCLOX_QUERY_HPP
9 #define XCLOX_QUERY_HPP
10 
11 #include "query_series.hpp"
12 
13 namespace xclox {
14 
15 namespace ntp {
16 
17  namespace internal {
18 
19  std::string getHost(const std::string& server)
20  {
21  return server.find(':') == std::string::npos ? server : server.substr(0, server.find(':'));
22  }
23 
24  std::string getPort(const std::string& server)
25  {
26  return server.find(':') == std::string::npos ? "123" : server.substr(server.find(':') + 1);
27  }
28 
29  auto stringify = [](const asio::ip::udp::endpoint& endpoint) {
30  std::stringstream ss;
31  ss << endpoint;
32  return ss.str();
33  };
34 
35  } // namespace internal
36 
44  class Query : public std::enable_shared_from_this<Query> {
45  public:
55  enum class Status : uint8_t {
56  ResolveError = 1,
57  SendError = 2,
58  ReceiveError = 4,
59  TimeoutError = 8,
60  Cancelled = 16,
61  Succeeded = 32
62  };
63 
64  using Callback = std::function<void(const std::string&, const std::string&, Status, const Packet&, const std::chrono::steady_clock::duration&)>;
65  using DefaultTimeout = internal::DefaultTimeout<QuerySingle, 5000>;
66 
68 
70  explicit Query(const std::string& server, Callback callback)
71  : m_server(server)
72  , m_callback(callback)
73  , m_timer(m_io)
74  , m_resolver(m_io)
75  , m_finalized(false)
76  {
77  }
78 
87  static std::weak_ptr<Query> start(asio::thread_pool& pool, const std::string& server, Callback callback, const std::chrono::milliseconds& timeout = std::chrono::milliseconds(DefaultTimeout::ms))
88  {
89  if (!callback) {
90  return {};
91  }
92  auto query = std::make_shared<Query>(server, callback);
93  query->m_timer.expires_after(timeout);
94  query->m_timer.async_wait([self = std::weak_ptr<Query>(query->shared_from_this())](const asio::error_code& error) {
95  if (error != asio::error::operation_aborted) {
96  if (auto query = self.lock()) {
97  query->m_io.stop();
98  query->finalize(query->m_server, "", Status::TimeoutError, Packet(), std::chrono::seconds(0));
99  }
100  }
101  });
102  query->m_resolver.async_resolve(
103  internal::getHost(server),
104  internal::getPort(server),
105  [query = std::weak_ptr<Query>(query->shared_from_this())](
106  const asio::error_code& error,
107  const asio::ip::udp::resolver::results_type& endpoints) {
108  auto self = query.lock();
109  if (!self) {
110  return;
111  }
112  if (error) {
113  self->m_timer.cancel();
114  self->finalize(self->m_server, "", Status::ResolveError, Packet(), std::chrono::seconds(0));
115  return;
116  }
117  self->m_subquery = QuerySeries::start(
118  self->m_io,
119  endpoints,
120  [query = std::weak_ptr<Query>(self->shared_from_this())](
121  const asio::ip::udp::endpoint& endpoint,
122  const asio::error_code& error,
123  const Packet& packet,
124  const std::chrono::steady_clock::duration& rtt) {
125  auto self = query.lock();
126  if (!self) {
127  return;
128  }
129  self->m_timer.cancel();
130  self->finalize(
131  self->m_server,
132  internal::stringify(endpoint),
133  error ? (packet.isNull() ? Status::ReceiveError : Status::SendError) : Status::Succeeded,
134  packet,
135  rtt);
136  });
137  });
138  asio::post(pool, [self = query] {
139  self->m_io.run();
140  });
141  return query;
142  }
143 
145  void cancel()
146  {
147  asio::dispatch(m_io, [query = std::weak_ptr<Query>(shared_from_this())] {
148  auto self = query.lock();
149  if (self && !self->m_io.stopped()) {
150  self->m_io.stop();
151  self->finalize(self->m_server, "", Status::Cancelled, Packet(), std::chrono::seconds(0));
152  }
153  });
154  }
155 
156  private:
157  void finalize(const std::string& name, const std::string& address, Status status, const Packet& packet, const std::chrono::steady_clock::duration& rtt)
158  {
159  if (!m_finalized) {
160  m_callback(name, address, status, packet, rtt);
161  m_finalized = true;
162  }
163  }
164 
165  std::string m_server;
166  Callback m_callback;
167  asio::io_context m_io;
168  asio::steady_timer m_timer;
169  asio::ip::udp::resolver m_resolver;
170  std::weak_ptr<QuerySeries> m_subquery;
171  bool m_finalized;
172  };
173 
174 } // namespace ntp
175 
176 } // namespace xclox
177 
178 #endif // XCLOX_QUERY_HPP
Packet is an immutable raw NTP packet.
Definition: packet.hpp:54
static std::weak_ptr< QuerySeries > start(asio::io_context &io, const asio::ip::udp::resolver::results_type &endpoints, Callback callback, const std::chrono::milliseconds &timeout=std::chrono::milliseconds(DefaultTimeout::ms))
Starts querying the given endpoints one at a time until success or all endpoints are queried.
Definition: query_series.hpp:52
Query is an ephemeral class representing a NTP query from start to end.
Definition: query.hpp:44
internal::DefaultTimeout< QuerySingle, 5000 > DefaultTimeout
Type of query timeout milliseconds holder.
Definition: query.hpp:65
void cancel()
Cancels the query reporting Query::Status::Cancelled to the caller.
Definition: query.hpp:145
std::function< void(const std::string &, const std::string &, Status, const Packet &, const std::chrono::steady_clock::duration &)> Callback
Type of query callback.
Definition: query.hpp:64
Query(const std::string &server, Callback callback)
Constructs a NTP query that targets server and uses callback for reporting back its result.
Definition: query.hpp:70
static std::weak_ptr< Query > start(asio::thread_pool &pool, const std::string &server, Callback callback, const std::chrono::milliseconds &timeout=std::chrono::milliseconds(DefaultTimeout::ms))
Starts querying all resolved addresses of server one at a time until success.
Definition: query.hpp:87
Status
Type of query status.
Definition: query.hpp:55
@ ResolveError
The server domain name is not resolved.
@ ReceiveError
The server packet is not received by the client.
@ TimeoutError
The query timed out while waiting for the server's packet.
@ Succeeded
The client received the server's packet successfully.
@ Cancelled
The client cancelled the query.
@ SendError
The client packet is not sent to the server.