#include "MessageClient.hpp" #include #include #include #include #include #include #include #include #include "DriftingDateTime.h" #include "MessageError.hpp" #include "pimpl_impl.hpp" #include "moc_MessageClient.cpp" /******************************************************************************/ // Constants /******************************************************************************/ namespace { constexpr auto PING_INTERVAL = std::chrono::seconds(15); } /******************************************************************************/ // Message Parsing /******************************************************************************/ namespace { // Exception thrown on message parsing errors. struct parse_error : public std::system_error { using std::system_error::system_error; explicit parse_error(QJsonParseError const & parse) : parse_error(MessageError::Code::json_parsing_error, parse.errorString().toStdString()) {} }; // Parse and return the provided datagram as a Message object; throw // if parsing failed. Message parse_message(QByteArray const & datagram) { using MessageError::Code; QJsonParseError parse; QJsonDocument document = QJsonDocument::fromJson(datagram, &parse); if (parse.error) throw parse_error(parse); if (!document.isObject()) throw parse_error(Code::json_not_an_object); Message message; message.read(document.object()); return message; } } /******************************************************************************/ // Private Implementation /******************************************************************************/ class MessageClient::impl final : public QUdpSocket { Q_OBJECT public: // Constructor impl(quint16 const port, MessageClient * self) : self_ {self} , port_ {port} , ping_ {new QTimer {this}} { connect(ping_, &QTimer::timeout, this, &impl::ping); connect(this, &QIODevice::readyRead, this, [this]() { while (hasPendingDatagrams()) { QByteArray datagram(pendingDatagramSize(), Qt::Uninitialized); if (readDatagram(datagram.data(), datagram.size()) > 0) { try { Q_EMIT self_->message (parse_message(datagram)); } catch (std::exception const & e) { Q_EMIT self_->error (QString {"MessageClient exception: %1"}.arg(e.what())); } catch (...) { Q_EMIT self_->error (QString {"Unexpected exception in MessageClient"}); } } } }); ping_->start(PING_INTERVAL); bind(); } // Destructor ~impl() { if (port_ && !host_.isNull()) { emit_message(Message{"CLOSE"}.toJson()); } abort_host_lookup(); } // Send a ping message, if we have a valid port and host. void ping() { if (port_ && !host_.isNull()) { emit_message(Message{"PING", "", { {"NAME", QVariant(QApplication::applicationName())}, {"VERSION", QVariant(QApplication::applicationVersion())}, {"UTC", QVariant(DriftingDateTime::currentDateTimeUtc().toMSecsSinceEpoch())} }}.toJson()); } } // If the message isn't exactly the same as the one sent prior to this // one, emit it as a datagram and note it as the prior message sent. // // Caller is required to make the determination that our port and host // are valid prior to calling this function. void emit_message(QByteArray const & message) { if (message != priorMessage_) { writeDatagram(message, host_, port_); priorMessage_ = message; } } // If we've got a port, i.e., we're supposed to send messages, then queue // the message for later transmission if we don't have a host yet; attempt // to send it immediately if we've got a host. // // The message will be dropped on the floor if we don't have a port defined // or the message duplicates the last one sent. void send_message(QByteArray const & message) { if (port_) { if (host_.isNull()) messageQueue_.enqueue(message); else emit_message(message); } } // If we've got a host lookup in flight, but not yet completed, abort it // and indicate that we no longer have one in flight. void abort_host_lookup() { if (hostLookupId_ != -1) { QHostInfo::abortHostLookup(hostLookupId_); hostLookupId_ = -1; } } // Abort any current host lookup that might be in flight, and start a new // host lookup for the provided server name, noting that we have a lookup // in flight. // // If, at the time of host lookup completion, we find ourselves to be the // active host lookup, and we were able to look up addresses, then use the // first address associated with the server as our host address, and send // a ping. // // No matter the result of the host lookup, we're going to drain the queue, // either via sending messages if the host lookup worked, or by clearing it // if the lookup failed. void queue_host_lookup(QString const & name) { abort_host_lookup(); hostLookupId_ = QHostInfo::lookupHost(name, this, [this](QHostInfo const & info) { // This functor is always called in the context of the thread that // made the call to lookupHost(), so we're safe to modify anything // that we were safe to modify outside. if (info.lookupId() == hostLookupId_) { hostLookupId_ = -1; if (auto const & list = info.addresses(); !list.isEmpty()) { host_ = list.first(); qDebug() << "MessageClient Host:" << host_.toString() << "loopback:" << host_.isLoopback() << "multicast:" << host_.isMulticast(); ping(); while (messageQueue_.size()) { send_message(messageQueue_.dequeue()); } } else { Q_EMIT self_->error (QString {"UDP server lookup failed: %1"}.arg(info.errorString())); messageQueue_.clear(); } } }); } // Data members MessageClient * self_; quint16 port_; QTimer * ping_; QHostAddress host_; int hostLookupId_ = -1; QQueue messageQueue_; QByteArray priorMessage_; }; /******************************************************************************/ // Implementation /******************************************************************************/ #include "MessageClient.moc" MessageClient::MessageClient(QString const & name, quint16 const port, QObject * parent) : QObject {parent} , m_ {port, this} { connect(&*m_, &impl::errorOccurred, [this](impl::SocketError e) { #if defined (Q_OS_WIN) // Remove this when Qt stops doing this spuriously. if (e != impl::NetworkError && e != impl::ConnectionRefusedError) #else Q_UNUSED (e); #endif { Q_EMIT error (m_->errorString()); } }); set_server_name(name); } QHostAddress MessageClient::server_host() const { return m_->host_; } quint16 MessageClient::server_port() const { return m_->port_; } void MessageClient::set_server_name(QString const & name) { m_->host_.clear(); if (name.isEmpty()) m_->abort_host_lookup(); else m_->queue_host_lookup(name); } void MessageClient::set_server_port(quint16 const port) { m_->port_ = port; } void MessageClient::send(Message const & message) { m_->send_message(message.toJson()); } void MessageClient::send_raw_datagram(QByteArray const & message, QHostAddress const & host, quint16 const port) { if (port && !host.isNull()) { m_->writeDatagram(message, host, port); } } /******************************************************************************/