js8call/MessageClient.cpp

302 lines
7.3 KiB
C++
Raw Normal View History

2018-02-08 21:28:33 -05:00
#include "MessageClient.hpp"
#include <QApplication>
2018-02-08 21:28:33 -05:00
#include <QByteArray>
#include <QHostAddress>
#include <QHostInfo>
#include <QQueue>
#include <QSet>
#include <QTimer>
#include <QUdpSocket>
2018-02-08 21:28:33 -05:00
#include "DriftingDateTime.h"
2024-11-03 06:20:05 -08:00
#include "MessageError.hpp"
2018-02-08 21:28:33 -05:00
#include "pimpl_impl.hpp"
#include "moc_MessageClient.cpp"
/******************************************************************************/
// Constants
/******************************************************************************/
namespace
{
2024-11-02 16:10:10 -07:00
constexpr auto PING_INTERVAL = std::chrono::seconds(15);
}
2024-11-02 22:10:28 -07:00
/******************************************************************************/
2024-11-03 06:27:13 -08:00
// Message Parsing
2024-11-02 22:10:28 -07:00
/******************************************************************************/
namespace
{
2024-11-03 06:20:05 -08:00
// Exception thrown on message parsing errors.
2024-11-02 22:10:28 -07:00
2024-11-03 06:20:05 -08:00
struct parse_error : public std::system_error
2024-11-02 22:10:28 -07:00
{
2024-11-03 06:20:05 -08:00
using std::system_error::system_error;
explicit parse_error(QJsonParseError const & parse)
: parse_error(MessageError::Code::json_parsing_error,
parse.errorString().toStdString())
2024-11-02 22:10:28 -07:00
{}
};
// Parse and return the provided datagram as a Message object; throw
// if parsing failed.
Message
2024-11-02 22:19:11 -07:00
parse_message(QByteArray const & datagram)
2024-11-02 22:10:28 -07:00
{
2024-11-03 06:20:05 -08:00
using MessageError::Code;
2024-11-02 22:10:28 -07:00
QJsonParseError parse;
QJsonDocument document = QJsonDocument::fromJson(datagram, &parse);
2024-11-03 06:20:05 -08:00
if (parse.error) throw parse_error(parse);
if (!document.isObject()) throw parse_error(Code::json_not_an_object);
2024-11-02 22:10:28 -07:00
Message message;
message.read(document.object());
return message;
}
}
/******************************************************************************/
// Private Implementation
/******************************************************************************/
class MessageClient::impl final : public QUdpSocket
2018-02-08 21:28:33 -05:00
{
Q_OBJECT
2018-02-08 21:28:33 -05:00
public:
// Constructor
2024-11-02 14:22:22 -07:00
impl(quint16 const port,
MessageClient * self)
2024-11-02 11:25:49 -07:00
: self_ {self}
, port_ {port}
, ping_ {new QTimer {this}}
2018-02-08 21:28:33 -05:00
{
2024-11-02 17:15:31 -07:00
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
{
2024-11-02 22:19:11 -07:00
Q_EMIT self_->message (parse_message(datagram));
2024-11-02 17:15:31 -07:00
}
catch (std::exception const & e)
{
Q_EMIT self_->error (QString {"MessageClient exception: %1"}.arg(e.what()));
}
catch (...)
{
2024-11-02 22:10:28 -07:00
Q_EMIT self_->error (QString {"Unexpected exception in MessageClient"});
2024-11-02 17:15:31 -07:00
}
}
}
});
2018-02-08 21:28:33 -05:00
2024-11-02 16:10:10 -07:00
ping_->start(PING_INTERVAL);
2018-02-08 21:28:33 -05:00
bind();
2018-02-08 21:28:33 -05:00
}
// Destructor
2024-11-02 14:22:22 -07:00
~impl()
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
if (port_ && !host_.isNull())
{
2024-11-02 11:25:49 -07:00
emit_message(Message{"CLOSE"}.toJson());
}
2018-02-08 21:28:33 -05:00
2024-11-02 11:25:49 -07:00
if (hostLookupId_ != -1)
{
2024-11-02 11:25:49 -07:00
QHostInfo::abortHostLookup(hostLookupId_);
}
2018-02-08 21:28:33 -05:00
}
2024-11-02 11:25:49 -07:00
// Send a ping message, if we have a valid port and host.
2018-02-08 21:28:33 -05:00
void
2024-11-02 11:25:49 -07:00
ping()
{
2024-11-02 11:25:49 -07:00
if (port_ && !host_.isNull())
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
emit_message(Message{"PING", "", {
{"NAME", QVariant(QApplication::applicationName())},
{"VERSION", QVariant(QApplication::applicationVersion())},
{"UTC", QVariant(DriftingDateTime::currentDateTimeUtc().toMSecsSinceEpoch())}
}}.toJson());
2018-02-08 21:28:33 -05:00
}
}
2018-02-08 21:28:33 -05:00
2024-11-02 14:22:22 -07:00
// 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.
2024-11-02 11:25:49 -07:00
//
// Caller is required to make the determination that our port and host
// are valid prior to calling this function.
void
2024-11-02 11:25:49 -07:00
emit_message(QByteArray const & message)
{
2024-11-02 14:22:22 -07:00
if (message != priorMessage_)
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
writeDatagram(message, host_, port_);
2024-11-02 14:22:22 -07:00
priorMessage_ = message;
2018-02-08 21:28:33 -05:00
}
}
2018-02-08 21:28:33 -05:00
2024-11-02 11:25:49 -07:00
// If we've got a port, i.e., we're supposed to send messages, then queue
2024-11-02 11:31:10 -07:00
// the message for later transmission if we don't have a host yet; attempt
2024-11-02 11:25:49 -07:00
// to send it immediately if we've got a host.
//
2024-11-02 11:31:10 -07:00
// The message will be dropped on the floor if we don't have a port defined
2024-11-02 11:25:49 -07:00
// or the message duplicates the last one sent.
void
send_message(QByteArray const & message)
{
2024-11-02 11:25:49 -07:00
if (port_)
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
if (host_.isNull()) messageQueue_.enqueue(message);
else emit_message(message);
2018-02-08 21:28:33 -05:00
}
}
2018-02-08 21:28:33 -05:00
2024-11-02 11:25:49 -07:00
// Start a DNS lookup for the provided server name, noting that we have a
// lookup in flight. If everything works out set our host to the first host
// address associated with the server and send a ping.
2024-11-02 11:31:10 -07:00
//
// 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_server_lookup(QString const & server)
{
2024-11-02 11:25:49 -07:00
hostLookupId_ = QHostInfo::lookupHost(server,
this,
[this](QHostInfo const & info)
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
if (info.lookupId() == hostLookupId_)
{
2024-11-02 11:25:49 -07:00
hostLookupId_ = -1;
2024-11-02 11:25:49 -07:00
if (auto const & list = info.addresses();
!list.isEmpty())
2018-02-08 21:28:33 -05:00
{
2024-11-03 06:58:08 -08:00
host_ = list.first();
2024-11-02 11:25:49 -07:00
ping();
while (messageQueue_.size())
{
send_message(messageQueue_.dequeue());
}
2018-02-08 21:28:33 -05:00
}
else
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
Q_EMIT self_->error (QString {"UDP server lookup failed: %1"}.arg(info.errorString()));
messageQueue_.clear();
2018-02-08 21:28:33 -05:00
}
}
});
}
2018-02-08 21:28:33 -05:00
// Data members
MessageClient * self_;
2024-11-02 14:22:22 -07:00
quint16 port_;
2024-11-02 11:25:49 -07:00
QTimer * ping_;
QHostAddress host_;
int hostLookupId_ = -1;
QQueue<QByteArray> messageQueue_;
2024-11-02 14:22:22 -07:00
QByteArray priorMessage_;
};
2018-02-08 21:28:33 -05:00
/******************************************************************************/
// Implementation
/******************************************************************************/
2018-02-08 21:28:33 -05:00
#include "MessageClient.moc"
2018-02-08 21:28:33 -05:00
2024-11-02 14:22:22 -07:00
MessageClient::MessageClient(QString const & server,
quint16 const port,
QObject * parent)
: QObject {parent}
, m_ {port, this}
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
connect(&*m_, &impl::errorOccurred, [this](impl::SocketError e)
{
2024-11-02 11:25:49 -07:00
#if defined (Q_OS_WIN) // Remove this when Qt stops doing this spuriously.
if (e != impl::NetworkError &&
e != impl::ConnectionRefusedError)
2018-02-08 21:28:33 -05:00
#else
Q_UNUSED (e);
2018-02-08 21:28:33 -05:00
#endif
{
Q_EMIT error (m_->errorString());
}
});
set_server(server);
2018-02-08 21:28:33 -05:00
}
QHostAddress
MessageClient::server_address() const
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
return m_->host_;
2018-02-08 21:28:33 -05:00
}
2024-11-02 14:22:22 -07:00
quint16
MessageClient::server_port() const
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
return m_->port_;
2018-02-08 21:28:33 -05:00
}
void
MessageClient::set_server(QString const & server)
2018-02-08 21:28:33 -05:00
{
qDebug() << "server changed to" << server;
2024-11-02 11:25:49 -07:00
m_->host_.clear();
if (!server.isEmpty()) m_->queue_server_lookup(server);
2018-02-08 21:28:33 -05:00
}
void
2024-11-02 14:22:22 -07:00
MessageClient::set_server_port(quint16 const port)
2018-02-08 21:28:33 -05:00
{
2024-11-02 11:25:49 -07:00
m_->port_ = port;
2018-02-08 21:28:33 -05:00
}
void
MessageClient::send(Message const & message)
{
m_->send_message(message.toJson());
}
void
MessageClient::send_raw_datagram(QByteArray const & message,
2024-11-03 06:27:13 -08:00
QHostAddress const & address,
quint16 const port)
2018-02-08 21:28:33 -05:00
{
2024-11-03 06:27:13 -08:00
if (port && !address.isNull())
{
2024-11-03 06:27:13 -08:00
m_->writeDatagram(message, address, port);
}
2018-02-08 21:28:33 -05:00
}
/******************************************************************************/