mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
Rename all header files with .h extension Fix function declaration in HamlibTransceiver.h that overrode member function but was not marked override Remove unused variable in Modulator.cpp Remove unused files from source tree Remove duplicate LazyFillComboBox files Reformat codebase to LLVM C++ standard
257 lines
8.6 KiB
C++
257 lines
8.6 KiB
C++
#include "MessageClient.h"
|
|
#include <QApplication>
|
|
#include <QHostInfo>
|
|
#include <QLoggingCategory>
|
|
#include <QNetworkDatagram>
|
|
#include <QQueue>
|
|
#include <QSet>
|
|
#include <QTimer>
|
|
#include <QUdpSocket>
|
|
#include <stdexcept>
|
|
|
|
#include "DriftingDateTime.h"
|
|
#include "JS8_Include/pimpl_impl.h"
|
|
#include "moc_MessageClient.cpp"
|
|
|
|
Q_DECLARE_LOGGING_CATEGORY(messageclient_js8)
|
|
|
|
/******************************************************************************/
|
|
// Constants
|
|
/******************************************************************************/
|
|
|
|
namespace {
|
|
constexpr auto PING_INTERVAL = std::chrono::seconds(15);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
// 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}} {
|
|
// Note that With UDP, error reporting is not guaranteed, which is not
|
|
// the same as a guarantee of no error reporting. Typically, a packet
|
|
// arriving on a port where there is no listener will trigger an ICMP
|
|
// Port Unreachable message back to the sender, and some implementations
|
|
// e.g., Windows, will report that to the application on the next
|
|
// attempt to transmit to the same destination.
|
|
|
|
connect(this, &QUdpSocket::errorOccurred, [this](SocketError const e) {
|
|
if (e != ConnectionRefusedError) {
|
|
Q_EMIT self_->error(errorString());
|
|
}
|
|
});
|
|
|
|
connect(this, &QIODevice::readyRead, this, [this]() {
|
|
while (hasPendingDatagrams()) {
|
|
if (auto const datagram = receiveDatagram();
|
|
datagram.isValid()) {
|
|
try {
|
|
Q_EMIT self_->message(
|
|
Message::fromJson(datagram.data()));
|
|
} catch (std::exception const &e) {
|
|
Q_EMIT self_->error(
|
|
QString{"MessageClient exception: %1"}.arg(
|
|
e.what()));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
connect(ping_, &QTimer::timeout, this, &impl::ping);
|
|
|
|
ping_->start(PING_INTERVAL);
|
|
}
|
|
|
|
// Destructor
|
|
|
|
~impl() {
|
|
abort_host_lookup();
|
|
|
|
if (port_ && !host_.isNull()) {
|
|
send_message({"CLOSE"});
|
|
}
|
|
}
|
|
|
|
// Send a ping message, if we have a valid port and host.
|
|
|
|
void ping() {
|
|
if (port_ && !host_.isNull()) {
|
|
send_message(
|
|
{"PING",
|
|
"",
|
|
{{"NAME", QVariant(QApplication::applicationName())},
|
|
{"VERSION", QVariant(QApplication::applicationVersion())},
|
|
{"UTC", QVariant(DriftingDateTime::currentDateTimeUtc()
|
|
.toMSecsSinceEpoch())}}});
|
|
}
|
|
}
|
|
|
|
// If the JSON-serialized form of the message isn't exactly the same as
|
|
// the one that we last sent, send it and note it as the prior datagram
|
|
// sent.
|
|
//
|
|
// Caller is required to make the determination that our port and host
|
|
// are valid prior to calling this function.
|
|
|
|
void send_message(Message const &message) {
|
|
// Skip sending if port is 0 (disabled)
|
|
if (!port_) {
|
|
return;
|
|
}
|
|
|
|
if (auto const datagram = message.toJson(); datagram != lastDatagram_) {
|
|
writeDatagram(datagram, host_, port_);
|
|
lastDatagram_ = datagram;
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
|
|
qCDebug(messageclient_js8)
|
|
<< "MessageClient Host:" << host_.toString()
|
|
<< "loopback:" << host_.isLoopback()
|
|
<< "multicast:" << host_.isMulticast();
|
|
|
|
if (state() != UnconnectedState)
|
|
close();
|
|
|
|
bind(host_.protocol() == IPv6Protocol
|
|
? QHostAddress::AnyIPv6
|
|
: QHostAddress::AnyIPv4);
|
|
|
|
if (host_.isMulticast())
|
|
joinMulticastGroup(host_);
|
|
|
|
ping();
|
|
|
|
if (port_ && !host_.isNull()) {
|
|
while (!messageQueue_.isEmpty())
|
|
send_message(messageQueue_.dequeue());
|
|
}
|
|
} else {
|
|
Q_EMIT self_->error(
|
|
QString{"Host lookup failed: %1"}.arg(
|
|
info.errorString()));
|
|
messageQueue_.clear();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Data members
|
|
|
|
MessageClient *self_;
|
|
quint16 port_;
|
|
QTimer *ping_;
|
|
QHostAddress host_;
|
|
int hostLookupId_ = -1;
|
|
QQueue<Message> messageQueue_;
|
|
QByteArray lastDatagram_;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
// Implementation
|
|
/******************************************************************************/
|
|
|
|
#include "MessageClient.moc"
|
|
|
|
// Constructor
|
|
|
|
MessageClient::MessageClient(QString const &name, quint16 const port,
|
|
QObject *parent)
|
|
: QObject{parent}, m_{port, this} {
|
|
set_server_name(name);
|
|
}
|
|
|
|
// Host accessor; returns host information for the server. Note
|
|
// that this will return an invalid object while a host lookup
|
|
// is in flight, so consider thread inertia before relying on
|
|
// this; the case may be that information is coming, but it's
|
|
// not here just yet.
|
|
|
|
QHostAddress MessageClient::server_host() const { return m_->host_; }
|
|
|
|
// Port accessor; if zero, anything sent to us will be dropped
|
|
// on the floor.
|
|
|
|
quint16 MessageClient::server_port() const { return m_->port_; }
|
|
|
|
// Set our server name. If the name is empty, then what happens to
|
|
// messages provided to us via send() depends on what our port is
|
|
// set to. If it's zero, they'll just be dropped on the floor. If
|
|
// it's non-zero, they'll be queued until this method is called
|
|
// again with a non-empty name.
|
|
|
|
void MessageClient::set_server_name(QString const &name) {
|
|
m_->host_.clear();
|
|
|
|
if (name.isEmpty())
|
|
m_->abort_host_lookup();
|
|
else
|
|
m_->queue_host_lookup(name);
|
|
}
|
|
|
|
// Set our port; if zero, no sending will be performed, and anything
|
|
// sent us for disposition via send() will be dropped on the floor.
|
|
|
|
void MessageClient::set_server_port(quint16 const port) { m_->port_ = port; }
|
|
|
|
// 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.
|
|
|
|
void MessageClient::send(Message const &message) {
|
|
if (m_->port_) {
|
|
if (m_->host_.isNull())
|
|
m_->messageQueue_.enqueue(message);
|
|
else
|
|
m_->send_message(message);
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
Q_LOGGING_CATEGORY(messageclient_js8, "messageclient.js8", QtWarningMsg)
|