mirror of
https://github.com/cculianu/Fulcrum.git
synced 2026-08-17 13:07:35 +02:00
234 lines
9.4 KiB
C++
234 lines
9.4 KiB
C++
#include "EXMgr.h"
|
|
#include "EXClient.h"
|
|
#include "Util.h"
|
|
#include "RPC.h"
|
|
#include <vector>
|
|
|
|
EXMgr::EXMgr(const QString & serversFile, QObject *parent)
|
|
: Mgr(parent), serversFile(serversFile)
|
|
{
|
|
}
|
|
|
|
EXMgr::~EXMgr()
|
|
{
|
|
Debug() << __FUNCTION__ ;
|
|
cleanup();
|
|
}
|
|
|
|
void EXMgr::startup()
|
|
{
|
|
if (clients.isEmpty()) {
|
|
initRPCMethods();
|
|
loadServers();
|
|
} else {
|
|
Error() << __PRETTY_FUNCTION__ << " called with EXClients already active! FIXME!";
|
|
}
|
|
}
|
|
|
|
void EXMgr::cleanup()
|
|
{
|
|
for (auto ex : clients) {
|
|
delete ex; // will wait for threads to finish
|
|
}
|
|
clients.clear();
|
|
clientsById.clear();
|
|
_rpcMethods.clear();
|
|
}
|
|
|
|
void EXMgr::loadServers()
|
|
{
|
|
auto v = Util::Json::parseFile(serversFile);
|
|
auto m = v.toMap();
|
|
for (auto it = m.constBegin(); it != m.constEnd(); ++it) {
|
|
auto smap = it.value().toMap();
|
|
bool versionok = !smap.isEmpty() && smap.value("version", "").toString().startsWith("1.4");
|
|
quint16 tport = versionok ? quint16(smap.value("t", 0).toUInt()) : 0U;
|
|
quint16 sport = versionok ? quint16(smap.value("s", 0).toUInt()) : 0U;
|
|
bool ok = versionok && (tport || sport);
|
|
QString host = it.key();
|
|
//Debug() << "Server: " << host << " s:" << sport << " t:" << tport << " " << (ok ? "ok" : "not ok");
|
|
if (ok) {
|
|
auto client = new EXClient(this, newId(), host, tport, sport);
|
|
clients.push_back(client);
|
|
clientsById[client->id] = client;
|
|
connect(client, &EXClient::newConnection, this, &EXMgr::onNewConnection);
|
|
connect(client, &EXClient::lostConnection, this, &EXMgr::onLostConnection);
|
|
connect(client, &EXClient::gotMessage, this, &EXMgr::onMessage);
|
|
client->start();
|
|
} else {
|
|
Warning() << "Bad server entry: " << host;
|
|
}
|
|
}
|
|
if (clients.isEmpty()) {
|
|
throw Exception("No ElectrumX servers! Cannot proceed.");
|
|
}
|
|
checkClientsTimer = new QTimer(this); checkClientsTimer->setSingleShot(false);
|
|
connect(checkClientsTimer, SIGNAL(timeout()), this, SLOT(checkClients()));
|
|
checkClientsTimer->start(EXClient::reconnectTime/2); // 1 minute
|
|
Log() << "ElectrumX Manager started, found " << clients.count() << " servers from compiled-in servers.json";
|
|
}
|
|
|
|
void EXMgr::onNewConnection(EXClient *client)
|
|
{
|
|
Debug () << "New connection for " << client->host;
|
|
emit client->sendRequest(newId(), "server.version", QVariantList({QString("%1/%2").arg(APPNAME).arg(VERSION), QString("1.4")}));
|
|
emit client->sendRequest(newId(), "blockchain.headers.subscribe");
|
|
}
|
|
|
|
void EXMgr::onLostConnection(EXClient *client)
|
|
{
|
|
Debug () << "Connection lost for " << client->host << ", status: " << client->status;
|
|
height.seenBy.remove(client->id);
|
|
client->info.clear();
|
|
}
|
|
|
|
void EXMgr::onMessage(EXClient *client, const RPC::Message &m)
|
|
{
|
|
if (m.isError()) {
|
|
// handle error replies -- for now just print yellow warning message to console log for debugging
|
|
// TODO: Figure out what to do about errors.
|
|
Warning("(%s) Got error reply: code: %d message: \"%s\"",
|
|
Q2C(client->host), m.errorCode, Q2C(m.errorMessage));
|
|
return;
|
|
}
|
|
Debug() << "(" << client->host << ") Got message in mgr, method: " << m.method;
|
|
if (m.method == "server.version") {
|
|
QVariantList l = m.data.toList();
|
|
if (l.size() == 2) {
|
|
client->info.serverVersion.first = l[0].toString();
|
|
client->info.serverVersion.second = l[1].toString();
|
|
Debug() << "Got server version: " << client->info.serverVersion.first << " / " << client->info.serverVersion.second;
|
|
} else {
|
|
Error() << "Bad server version reply! Schema should have handled this. FIXME! Json: " << m.toJsonString();
|
|
}
|
|
} else if (m.method == "blockchain.headers.subscribe") {
|
|
// list of dicts.. or a simple value.. handle either. TODO: make this a more general mechanism.
|
|
const QVariantList list ( m.data.toList() );
|
|
const QVariantMap map ( list.isEmpty() ? m.data.toMap() : list.back().toMap() );
|
|
int ht = map.value("height", 0).toInt();
|
|
QString hdr = map.value("hex", "").toString();
|
|
if (ht > 0 && !hdr.isEmpty()) {
|
|
if (ht > height.height) {
|
|
height.height = ht;
|
|
height.ts = Util::getTime();
|
|
height.header = hdr;
|
|
height.seenBy.clear();
|
|
}
|
|
if (ht == height.height)
|
|
height.seenBy.insert(client->id);
|
|
client->info.height = ht;
|
|
client->info.header = hdr;
|
|
} else {
|
|
Error() << "Bad server headers reply! Schema should have handled this. FIXME! Json: " << m.toJsonString();
|
|
}
|
|
Debug() << "Got header subscribe: " << client->info.height << " / " << client->info.header << " (count for height = " << height.seenBy.count() << ")";
|
|
} else if (m.method == "server.ping") {
|
|
// ignore; timestamps updated in EXClient and RPC::Connection
|
|
//Debug() << "server.ping reply... yay";
|
|
} else {
|
|
Error() << "Unknown method \"" << m.method << "\" from " << client->host;
|
|
}
|
|
}
|
|
|
|
void EXMgr::checkClients() ///< called from the checkClientsTimer every 1 mins
|
|
{
|
|
static const qint64 bad_timeout = 15*60*1000, // 15 mins
|
|
low_server_timeout = checkClientsTimer->interval()/2; // 30 seconds
|
|
Debug() << "EXMgr: Checking clients...";
|
|
const bool lowServers = height.seenBy.count() == 0;
|
|
const qint64 stale_timeout = lowServers ? low_server_timeout : EXClient::reconnectTime; // 1 or 2 mins
|
|
QStringList laggers;
|
|
for (EXClient *client : clients) {
|
|
const auto now = Util::getTime();
|
|
if (const bool lagging = client->info.height < height.height && client->info.isValid();
|
|
client->isGood() && !client->isStale())
|
|
{
|
|
if (lagging) laggers += QString("%1 (%2)").arg(client->host).arg(client->info.height);
|
|
continue;
|
|
}
|
|
qint64 elapsed = qMin(now-client->lastConnectionAttempt, now-client->lastGood);
|
|
if (client->isBad() && elapsed > bad_timeout) {
|
|
Log() << "'Bad' EX host " << client->host << ", reconnecting...";
|
|
client->restart();
|
|
} else if (client->isStale() && elapsed > stale_timeout) {
|
|
Log() << "'Stale' EX host " << client->host << ", reconnecting...";
|
|
client->restart();
|
|
} else if (!client->isGood() && elapsed > stale_timeout) {
|
|
Log() << "EX host " << client->host << ", retrying...";
|
|
client->restart();
|
|
}
|
|
}
|
|
if (int ct = laggers.count(); ct) {
|
|
QString s = ct == 1 ? " is" : "s are";
|
|
Log("%d server%s lagging behind the latest block height of %d: %s", ct, Q2C(s), height.height, Q2C(laggers.join(", ")));
|
|
}
|
|
}
|
|
|
|
EXClient * EXMgr::pick()
|
|
{
|
|
// defensive programming: enforce caller is in main thread
|
|
if (QThread::currentThread() != qApp->thread()) {
|
|
// crash the program
|
|
throw InternalError(QString("%1 was called from a thread other than the main thread")
|
|
.arg(__PRETTY_FUNCTION__));
|
|
}
|
|
auto unpicked = height.seenBy - recentPicks;
|
|
if (unpicked.isEmpty()) {
|
|
// reset picking
|
|
recentPicks.clear();
|
|
unpicked = height.seenBy - recentPicks;
|
|
}
|
|
std::vector<decltype(unpicked)::key_type> shuffled(unpicked.begin(), unpicked.end());
|
|
Util::shuffle(shuffled.begin(), shuffled.end());
|
|
for (auto id : shuffled) {
|
|
EXClient *client = clientsById[id];
|
|
if (client->status == EXClient::Connected) {
|
|
recentPicks.insert(client->id);
|
|
return client;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void EXMgr::initRPCMethods()
|
|
{
|
|
QString m, d;
|
|
m = "blockchain.headers.subscribe";
|
|
d = "{\"hex\" : \"somestring\", \"height\" : 1}";
|
|
_rpcMethods.insert(m, QSharedPointer<RPC::Method>(new RPC::Method(
|
|
m,
|
|
RPC::schemaMethod + QString(" { \"method\" : \"%1!\", \"params\" : [%2] }").arg(m).arg(d), // in schema (asynch from server -> us)
|
|
RPC::schemaResult + QString(" { \"result\" : %1}").arg(d), // result schema (synch. server -> us)
|
|
RPC::schemaMethod + QString(" { \"method\" : \"%1!\", \"params\" : [\"=0\"] }").arg(m) // out schema (req. us -> server)
|
|
)));
|
|
|
|
m = "server.version";
|
|
_rpcMethods.insert(m, QSharedPointer<RPC::Method>(new RPC::Method(
|
|
m,
|
|
RPC::Schema(), // in schema (asynch from server -> us) -- DISABLED for server.version
|
|
RPC::schemaResult + QString(" { \"result\" : [\"=2\"] }"), // result schema (synch. server -> us) -- enforce must have 2 string args
|
|
RPC::schemaMethod + QString(" { \"method\" : \"%1!\", \"params\" : [\"=2\"] }").arg(m) // out schema (req. us -> server) -- enforce must have 2 string args
|
|
)));
|
|
|
|
m = "server.ping";
|
|
_rpcMethods.insert(m, QSharedPointer<RPC::Method>(new RPC::Method(
|
|
m,
|
|
RPC::Schema(), // in schema (asynch from server -> us) -- DISABLED for server.ping
|
|
RPC::schemaResult + QString(" { \"result\" : null }"), // result schema -- 'result' arg should be there and be null.
|
|
RPC::schemaMethodNoParams // out schema, ping to server takes no args
|
|
)));
|
|
}
|
|
|
|
void EXMgr::pickTest()
|
|
{
|
|
for (int i = 0; i < 100; ++i) {
|
|
auto client = pick();
|
|
if (client) {
|
|
Log() << "Picked " << client->id;
|
|
} else {
|
|
Warning() << "Pick found nothing!";
|
|
}
|
|
QThread::msleep(100);
|
|
if (qApp) qApp->processEvents();
|
|
}
|
|
}
|