2019-11-18 14:32:48 +02:00
# include <QPointer>
2019-11-24 18:23:34 +02:00
# include "bitcoin/rpc/protocol.h"
2019-11-12 22:14:04 +02:00
# include "BitcoinD.h"
BitcoinDMgr : : BitcoinDMgr ( const QHostAddress & host , quint16 port ,
const QString & user , const QString & pass )
: Mgr ( nullptr ) , IdMixin ( newId ( ) ) , host ( host ) , port ( port ) , user ( user ) , pass ( pass )
{
setObjectName ( " BitcoinDMgr " ) ;
2019-11-13 00:57:20 +02:00
_thread . setObjectName ( objectName ( ) ) ;
2019-11-12 22:14:04 +02:00
}
BitcoinDMgr : : ~ BitcoinDMgr ( ) { cleanup ( ) ; }
void BitcoinDMgr : : startup ( ) {
2019-11-27 14:07:09 +02:00
Log ( ) < < objectName ( ) < < " : starting " < < N_CLIENTS < < " " < < Util : : Pluralize ( " bitcoin rpc client " , N_CLIENTS ) < < " ... " ;
2019-11-12 22:14:04 +02:00
2019-11-13 00:57:20 +02:00
for ( auto & client : clients ) {
client = std : : make_unique < BitcoinD > ( host , port , user , pass ) ;
2019-11-13 01:55:55 +02:00
// connect client to us -- TODO: figure out workflow: how requests for work and results will get dispatched
connect ( client . get ( ) , & BitcoinD : : gotMessage , this , & BitcoinDMgr : : on_Message ) ;
connect ( client . get ( ) , & BitcoinD : : gotErrorMessage , this , & BitcoinDMgr : : on_ErrorMessage ) ;
2019-11-18 09:47:18 +02:00
connect ( client . get ( ) , & BitcoinD : : authenticated , this , [ this ] ( BitcoinD * b ) {
// guard against stale/old signal
2019-11-19 01:10:45 +02:00
if ( ! b - > isGood ( ) ) {
2019-11-18 09:47:18 +02:00
Debug ( ) < < " got authenticated for id: " < < b - > id < < " but isGood() is false! " ;
return ; // false/stale signal
}
2019-11-18 14:32:48 +02:00
const bool wasEmpty = goodSet . empty ( ) ;
goodSet . insert ( b - > id ) ;
2019-11-18 09:47:18 +02:00
if ( wasEmpty )
emit gotFirstGoodConnection ( b - > id ) ;
} ) ;
connect ( client . get ( ) , & BitcoinD : : lostConnection , this , [ this ] ( AbstractConnection * c ) {
// guard against stale/old signal
2019-11-19 01:10:45 +02:00
if ( c - > isGood ( ) ) {
2019-11-18 09:47:18 +02:00
Debug ( ) < < " got lostConnection for id: " < < c - > id < < " but isGood() is true! " ;
return ; // false/stale signal
}
2019-11-18 14:32:48 +02:00
goodSet . erase ( c - > id ) ;
2019-11-18 09:47:18 +02:00
auto constexpr chkTimer = " checkNoMoreBitcoinDs " ;
// we throttle the spamming of the allConnectionsLost signal via this mechanism
callOnTimerSoonNoRepeat ( miniTimeout , chkTimer , [ this ] {
2019-11-18 14:32:48 +02:00
if ( goodSet . empty ( ) )
2019-11-18 09:47:18 +02:00
emit allConnectionsLost ( ) ;
} , true ) ;
} ) ;
2019-11-13 01:55:55 +02:00
2019-11-13 00:57:20 +02:00
client - > start ( ) ;
}
2019-11-12 22:14:04 +02:00
start ( ) ;
Log ( ) < < objectName ( ) < < " : started ok " ;
}
void BitcoinDMgr : : cleanup ( ) {
stop ( ) ;
for ( auto & client : clients ) {
client . reset ( ) ; /// implicitly calls client->stop()
}
2019-11-18 14:32:48 +02:00
goodSet . clear ( ) ;
2019-11-18 09:47:18 +02:00
2019-11-13 00:57:20 +02:00
Debug ( ) < < " BitcoinDMgr cleaned up " ;
2019-11-12 22:14:04 +02:00
}
2019-11-13 01:52:15 +02:00
void BitcoinDMgr : : on_Message ( quint64 bid , const RPC : : Message & msg )
{
2019-11-27 20:32:50 +02:00
if ( Trace : : isEnabled ( ) ) Trace ( ) < < " Msg from: " < < bid < < " method= " < < msg . method ;
2019-11-13 01:52:15 +02:00
}
void BitcoinDMgr : : on_ErrorMessage ( quint64 bid , const RPC : : Message & msg )
{
Debug ( ) < < " ErrMsg from: " < < bid < < " error= " < < msg . errorMessage ( ) ;
2019-11-24 18:23:34 +02:00
if ( msg . errorCode ( ) = = bitcoin : : RPCErrorCode : : RPC_IN_WARMUP ) {
emit inWarmUp ( msg . errorMessage ( ) ) ;
}
2019-11-13 01:52:15 +02:00
}
2019-11-12 22:14:04 +02:00
auto BitcoinDMgr : : stats ( ) const - > Stats
{
2019-11-13 00:57:20 +02:00
QVariantList l ;
2019-11-18 09:47:18 +02:00
constexpr int timeout = kDefaultTimeout / qMax ( N_CLIENTS , 1 ) ;
2019-11-13 00:57:20 +02:00
for ( const auto & client : clients ) {
if ( ! client ) continue ;
2019-11-27 14:07:09 +02:00
auto map = client - > statsSafe ( timeout ) . toMap ( ) ;
2019-11-16 22:27:21 +02:00
auto name = map . take ( " name " ) . toString ( ) ;
l + = QVariantMap ( { { name , map } } ) ;
2019-11-13 00:57:20 +02:00
}
2019-11-27 14:07:09 +02:00
return l ;
2019-11-12 22:14:04 +02:00
}
2019-11-18 14:32:48 +02:00
BitcoinD * BitcoinDMgr : : getBitcoinD ( )
{
BitcoinD * ret = nullptr ;
2019-11-19 01:58:30 +02:00
unsigned which = 0 ;
if ( unsigned n = quint32 ( goodSet . size ( ) ) ; n > 1 )
which = QRandomGenerator : : system ( ) - > bounded ( n ) ; // pick a random client in the set (which is an index of the "good clients")
2019-11-18 14:32:48 +02:00
if ( ! goodSet . empty ( ) ) {
// linear search for a bitcoind that is not lastBitcoinDUsed
2019-11-19 01:58:30 +02:00
unsigned i = 0 ;
2019-11-18 14:32:48 +02:00
for ( auto & client : clients ) {
2019-11-19 01:58:30 +02:00
if ( goodSet . count ( client - > id ) & & i + + = = which & & client - > isGood ( ) ) {
2019-11-18 14:32:48 +02:00
ret = client . get ( ) ;
break ;
}
}
}
return ret ;
}
/// this is safe to call from any thread. internally it dispatches messages to this obejct's thread.
/// may throw (TODO list exceptions it may throw). Results/Error/Fail functions are called in the context of the sender's thread.
/// Returns the BitcoinD->id that was given the message.
void BitcoinDMgr : : submitRequest ( QObject * sender , const RPC : : Message : : Id & rid , const QString & method , const QVariantList & params ,
const ResultsF & resf , const ErrorF & errf , const FailF & failf )
{
QPointer < QObject > context ( new QObject ( sender ) ) ; // this is a weak ref that gets killed when sender is killed. This way stuff just "goes away" if sender dies.
context - > setObjectName ( QString ( " context for '%1' request id: %2 " ) . arg ( sender ? sender - > objectName ( ) : " " ) . arg ( rid . toString ( ) ) ) ;
2019-11-20 10:26:31 +02:00
auto killContext = [ context , this ] ( bool thisDestroyed = false ) {
2019-11-18 14:32:48 +02:00
if ( LIKELY ( context ) ) { // need to check context because race conditions
2019-11-20 10:26:31 +02:00
if ( LIKELY ( ! thisDestroyed ) )
Util : : VoidFuncOnObjectNoThrow ( this , [ context , this ] { if ( LIKELY ( context ) ) disconnect ( this , & QObject : : destroyed , context , nullptr ) ; } , 0 ) ;
2019-11-18 14:32:48 +02:00
Util : : VoidFuncOnObjectNoThrow ( context , [ context ] { if ( LIKELY ( context ) ) context - > deleteLater ( ) ; } , 0 ) ;
}
} ;
2019-11-20 10:42:14 +02:00
// paranoia: make sure that if we die, to kill the context ASAP too to clean up resources
2019-11-20 10:26:31 +02:00
connect ( this , & QObject : : destroyed , context , [ killContext ] { killContext ( true ) ; } , Qt : : DirectConnection ) ;
2019-11-18 14:32:48 +02:00
// schedule this ASAP
QTimer : : singleShot ( 0 , this , [ this , context , resf , errf , failf , rid , method , params , killContext ] {
2019-11-19 01:10:45 +02:00
if ( UNLIKELY ( ! context ) )
// sender parent must have been deleted before we got a chance to run
return ;
2019-11-18 14:32:48 +02:00
auto replied = std : : make_shared < std : : atomic_bool > ( false ) ; // guards against spurious lostConnection arriving after reply msg
2019-11-19 01:10:45 +02:00
// this is called from either context thread or this thread
2019-11-18 14:32:48 +02:00
auto do_fail = [ failf , context , killContext , rid , replied ] ( const QString & reason ) {
2019-11-19 01:10:45 +02:00
if ( LIKELY ( ! replied - > exchange ( true ) & & failf & & context ) ) {
2019-11-18 14:32:48 +02:00
Util : : VoidFuncOnObjectNoThrow ( context , [ context , failf , rid , reason ] {
if ( LIKELY ( context ) ) // need to check context again because race conditions
failf ( rid , reason ) ;
2019-11-20 10:26:31 +02:00
} ) ; // fixme: this has an infinite timeout -- HOWEVER, it's usually called from 'context' thread which means it's a direct function call. USUALLY.
// ^ fixme2: also -- the above can theoretically hang forever if context gets deleted or its thread is stopped during this call.
2019-11-18 14:32:48 +02:00
}
killContext ( ) ;
} ;
auto bd = getBitcoinD ( ) ;
if ( UNLIKELY ( ! bd ) ) {
do_fail ( " Unable to find a good BitcoinD connection " ) ;
return ;
}
2019-11-19 01:10:45 +02:00
connect ( bd , & BitcoinD : : gotMessage , context , [ resf , replied , rid , killContext ] ( quint64 , const RPC : : Message & reply ) {
if ( reply . id = = rid ) { // filter out messages not for us
if ( ! replied - > exchange ( true ) & & resf )
resf ( reply ) ;
killContext ( ) ;
}
2019-11-18 14:32:48 +02:00
} ) ;
2019-11-19 01:10:45 +02:00
connect ( bd , & BitcoinD : : gotErrorMessage , context , [ errf , replied , rid , killContext ] ( quint64 , const RPC : : Message & errMsg ) {
if ( errMsg . id = = rid ) { // filter out error messages not for us
if ( ! replied - > exchange ( true ) & & errf )
errf ( errMsg ) ;
killContext ( ) ;
}
2019-11-18 14:32:48 +02:00
} ) ;
connect ( bd , & BitcoinD : : lostConnection , context , [ do_fail , rid ] ( AbstractConnection * ) {
do_fail ( " connection lost " ) ;
} ) ;
bd - > sendRequest ( rid , method , params ) ;
} ) ;
// .. aand.. return right away
}
/* --- BitcoinD --- */
2019-11-17 00:18:44 +02:00
auto BitcoinD : : stats ( ) const - > Stats
2019-11-13 00:57:20 +02:00
{
2019-11-27 14:07:09 +02:00
auto m = RPC : : HttpConnection : : stats ( ) . toMap ( ) ;
2019-11-13 00:57:20 +02:00
m [ " lastPeerError " ] = badAuth ? " Auth Failure " : lastPeerError ;
2019-11-17 12:38:36 +02:00
m . remove ( " nErrorsSent " ) ; // should always be 0
m . remove ( " nNotificationsSent " ) ; // again, 0
m . remove ( " nResultsSent " ) ; // again, 0
2019-11-16 22:27:21 +02:00
return m ;
2019-11-13 00:57:20 +02:00
}
2019-11-12 22:14:04 +02:00
2019-11-30 12:29:32 +02:00
BitcoinD : : BitcoinD ( const QHostAddress & host , quint16 port , const QString & user , const QString & pass , qint64 maxBuffer )
: RPC : : HttpConnection ( RPC : : MethodMap { } , newId ( ) , nullptr , maxBuffer ) , host ( host ) , port ( port )
2019-11-12 22:14:04 +02:00
{
static int N = 1 ;
2019-11-13 00:57:20 +02:00
setObjectName ( QString ( " BitcoinD.%1 " ) . arg ( N + + ) ) ;
2019-11-12 22:14:04 +02:00
_thread . setObjectName ( objectName ( ) ) ;
setAuth ( user , pass ) ;
setV1 ( true ) ; // bitcoind uses jsonrpc v1
pingtime_ms = 10000 ;
stale_threshold = pingtime_ms * 2 ;
2019-11-16 20:17:18 +02:00
connectMiscSignals ( ) ;
2019-11-12 22:14:04 +02:00
}
BitcoinD : : ~ BitcoinD ( )
{
stop ( ) ;
}
2019-11-16 20:17:18 +02:00
void BitcoinD : : connectMiscSignals ( )
{
connect ( this , & BitcoinD : : gotMessage , this , [ this ] {
// this hook emits "authenticated" as soon as we get a good result message via 'do_ping' initiated from 'on_connected' below
if ( needAuth | | badAuth ) {
needAuth = badAuth = false ;
emit authenticated ( this ) ;
}
} ) ;
}
bool BitcoinD : : isGood ( ) const
{
return ! badAuth & & ! needAuth & & RPC : : HttpConnection : : isGood ( ) ;
}
2019-11-12 22:14:04 +02:00
void BitcoinD : : on_started ( )
{
ThreadObjectMixin : : on_started ( ) ;
2019-11-12 22:42:16 +02:00
2019-11-12 23:19:46 +02:00
{ // setup the "reconnect timer"
2019-11-18 09:47:18 +02:00
constexpr auto reconnectTimer = " reconnectTimer " ;
2019-11-12 22:42:16 +02:00
const auto SetTimer = [ this ] {
2019-11-16 20:17:18 +02:00
callOnTimerSoon ( 5000 , reconnectTimer , [ this ] {
2019-11-12 22:42:16 +02:00
if ( ! isGood ( ) ) {
Debug ( ) < < prettyName ( ) < < " reconnecting... " ;
reconnect ( ) ;
2019-11-16 20:17:18 +02:00
return true ; // keep the timer alive
2019-11-12 22:42:16 +02:00
}
2019-11-16 20:17:18 +02:00
return false ; // kill timer
2019-11-12 22:42:16 +02:00
} ) ;
} ;
2019-11-12 23:19:46 +02:00
conns + = connect ( this , & BitcoinD : : lostConnection , this , [ SetTimer ] {
2019-11-12 22:42:16 +02:00
Log ( ) < < " Lost connection to bitcoind, will retry every 5 seconds ... " ;
SetTimer ( ) ;
} ) ;
2019-11-13 00:57:20 +02:00
conns + = connect ( this , & BitcoinD : : authFailure , this , [ SetTimer , this ] {
2019-11-12 22:42:16 +02:00
Error ( ) < < " Authentication to bitcoind rpc failed. Please check the rpcuser and rpcpass are correct and restart! " ;
2019-11-13 00:57:20 +02:00
badAuth = true ;
2019-11-12 22:42:16 +02:00
SetTimer ( ) ;
} ) ;
2019-11-16 20:17:18 +02:00
conns + = connect ( this , & BitcoinD : : authenticated , this , [ this ] { stopTimer ( reconnectTimer ) ; } ) ;
2019-11-12 22:42:16 +02:00
SetTimer ( ) ;
}
2019-11-12 22:14:04 +02:00
reconnect ( ) ;
}
void BitcoinD : : reconnect ( )
{
if ( socket ) delete socket ;
socket = new QTcpSocket ( this ) ;
socketConnectSignals ( ) ;
socket - > connectToHost ( host , port ) ;
}
void BitcoinD : : on_connected ( )
{
RPC : : HttpConnection : : on_connected ( ) ;
lastGood = Util : : getTime ( ) ;
2019-11-13 00:57:20 +02:00
nSent = nReceived = 0 ;
lastPeerError . clear ( ) ;
lastSocketError . clear ( ) ;
badAuth = false ;
2019-11-16 20:17:18 +02:00
needAuth = true ;
2019-11-12 22:14:04 +02:00
emit connected ( this ) ;
2019-11-16 20:17:18 +02:00
// note that the 'authenticated' signal is only emitted after good auth is confirmed via the reply from the do_ping below
2019-11-12 22:14:04 +02:00
do_ping ( ) ;
}
void BitcoinD : : do_ping ( )
{
if ( isStale ( ) ) {
Debug ( ) < < " Stale connection, reconnecting. " ;
reconnect ( ) ;
} else
2019-11-18 14:32:48 +02:00
emit sendRequest ( newId ( ) , " ping " ) ;
2019-11-12 22:14:04 +02:00
}