mirror of
https://github.com/cculianu/Fulcrum.git
synced 2026-08-13 12:33:27 +02:00
These control the number of jobs that may be enqueued before error, and
the number of concurrent threads to process jobs, respectively.
Also renamed 'misc' subdir to 'doc'
Also added some /stats: show all the configured global Options as they
came in from conf file/CLI. ("Config" subdict in /stats JSON).
281 lines
10 KiB
C++
281 lines
10 KiB
C++
//
|
|
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
|
|
// Copyright (C) 2019-2020 Calin A. Culianu <calin.culianu@gmail.com>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program (see LICENSE.txt). If not, see
|
|
// <https://www.gnu.org/licenses/>.
|
|
//
|
|
#include "AbstractConnection.h"
|
|
#include "Util.h"
|
|
#include <QTcpSocket>
|
|
#include <QSslSocket>
|
|
#include <QHostAddress>
|
|
|
|
#include <cassert>
|
|
|
|
AbstractConnection::AbstractConnection(IdMixin::Id id_in, QObject *parent, qint64 maxBuffer)
|
|
: QObject(parent), IdMixin(id_in), MAX_BUFFER(maxBuffer)
|
|
{
|
|
assert(qobj()); // Runtime check that derived class followed the rules outlined at the top of Mixins.h
|
|
}
|
|
|
|
|
|
/// this should only be called from our thread, because it accesses socket which should only be touched from thread
|
|
QString AbstractConnection::prettyName(bool dontTouchSocket, bool showId) const
|
|
{
|
|
QString type = socket && !dontTouchSocket ? (dynamic_cast<QSslSocket *>(socket) ? "SSL" : "TCP") : "(NoSocket)";
|
|
QString port = socket && !dontTouchSocket && socket->peerPort() ? QString(":%1").arg(socket->peerPort()) : "";
|
|
QString ip = socket && !dontTouchSocket && !socket->peerAddress().isNull() ? socket->peerAddress().toString() : "";
|
|
QString idStr = showId ? QString(" (id: %1)").arg(id) : QString();
|
|
return QString("%1 %2%3 %4%5").arg(type).arg(!objectName().isNull()?objectName():"(AbstractSocket)").arg(idStr).arg(ip).arg(port);
|
|
}
|
|
|
|
bool AbstractConnection::isGood() const
|
|
{
|
|
return status == Connected;
|
|
}
|
|
|
|
bool AbstractConnection::isStale() const
|
|
{
|
|
return isGood() && Util::getTime() - lastGood > stale_threshold;
|
|
}
|
|
|
|
// The below 4 will only return valid results if this->thread() == QThread::currentThread(), and if socket != nullptr
|
|
QHostAddress AbstractConnection::localAddress() const
|
|
{
|
|
QHostAddress ret;
|
|
if (thread() == QThread::currentThread() && socket) {
|
|
ret = socket->localAddress();
|
|
}
|
|
return ret;
|
|
}
|
|
quint16 AbstractConnection::localPort() const
|
|
{
|
|
quint16 ret{};
|
|
if (thread() == QThread::currentThread() && socket) {
|
|
ret = socket->localPort();
|
|
}
|
|
return ret;
|
|
}
|
|
QHostAddress AbstractConnection::peerAddress() const
|
|
{
|
|
QHostAddress ret;
|
|
if (thread() == QThread::currentThread() && socket) {
|
|
ret = socket->peerAddress();
|
|
}
|
|
return ret;
|
|
}
|
|
quint16 AbstractConnection::peerPort() const
|
|
{
|
|
quint16 ret{};
|
|
if (thread() == QThread::currentThread() && socket) {
|
|
ret = socket->peerPort();
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
bool AbstractConnection::isSsl() const
|
|
{
|
|
bool ret{};
|
|
if (thread() == QThread::currentThread() && socket) {
|
|
ret = dynamic_cast<QSslSocket *>(socket) != nullptr;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void AbstractConnection::do_disconnect(bool graceful)
|
|
{
|
|
status = status == Bad ? Bad : NotConnected; // try and keep Bad status around so EXMgr can decide when to reconnect based on it
|
|
if (socket) {
|
|
if (!graceful) {
|
|
Debug() << __FUNCTION__ << " (abort)";
|
|
socket->abort(); // this will set status too because state change, but we set it first above to be paranoid
|
|
} else {
|
|
socket->disconnectFromHost();
|
|
Debug() << __FUNCTION__ << " (graceful)";
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
void setSockOpts(QAbstractSocket *socket) {
|
|
if (socket) {
|
|
// don't we want to disable KeepAliveOption ? it appears to eat some bandwidth .. 1 packet per second on Windows.
|
|
//socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); // from Qt docs: required on Windows before connection
|
|
socket->setSocketOption(QAbstractSocket::SocketOption::LowDelayOption, 1); // disable Nagling for lower latency
|
|
}
|
|
}
|
|
}
|
|
|
|
void AbstractConnection::socketConnectSignals()
|
|
{
|
|
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(on_error(QAbstractSocket::SocketError)));
|
|
connect(socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(on_socketState(QAbstractSocket::SocketState)));
|
|
connect(socket, &QAbstractSocket::connected, this, [this]{on_connected();});
|
|
setSockOpts(socket); // from Qt docs: required on Windows before connection
|
|
}
|
|
|
|
bool AbstractConnection::do_write(const QByteArray & data)
|
|
{
|
|
QString err = "";
|
|
if (!socket) {
|
|
err = " called with no socket! FIXME!";
|
|
} else if (QThread::currentThread() != thread()) {
|
|
err = " called from another thread! FIXME!";
|
|
}
|
|
if (!err.isEmpty()) {
|
|
Error() << __FUNCTION__ << " (" << objectName() << ") " << err << " id=" << id;
|
|
return false;
|
|
}
|
|
auto data2write = writeBackLog + data;
|
|
qint64 written = socket->write(data2write);
|
|
if (written < 0) {
|
|
Error() << __FUNCTION__ << " error on write " << socket->error() << " (" << socket->errorString() << ") id=" << id;
|
|
do_disconnect();
|
|
return false;
|
|
} else if (written < data2write.length()) {
|
|
writeBackLog = data2write.mid(int(written));
|
|
}
|
|
nSent += written;
|
|
if (writeBackLog.length() > MAX_BUFFER) {
|
|
Warning(Log::Magenta) << __FUNCTION__ << " MAX_BUFFER reached on write (" << MAX_BUFFER << ") id=" << id;
|
|
do_disconnect();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void AbstractConnection::slot_on_readyRead() { on_readyRead(); }
|
|
|
|
void AbstractConnection::on_connected()
|
|
{
|
|
// runs in our thread's context
|
|
Debug() << __FUNCTION__;
|
|
connectedTS = Util::getTime();
|
|
setSockOpts(socket); // ensure nagling disabled
|
|
socket->setReadBufferSize(MAX_BUFFER); // ensure memory exhaustion from peer can't happen in case we're too busy to read.
|
|
connectedConns.push_back(connect(this, &AbstractConnection::send, this, &AbstractConnection::do_write));
|
|
connectedConns.push_back(connect(socket, SIGNAL(readyRead()), this, SLOT(slot_on_readyRead())));
|
|
if (dynamic_cast<QSslSocket *>(socket)) {
|
|
// for some reason Qt can't find this old-style signal for QSslSocket so we do the below.
|
|
// Additionally, bytesWritten is never emitted for QSslSocket, violating OOP! Thanks Qt. :P
|
|
connectedConns.push_back(connect(socket, SIGNAL(encryptedBytesWritten(qint64)), this, SLOT(on_bytesWritten())));
|
|
} else {
|
|
connectedConns.push_back(connect(socket, SIGNAL(bytesWritten(qint64)), this, SLOT(on_bytesWritten())));
|
|
}
|
|
connectedConns.push_back(
|
|
connect(socket, &QAbstractSocket::disconnected, this, [this]{
|
|
Debug() << prettyName() << " socket disconnected";
|
|
for (const auto & connection : connectedConns) {
|
|
QObject::disconnect(connection);
|
|
}
|
|
connectedConns.clear(); // be sure to empty the list out when we are done!
|
|
stopTimer(pingTimer); // kill the ping timer (method from TimersByNameMixin)
|
|
on_disconnected();
|
|
emit lostConnection(this);
|
|
// todo: put stuff to queue up a reconnect sometime later?
|
|
})
|
|
);
|
|
{ // set up the "pingTimer"
|
|
auto on_pingTimer = [this]{
|
|
if (Util::getTime() - lastGood > pingtime_ms)
|
|
// only call do_ping if we've been idle for longer than pingtime_ms
|
|
do_ping();
|
|
return true;
|
|
};
|
|
const int period_ms = pingtime_ms/* 1 minute */ / 2;
|
|
callOnTimerSoon(period_ms, pingTimer, on_pingTimer, true, Qt::TimerType::VeryCoarseTimer); // method inherited from TimersByNameMixin
|
|
}
|
|
}
|
|
|
|
void AbstractConnection::on_disconnected()
|
|
{
|
|
++nDisconnects;
|
|
}
|
|
|
|
void AbstractConnection::on_socketState(QAbstractSocket::SocketState s)
|
|
{
|
|
Debug() << prettyName() << " socket state: " << s;
|
|
switch (s) {
|
|
case QAbstractSocket::ConnectedState:
|
|
status = Connected;
|
|
break;
|
|
case QAbstractSocket::HostLookupState:
|
|
case QAbstractSocket::ConnectingState:
|
|
status = Connecting;
|
|
break;
|
|
case QAbstractSocket::UnconnectedState:
|
|
case QAbstractSocket::ClosingState:
|
|
default:
|
|
status = NotConnected;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void AbstractConnection::on_bytesWritten()
|
|
{
|
|
Trace() << __FUNCTION__;
|
|
if (!writeBackLog.isEmpty() && status == Connected && socket) {
|
|
Debug() << prettyName() << " writeBackLog size: " << writeBackLog.length();
|
|
do_write();
|
|
}
|
|
}
|
|
|
|
void AbstractConnection::do_ping()
|
|
{
|
|
Debug() << __FUNCTION__ << " " << prettyName() << " stub ...";
|
|
}
|
|
|
|
void AbstractConnection::on_error(QAbstractSocket::SocketError err)
|
|
{
|
|
Warning() << prettyName() << ": error " << err << " (" << (lastSocketError = (socket ? socket->errorString() : "(null)")) << ")";
|
|
++nSocketErrors;
|
|
do_disconnect();
|
|
}
|
|
|
|
/// call this only from this object's thread
|
|
auto AbstractConnection::stats() const -> Stats
|
|
{
|
|
QVariantMap m;
|
|
m["name"] = objectName();
|
|
m["id"] = id;
|
|
m["connectedTime"] = isGood() ? QVariant(double(Util::getTime() - connectedTS)/1e3) : QVariant();
|
|
m["nBytesSent"] = nSent.load();
|
|
m["nBytesReceived"] = nReceived.load();
|
|
m["idleTime"] = isGood() ? QVariant(double(Util::getTime() - lastGood)/1e3) : QVariant();
|
|
m["lastSocketError"] = lastSocketError;
|
|
m["nDisconnects"] = nDisconnects.load();
|
|
m["nSocketErrors"] = nSocketErrors.load();
|
|
m["writeBackLog"] = writeBackLog.size();
|
|
m["readBytesAvailable"] = socket ? socket->bytesAvailable() : 0;
|
|
auto atl = activeTimers();
|
|
QVariantMap timerMap;
|
|
for (const auto & name : atl)
|
|
timerMap[name] = timerInterval(name);
|
|
m["activeTimers"] = timerMap;
|
|
m["remote"] = [this]() -> QVariant {
|
|
if (QString addr; socket && !(addr=socket->peerAddress().toString()).isNull()) {
|
|
return QString("%1:%2").arg(addr).arg(socket->peerPort());
|
|
}
|
|
return QVariant(); // null
|
|
}();
|
|
m["local"] = [this]() -> QVariant {
|
|
if (QString addr; socket && !(addr=socket->localAddress().toString()).isNull()) {
|
|
return QString("%1:%2").arg(addr).arg(socket->localPort());
|
|
}
|
|
return QVariant(); // null
|
|
}();
|
|
m["maxBuffer"] = MAX_BUFFER;
|
|
return m;
|
|
}
|