1273 lines
43 KiB
C++
1273 lines
43 KiB
C++
#include "ApiServer.h"
|
|
|
|
#include <progminer/buildinfo.h>
|
|
|
|
#include <libethcore/Farm.h>
|
|
|
|
#ifndef HOST_NAME_MAX
|
|
#define HOST_NAME_MAX 255
|
|
#endif
|
|
|
|
// Define grayscale palette
|
|
#define HTTP_HDR0_COLOR "#e8e8e8"
|
|
#define HTTP_HDR1_COLOR "#f0f0f0"
|
|
#define HTTP_ROW0_COLOR "#f8f8f8"
|
|
#define HTTP_ROW1_COLOR "#ffffff"
|
|
#define HTTP_ROWRED_COLOR "#f46542"
|
|
|
|
|
|
/* helper functions getting values from a JSON request */
|
|
static bool getRequestValue(const char* membername, bool& refValue, Json::Value& jRequest,
|
|
bool optional, Json::Value& jResponse)
|
|
{
|
|
if (!jRequest.isMember(membername))
|
|
{
|
|
if (!optional)
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Missing '") + std::string(membername) + std::string("'");
|
|
}
|
|
return optional;
|
|
}
|
|
if (!jRequest[membername].isBool())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Invalid type of value '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
if (jRequest[membername].empty())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Empty '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
refValue = jRequest[membername].asBool();
|
|
return true;
|
|
}
|
|
|
|
static bool getRequestValue(const char* membername, unsigned& refValue, Json::Value& jRequest,
|
|
bool optional, Json::Value& jResponse)
|
|
{
|
|
if (!jRequest.isMember(membername))
|
|
{
|
|
if (!optional)
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Missing '") + std::string(membername) + std::string("'");
|
|
}
|
|
return optional;
|
|
}
|
|
if (!jRequest[membername].isUInt())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Invalid type of value '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
if (jRequest[membername].empty())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Empty '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
refValue = jRequest[membername].asUInt();
|
|
return true;
|
|
}
|
|
|
|
static bool getRequestValue(const char* membername, uint64_t& refValue, Json::Value& jRequest,
|
|
bool optional, Json::Value& jResponse)
|
|
{
|
|
if (!jRequest.isMember(membername))
|
|
{
|
|
if (!optional)
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Missing '") + std::string(membername) + std::string("'");
|
|
}
|
|
return optional;
|
|
}
|
|
/* as there is no isUInt64() function we can not check the type */
|
|
if (jRequest[membername].empty())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Empty '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
try
|
|
{
|
|
refValue = jRequest[membername].asUInt64();
|
|
}
|
|
catch (...)
|
|
{
|
|
jRequest["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Bad value in '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool getRequestValue(const char* membername, Json::Value& refValue, Json::Value& jRequest,
|
|
bool optional, Json::Value& jResponse)
|
|
{
|
|
if (!jRequest.isMember(membername))
|
|
{
|
|
if (!optional)
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Missing '") + std::string(membername) + std::string("'");
|
|
}
|
|
return optional;
|
|
}
|
|
if (!jRequest[membername].isObject())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Invalid type of value '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
if (jRequest[membername].empty())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Empty '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
refValue = jRequest[membername];
|
|
return true;
|
|
}
|
|
|
|
static bool getRequestValue(const char* membername, std::string& refValue, Json::Value& jRequest,
|
|
bool optional, Json::Value& jResponse)
|
|
{
|
|
if (!jRequest.isMember(membername))
|
|
{
|
|
if (!optional)
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Missing '") + std::string(membername) + std::string("'");
|
|
}
|
|
return optional;
|
|
}
|
|
if (!jRequest[membername].isString())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Invalid type of value '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
if (jRequest[membername].empty())
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] =
|
|
std::string("Empty '") + std::string(membername) + std::string("'");
|
|
return false;
|
|
}
|
|
refValue = jRequest[membername].asString();
|
|
return true;
|
|
}
|
|
|
|
static bool checkApiWriteAccess(bool is_read_only, Json::Value& jResponse)
|
|
{
|
|
if (is_read_only)
|
|
{
|
|
jResponse["error"]["code"] = -32601;
|
|
jResponse["error"]["message"] = "Method not available";
|
|
}
|
|
return !is_read_only;
|
|
}
|
|
|
|
static bool parseRequestId(Json::Value& jRequest, Json::Value& jResponse)
|
|
{
|
|
const char* membername = "id";
|
|
|
|
// NOTE: all errors have the same code (-32600) indicating this is an invalid request
|
|
|
|
// be sure id is there and it's not empty, otherwise raise an error
|
|
if (!jRequest.isMember(membername) || jRequest[membername].empty())
|
|
{
|
|
jResponse[membername] = Json::nullValue;
|
|
jResponse["error"]["code"] = -32600;
|
|
jResponse["error"]["message"] = "Invalid Request (missing or empty id)";
|
|
return false;
|
|
}
|
|
|
|
// try to parse id as Uint
|
|
if (jRequest[membername].isUInt())
|
|
{
|
|
jResponse[membername] = jRequest[membername].asUInt();
|
|
return true;
|
|
}
|
|
|
|
// try to parse id as String
|
|
if (jRequest[membername].isString())
|
|
{
|
|
jResponse[membername] = jRequest[membername].asString();
|
|
return true;
|
|
}
|
|
|
|
// id has invalid type
|
|
jResponse[membername] = Json::nullValue;
|
|
jResponse["error"]["code"] = -32600;
|
|
jResponse["error"]["message"] = "Invalid Request (id has invalid type)";
|
|
return false;
|
|
}
|
|
|
|
ApiServer::ApiServer(string address, int portnum, string password)
|
|
: m_password(std::move(password)),
|
|
m_address(address),
|
|
m_acceptor(g_io_service),
|
|
m_io_strand(g_io_service)
|
|
{
|
|
if (portnum < 0)
|
|
{
|
|
m_portnumber = -portnum;
|
|
m_readonly = true;
|
|
}
|
|
else
|
|
{
|
|
m_portnumber = portnum;
|
|
m_readonly = false;
|
|
}
|
|
}
|
|
|
|
void ApiServer::start()
|
|
{
|
|
// cnote << "ApiServer::start";
|
|
if (m_portnumber == 0)
|
|
return;
|
|
|
|
tcp::endpoint endpoint(boost::asio::ip::address::from_string(m_address), m_portnumber);
|
|
|
|
// Try to bind to port number
|
|
// if exception occurs it may be due to the fact that
|
|
// requested port is already in use by another service
|
|
try
|
|
{
|
|
m_acceptor.open(endpoint.protocol());
|
|
m_acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
|
|
m_acceptor.bind(endpoint);
|
|
m_acceptor.listen(64);
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
cwarn << "Could not start API server on port: " +
|
|
to_string(m_acceptor.local_endpoint().port());
|
|
cwarn << "Ensure port is not in use by another service";
|
|
return;
|
|
}
|
|
|
|
cnote << "Api server listening on port " + to_string(m_acceptor.local_endpoint().port())
|
|
<< (m_password.empty() ? "." : ". Authentication needed.");
|
|
m_workThread = std::thread{boost::bind(&ApiServer::begin_accept, this)};
|
|
m_running.store(true, std::memory_order_relaxed);
|
|
}
|
|
|
|
void ApiServer::stop()
|
|
{
|
|
// Exit if not started
|
|
if (!m_running.load(std::memory_order_relaxed))
|
|
return;
|
|
|
|
m_acceptor.cancel();
|
|
m_acceptor.close();
|
|
m_workThread.join();
|
|
m_running.store(false, std::memory_order_relaxed);
|
|
|
|
// Dispose all sessions (if any)
|
|
m_sessions.clear();
|
|
}
|
|
|
|
void ApiServer::begin_accept()
|
|
{
|
|
if (!isRunning())
|
|
return;
|
|
|
|
auto session =
|
|
std::make_shared<ApiConnection>(m_io_strand, ++lastSessionId, m_readonly, m_password);
|
|
m_acceptor.async_accept(
|
|
session->socket(), m_io_strand.wrap(boost::bind(&ApiServer::handle_accept, this, session,
|
|
boost::asio::placeholders::error)));
|
|
}
|
|
|
|
void ApiServer::handle_accept(std::shared_ptr<ApiConnection> session, boost::system::error_code ec)
|
|
{
|
|
// Start new connection
|
|
// cnote << "ApiServer::handle_accept";
|
|
if (!ec)
|
|
{
|
|
session->onDisconnected([&](int id) {
|
|
// Destroy pointer to session
|
|
auto it = find_if(m_sessions.begin(), m_sessions.end(),
|
|
[&id](const std::shared_ptr<ApiConnection> session) {
|
|
return session->getId() == id;
|
|
});
|
|
if (it != m_sessions.end())
|
|
{
|
|
auto index = std::distance(m_sessions.begin(), it);
|
|
m_sessions.erase(m_sessions.begin() + index);
|
|
}
|
|
});
|
|
m_sessions.push_back(session);
|
|
cnote << "New API session from " << session->socket().remote_endpoint();
|
|
session->start();
|
|
}
|
|
else
|
|
{
|
|
session.reset();
|
|
}
|
|
|
|
// Resubmit new accept
|
|
begin_accept();
|
|
}
|
|
|
|
void ApiConnection::disconnect()
|
|
{
|
|
// cnote << "ApiConnection::disconnect";
|
|
|
|
// Cancel pending operations
|
|
m_socket.cancel();
|
|
|
|
if (m_socket.is_open())
|
|
{
|
|
boost::system::error_code ec;
|
|
m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
|
|
m_socket.close(ec);
|
|
}
|
|
|
|
if (m_onDisconnected)
|
|
{
|
|
m_onDisconnected(this->getId());
|
|
}
|
|
}
|
|
|
|
ApiConnection::ApiConnection(
|
|
boost::asio::io_service::strand& _strand, int id, bool readonly, string password)
|
|
: m_sessionId(id),
|
|
m_socket(g_io_service),
|
|
m_io_strand(_strand),
|
|
m_readonly(readonly),
|
|
m_password(std::move(password))
|
|
{
|
|
m_jSwBuilder.settings_["indentation"] = "";
|
|
if (!m_password.empty())
|
|
m_is_authenticated = false;
|
|
}
|
|
|
|
void ApiConnection::start()
|
|
{
|
|
// cnote << "ApiConnection::start";
|
|
recvSocketData();
|
|
}
|
|
|
|
void ApiConnection::processRequest(Json::Value& jRequest, Json::Value& jResponse)
|
|
{
|
|
jResponse["jsonrpc"] = "2.0";
|
|
|
|
// Strict sanity checks over jsonrpc v2
|
|
if (!parseRequestId(jRequest, jResponse))
|
|
return;
|
|
|
|
std::string jsonrpc;
|
|
std::string _method;
|
|
if (!getRequestValue("jsonrpc", jsonrpc, jRequest, false, jResponse) || jsonrpc != "2.0" ||
|
|
!getRequestValue("method", _method, jRequest, false, jResponse))
|
|
{
|
|
jResponse["error"]["code"] = -32600;
|
|
jResponse["error"]["message"] = "Invalid Request";
|
|
return;
|
|
}
|
|
|
|
// Check authentication
|
|
if (!m_is_authenticated || _method == "api_authorize")
|
|
{
|
|
if (_method != "api_authorize")
|
|
{
|
|
// Use error code like http 403 Forbidden
|
|
jResponse["error"]["code"] = -403;
|
|
jResponse["error"]["message"] = "Authorization needed";
|
|
return;
|
|
}
|
|
|
|
m_is_authenticated =
|
|
false; /* we allow api_authorize method even if already authenticated */
|
|
|
|
Json::Value jRequestParams;
|
|
if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
|
|
return;
|
|
|
|
std::string psw;
|
|
if (!getRequestValue("psw", psw, jRequestParams, false, jResponse))
|
|
return;
|
|
|
|
// max password length that we actually verify
|
|
// (this limit can be removed by introducing a collision-resistant compressing hash,
|
|
// like blake2b/sha3, but 500 should suffice and is much easier to implement)
|
|
const int max_length = 500;
|
|
char input_copy[max_length] = {0};
|
|
char password_copy[max_length] = {0};
|
|
// note: copy() is not O(1) , but i don't think it matters
|
|
psw.copy(&input_copy[0], max_length);
|
|
// ps, the following line can be optimized to only run once on startup and thus save a
|
|
// minuscule amount of cpu cycles.
|
|
m_password.copy(&password_copy[0], max_length);
|
|
int result = 0;
|
|
for (int i = 0; i < max_length; ++i)
|
|
{
|
|
result |= input_copy[i] ^ password_copy[i];
|
|
}
|
|
|
|
if (result == 0)
|
|
{
|
|
m_is_authenticated = true;
|
|
}
|
|
else
|
|
{
|
|
// Use error code like http 401 Unauthorized
|
|
jResponse["error"]["code"] = -401;
|
|
jResponse["error"]["message"] = "Invalid password";
|
|
cerr << "API : Invalid password provided.";
|
|
// Should we close the connection in the outer function after invalid password ?
|
|
}
|
|
/*
|
|
* possible wait here a fixed time of eg 10s before respond after 5 invalid
|
|
authentications were submitted to prevent brute force password attacks.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
assert(m_is_authenticated);
|
|
cnote << "API : Method " << _method << " requested";
|
|
if (_method == "miner_getstat1")
|
|
{
|
|
jResponse["result"] = getMinerStat1();
|
|
}
|
|
|
|
else if (_method == "miner_getstatdetail")
|
|
{
|
|
jResponse["result"] = getMinerStatDetail();
|
|
}
|
|
|
|
else if (_method == "miner_shuffle")
|
|
{
|
|
// Gives nonce scrambler a new range
|
|
jResponse["result"] = true;
|
|
Farm::f().shuffle();
|
|
}
|
|
|
|
else if (_method == "miner_ping")
|
|
{
|
|
// Replies back to (check for liveness)
|
|
jResponse["result"] = "pong";
|
|
}
|
|
|
|
else if (_method == "miner_restart")
|
|
{
|
|
// Send response to client of success
|
|
// and invoke an async restart
|
|
// to prevent locking
|
|
if (!checkApiWriteAccess(m_readonly, jResponse))
|
|
return;
|
|
jResponse["result"] = true;
|
|
Farm::f().restart_async();
|
|
}
|
|
|
|
else if (_method == "miner_reboot")
|
|
{
|
|
if (!checkApiWriteAccess(m_readonly, jResponse))
|
|
return;
|
|
|
|
jResponse["result"] = Farm::f().reboot({{"api_miner_reboot"}});
|
|
}
|
|
|
|
else if (_method == "miner_getconnections")
|
|
{
|
|
// Returns a list of configured pools
|
|
jResponse["result"] = PoolManager::p().getConnectionsJson();
|
|
}
|
|
|
|
else if (_method == "miner_addconnection")
|
|
{
|
|
if (!checkApiWriteAccess(m_readonly, jResponse))
|
|
return;
|
|
|
|
Json::Value jRequestParams;
|
|
if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
|
|
return;
|
|
|
|
std::string sUri;
|
|
if (!getRequestValue("uri", sUri, jRequestParams, false, jResponse))
|
|
return;
|
|
|
|
try
|
|
{
|
|
// If everything ok then add this new uri
|
|
PoolManager::p().addConnection(sUri);
|
|
jResponse["result"] = true;
|
|
}
|
|
catch (...)
|
|
{
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] = "Bad URI : " + sUri;
|
|
}
|
|
}
|
|
|
|
else if (_method == "miner_setactiveconnection")
|
|
{
|
|
if (!checkApiWriteAccess(m_readonly, jResponse))
|
|
return;
|
|
|
|
Json::Value jRequestParams;
|
|
if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
|
|
return;
|
|
if (jRequestParams.isMember("index"))
|
|
{
|
|
unsigned index;
|
|
if (getRequestValue("index", index, jRequestParams, false, jResponse))
|
|
{
|
|
try
|
|
{
|
|
PoolManager::p().setActiveConnection(index);
|
|
}
|
|
catch (const std::exception& _ex)
|
|
{
|
|
std::string what = _ex.what();
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] = what;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] = "Invalid index";
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string uri;
|
|
if (getRequestValue("URI", uri, jRequestParams, false, jResponse))
|
|
{
|
|
try
|
|
{
|
|
PoolManager::p().setActiveConnection(uri);
|
|
}
|
|
catch (const std::exception& _ex)
|
|
{
|
|
std::string what = _ex.what();
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] = what;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] = "Invalid index";
|
|
return;
|
|
}
|
|
}
|
|
jResponse["result"] = true;
|
|
}
|
|
|
|
else if (_method == "miner_removeconnection")
|
|
{
|
|
if (!checkApiWriteAccess(m_readonly, jResponse))
|
|
return;
|
|
|
|
Json::Value jRequestParams;
|
|
if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
|
|
return;
|
|
|
|
unsigned index;
|
|
if (!getRequestValue("index", index, jRequestParams, false, jResponse))
|
|
return;
|
|
|
|
try
|
|
{
|
|
PoolManager::p().removeConnection(index);
|
|
jResponse["result"] = true;
|
|
}
|
|
catch (const std::exception& _ex)
|
|
{
|
|
std::string what = _ex.what();
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] = what;
|
|
return;
|
|
}
|
|
}
|
|
|
|
else if (_method == "miner_getscramblerinfo")
|
|
{
|
|
jResponse["result"] = Farm::f().get_nonce_scrambler_json();
|
|
}
|
|
|
|
else if (_method == "miner_setscramblerinfo")
|
|
{
|
|
if (!checkApiWriteAccess(m_readonly, jResponse))
|
|
return;
|
|
|
|
Json::Value jRequestParams;
|
|
if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
|
|
return;
|
|
|
|
bool any_value_provided = false;
|
|
uint64_t nonce = Farm::f().get_nonce_scrambler();
|
|
unsigned exp = Farm::f().get_segment_width();
|
|
|
|
if (jRequestParams.isMember("noncescrambler"))
|
|
{
|
|
string nonceHex;
|
|
|
|
any_value_provided = true;
|
|
|
|
nonceHex = jRequestParams["noncescrambler"].asString();
|
|
if (nonceHex.substr(0, 2) == "0x")
|
|
{
|
|
try
|
|
{
|
|
nonce = std::stoul(nonceHex, nullptr, 16);
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] = "Invalid nonce";
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// as we already know there is a "noncescrambler" element we can use optional=false
|
|
if (!getRequestValue("noncescrambler", nonce, jRequestParams, false, jResponse))
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (jRequestParams.isMember("segmentwidth"))
|
|
{
|
|
any_value_provided = true;
|
|
if (!getRequestValue("segmentwidth", exp, jRequestParams, false, jResponse))
|
|
return;
|
|
}
|
|
|
|
if (!any_value_provided)
|
|
{
|
|
jResponse["error"]["code"] = -32602;
|
|
jResponse["error"]["message"] = "Missing parameters";
|
|
return;
|
|
}
|
|
|
|
if (exp < 10)
|
|
exp = 10; // Not below
|
|
if (exp > 50)
|
|
exp = 40; // Not above
|
|
Farm::f().set_nonce_scrambler(nonce);
|
|
Farm::f().set_nonce_segment_width(exp);
|
|
jResponse["result"] = true;
|
|
}
|
|
|
|
else if (_method == "miner_pausegpu")
|
|
{
|
|
if (!checkApiWriteAccess(m_readonly, jResponse))
|
|
return;
|
|
|
|
Json::Value jRequestParams;
|
|
if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
|
|
return;
|
|
|
|
unsigned index;
|
|
if (!getRequestValue("index", index, jRequestParams, false, jResponse))
|
|
return;
|
|
|
|
bool pause;
|
|
if (!getRequestValue("pause", pause, jRequestParams, false, jResponse))
|
|
return;
|
|
|
|
auto const& miner = Farm::f().getMiner(index);
|
|
if (miner)
|
|
{
|
|
if (pause)
|
|
miner->pause(MinerPauseEnum::PauseDueToAPIRequest);
|
|
else
|
|
miner->resume(MinerPauseEnum::PauseDueToAPIRequest);
|
|
|
|
jResponse["result"] = true;
|
|
}
|
|
else
|
|
{
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] = "Index out of bounds";
|
|
return;
|
|
}
|
|
}
|
|
|
|
else if (_method == "miner_setverbosity")
|
|
{
|
|
if (!checkApiWriteAccess(m_readonly, jResponse))
|
|
return;
|
|
|
|
Json::Value jRequestParams;
|
|
if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
|
|
return;
|
|
|
|
unsigned verbosity;
|
|
if (!getRequestValue("verbosity", verbosity, jRequestParams, false, jResponse))
|
|
return;
|
|
|
|
if (verbosity >= LOG_NEXT)
|
|
{
|
|
jResponse["error"]["code"] = -422;
|
|
jResponse["error"]["message"] =
|
|
"Verbosity out of bounds (0-" + to_string(LOG_NEXT - 1) + ")";
|
|
return;
|
|
}
|
|
cnote << "Setting verbosity level to " << verbosity;
|
|
g_logOptions = verbosity;
|
|
jResponse["result"] = true;
|
|
}
|
|
|
|
else
|
|
{
|
|
// Any other method not found
|
|
jResponse["error"]["code"] = -32601;
|
|
jResponse["error"]["message"] = "Method not found";
|
|
}
|
|
}
|
|
|
|
void ApiConnection::recvSocketData()
|
|
{
|
|
boost::asio::async_read(m_socket, m_recvBuffer, boost::asio::transfer_at_least(1),
|
|
m_io_strand.wrap(boost::bind(&ApiConnection::onRecvSocketDataCompleted, this,
|
|
boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)));
|
|
}
|
|
|
|
void ApiConnection::onRecvSocketDataCompleted(
|
|
const boost::system::error_code& ec, std::size_t bytes_transferred)
|
|
{
|
|
/*
|
|
Standard http request detection pattern
|
|
1st group : any UPPERCASE word
|
|
2nd group : the path
|
|
3rd group : HTTP version
|
|
*/
|
|
static std::regex http_pattern("^([A-Z]{1,6}) (\\/[\\S]*) (HTTP\\/1\\.[0-9]{1})");
|
|
std::smatch http_matches;
|
|
|
|
if (!ec && bytes_transferred > 0)
|
|
{
|
|
// Extract received message and free the buffer
|
|
std::string rx_message(
|
|
boost::asio::buffer_cast<const char*>(m_recvBuffer.data()), bytes_transferred);
|
|
m_recvBuffer.consume(bytes_transferred);
|
|
m_message.append(rx_message);
|
|
|
|
std::string line;
|
|
std::string linedelimiter;
|
|
std::size_t linedelimiteroffset;
|
|
|
|
if (m_message.size() < 4)
|
|
return; // Wait for other data to come in
|
|
|
|
if (std::regex_search(
|
|
m_message, http_matches, http_pattern, std::regex_constants::match_default))
|
|
{
|
|
// We got an HTTP request
|
|
std::string http_method = http_matches[1].str();
|
|
std::string http_path = http_matches[2].str();
|
|
std::string http_ver = http_matches[3].str();
|
|
|
|
// Do we support method ?
|
|
if (http_method != "GET")
|
|
{
|
|
std::string what = "Method " + http_method + " not allowed";
|
|
std::stringstream ss;
|
|
ss << http_ver << " "
|
|
<< "405 Method not allowed\r\n"
|
|
<< "Server: " << progminer_get_buildinfo()->project_name_with_version << "\r\n"
|
|
<< "Content-Type: text/plain\r\n"
|
|
<< "Content-Length: " << what.size() << "\r\n\r\n"
|
|
<< what << "\r\n";
|
|
sendSocketData(ss.str(), true);
|
|
m_message.clear();
|
|
return;
|
|
}
|
|
|
|
// Do we support path ?
|
|
if (http_path != "/" && http_path != "/getstat1")
|
|
{
|
|
std::string what =
|
|
"The requested resource " + http_path + " not found on this server";
|
|
std::stringstream ss;
|
|
ss << http_ver << " "
|
|
<< "404 Not Found\r\n"
|
|
<< "Server: " << progminer_get_buildinfo()->project_name_with_version << "\r\n"
|
|
<< "Content-Type: text/plain\r\n"
|
|
<< "Content-Length: " << what.size() << "\r\n\r\n"
|
|
<< what << "\r\n";
|
|
sendSocketData(ss.str(), true);
|
|
m_message.clear();
|
|
return;
|
|
}
|
|
|
|
//// Get all the lines - we actually don't care much
|
|
//// until we support other http methods or paths
|
|
//// Keep this for future use (if any)
|
|
//// Remember to #include <boost/algorithm/string.hpp>
|
|
// std::vector<std::string> lines;
|
|
// boost::split(lines, m_message, [](char _c) { return _c == '\n'; });
|
|
|
|
std::stringstream ss; // Builder of the response
|
|
|
|
if (http_method == "GET" && (http_path == "/" || http_path == "/getstat1"))
|
|
{
|
|
try
|
|
{
|
|
std::string body = getHttpMinerStatDetail();
|
|
ss.clear();
|
|
ss << http_ver << " "
|
|
<< "200 Ok Error\r\n"
|
|
<< "Server: " << progminer_get_buildinfo()->project_name_with_version
|
|
<< "\r\n"
|
|
<< "Content-Type: text/html; charset=utf-8\r\n"
|
|
<< "Content-Length: " << body.size() << "\r\n\r\n"
|
|
<< body << "\r\n";
|
|
}
|
|
catch (const std::exception& _ex)
|
|
{
|
|
std::string what = "Internal error : " + std::string(_ex.what());
|
|
ss.clear();
|
|
ss << http_ver << " "
|
|
<< "500 Internal Server Error\r\n"
|
|
<< "Server: " << progminer_get_buildinfo()->project_name_with_version
|
|
<< "\r\n"
|
|
<< "Content-Type: text/plain\r\n"
|
|
<< "Content-Length: " << what.size() << "\r\n\r\n"
|
|
<< what << "\r\n";
|
|
}
|
|
}
|
|
|
|
sendSocketData(ss.str(), true);
|
|
m_message.clear();
|
|
}
|
|
else
|
|
{
|
|
// We got a Json request
|
|
// Process each line in the transmission
|
|
linedelimiter = "\n";
|
|
|
|
linedelimiteroffset = m_message.find(linedelimiter);
|
|
while (linedelimiteroffset != string::npos)
|
|
{
|
|
if (linedelimiteroffset > 0)
|
|
{
|
|
line = m_message.substr(0, linedelimiteroffset);
|
|
boost::trim(line);
|
|
|
|
if (!line.empty())
|
|
{
|
|
// Test validity of chunk and process
|
|
Json::Value jMsg;
|
|
Json::Value jRes;
|
|
Json::Reader jRdr;
|
|
if (jRdr.parse(line, jMsg))
|
|
{
|
|
try
|
|
{
|
|
// Run in sync so no 2 different async reads may overlap
|
|
processRequest(jMsg, jRes);
|
|
}
|
|
catch (const std::exception& _ex)
|
|
{
|
|
jRes = Json::Value();
|
|
jRes["jsonrpc"] = "2.0";
|
|
jRes["id"] = Json::Value::null;
|
|
jRes["error"]["errorcode"] = "500";
|
|
jRes["error"]["message"] = _ex.what();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jRes = Json::Value();
|
|
jRes["jsonrpc"] = "2.0";
|
|
jRes["id"] = Json::Value::null;
|
|
jRes["error"]["errorcode"] = "-32700";
|
|
string what = jRdr.getFormattedErrorMessages();
|
|
boost::replace_all(what, "\n", " ");
|
|
cwarn << "API : Got invalid Json message " << what;
|
|
jRes["error"]["message"] = "Json parse error : " + what;
|
|
}
|
|
|
|
// Send response to client
|
|
sendSocketData(jRes);
|
|
}
|
|
}
|
|
|
|
// Next line (if any)
|
|
m_message.erase(0, linedelimiteroffset + 1);
|
|
linedelimiteroffset = m_message.find(linedelimiter);
|
|
}
|
|
|
|
// Eventually keep reading from socket
|
|
if (m_socket.is_open())
|
|
recvSocketData();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
disconnect();
|
|
}
|
|
}
|
|
|
|
void ApiConnection::sendSocketData(Json::Value const& jReq, bool _disconnect)
|
|
{
|
|
if (!m_socket.is_open())
|
|
return;
|
|
std::stringstream line;
|
|
line << Json::writeString(m_jSwBuilder, jReq) << std::endl;
|
|
sendSocketData(line.str(), _disconnect);
|
|
}
|
|
|
|
void ApiConnection::sendSocketData(std::string const& _s, bool _disconnect)
|
|
{
|
|
if (!m_socket.is_open())
|
|
return;
|
|
std::ostream os(&m_sendBuffer);
|
|
os << _s;
|
|
|
|
async_write(m_socket, m_sendBuffer,
|
|
m_io_strand.wrap(boost::bind(&ApiConnection::onSendSocketDataCompleted, this,
|
|
boost::asio::placeholders::error, _disconnect)));
|
|
}
|
|
|
|
void ApiConnection::onSendSocketDataCompleted(const boost::system::error_code& ec, bool _disconnect)
|
|
{
|
|
if (ec || _disconnect)
|
|
disconnect();
|
|
}
|
|
|
|
Json::Value ApiConnection::getMinerStat1()
|
|
{
|
|
auto connection = PoolManager::p().getActiveConnection();
|
|
TelemetryType t = Farm::f().Telemetry();
|
|
auto runningTime =
|
|
std::chrono::duration_cast<std::chrono::minutes>(steady_clock::now() - t.start);
|
|
|
|
|
|
ostringstream totalMhEth;
|
|
ostringstream totalMhDcr;
|
|
ostringstream detailedMhEth;
|
|
ostringstream detailedMhDcr;
|
|
ostringstream tempAndFans;
|
|
ostringstream poolAddresses;
|
|
ostringstream invalidStats;
|
|
|
|
totalMhEth << std::fixed << std::setprecision(0) << t.farm.hashrate / 1000.0f << ";"
|
|
<< t.farm.solutions.accepted << ";" << t.farm.solutions.rejected;
|
|
totalMhDcr << "0;0;0"; // DualMining not supported
|
|
invalidStats << t.farm.solutions.failed << ";0"; // Invalid + Pool switches
|
|
poolAddresses << connection->Host() << ':' << connection->Port();
|
|
invalidStats << ";0;0"; // DualMining not supported
|
|
|
|
int gpuIndex;
|
|
int numGpus = t.miners.size();
|
|
|
|
for (gpuIndex = 0; gpuIndex < numGpus; gpuIndex++)
|
|
{
|
|
detailedMhEth << std::fixed << std::setprecision(0)
|
|
<< t.miners.at(gpuIndex).hashrate / 1000.0f
|
|
<< (((numGpus - 1) > gpuIndex) ? ";" : "");
|
|
detailedMhDcr << "off"
|
|
<< (((numGpus - 1) > gpuIndex) ? ";" : ""); // DualMining not supported
|
|
}
|
|
|
|
for (gpuIndex = 0; gpuIndex < numGpus; gpuIndex++)
|
|
{
|
|
tempAndFans << t.miners.at(gpuIndex).sensors.tempC << ";"
|
|
<< t.miners.at(gpuIndex).sensors.fanP
|
|
<< (((numGpus - 1) > gpuIndex) ? ";" : ""); // Fetching Temp and Fans
|
|
}
|
|
|
|
Json::Value jRes;
|
|
|
|
jRes[0] = progminer_get_buildinfo()->project_name_with_version; // miner version.
|
|
jRes[1] = toString(runningTime.count()); // running time, in minutes.
|
|
jRes[2] = totalMhEth.str(); // total ETH hashrate in MH/s, number of ETH shares, number of ETH
|
|
// rejected shares.
|
|
jRes[3] = detailedMhEth.str(); // detailed ETH hashrate for all GPUs.
|
|
jRes[4] = totalMhDcr.str(); // total DCR hashrate in MH/s, number of DCR shares, number of DCR
|
|
// rejected shares.
|
|
jRes[5] = detailedMhDcr.str(); // detailed DCR hashrate for all GPUs.
|
|
jRes[6] = tempAndFans.str(); // Temperature and Fan speed(%) pairs for all GPUs.
|
|
jRes[7] =
|
|
poolAddresses.str(); // current mining pool. For dual mode, there will be two pools here.
|
|
jRes[8] = invalidStats.str(); // number of ETH invalid shares, number of ETH pool switches,
|
|
// number of DCR invalid shares, number of DCR pool switches.
|
|
|
|
return jRes;
|
|
}
|
|
|
|
Json::Value ApiConnection::getMinerStatDetailPerMiner(
|
|
const TelemetryType& _t, std::shared_ptr<Miner> _miner)
|
|
{
|
|
unsigned _index = _miner->Index();
|
|
std::chrono::steady_clock::time_point _now = std::chrono::steady_clock::now();
|
|
|
|
Json::Value jRes;
|
|
DeviceDescriptor minerDescriptor = _miner->getDescriptor();
|
|
|
|
jRes["_index"] = _index;
|
|
jRes["_mode"] =
|
|
(minerDescriptor.subscriptionType == DeviceSubscriptionTypeEnum::Cuda ? "CUDA" : "OpenCL");
|
|
|
|
/* Hardware Info */
|
|
Json::Value hwinfo;
|
|
hwinfo["pci"] = minerDescriptor.uniqueId;
|
|
hwinfo["type"] =
|
|
(minerDescriptor.type == DeviceTypeEnum::Gpu ?
|
|
"GPU" :
|
|
(minerDescriptor.type == DeviceTypeEnum::Accelerator ? "ACCELERATOR" : "CPU"));
|
|
ostringstream ss;
|
|
ss << (minerDescriptor.clDetected ? minerDescriptor.clName : minerDescriptor.cuName) << " "
|
|
<< dev::getFormattedMemory((double)minerDescriptor.totalMemory);
|
|
hwinfo["name"] = ss.str();
|
|
|
|
/* Hardware Sensors*/
|
|
Json::Value sensors = Json::Value(Json::arrayValue);
|
|
|
|
sensors.append(_t.miners.at(_index).sensors.tempC);
|
|
sensors.append(_t.miners.at(_index).sensors.fanP);
|
|
sensors.append(_t.miners.at(_index).sensors.powerW);
|
|
|
|
hwinfo["sensors"] = sensors;
|
|
|
|
/* Mining Info */
|
|
Json::Value mininginfo;
|
|
Json::Value jshares = Json::Value(Json::arrayValue);
|
|
Json::Value jsegment = Json::Value(Json::arrayValue);
|
|
jshares.append(_t.miners.at(_index).solutions.accepted);
|
|
jshares.append(_t.miners.at(_index).solutions.rejected);
|
|
jshares.append(_t.miners.at(_index).solutions.failed);
|
|
|
|
auto solution_lastupdated = std::chrono::duration_cast<std::chrono::seconds>(
|
|
_now - _t.miners.at(_index).solutions.tstamp);
|
|
jshares.append(uint64_t(solution_lastupdated.count())); // interval in seconds from last found
|
|
// share
|
|
|
|
mininginfo["shares"] = jshares;
|
|
mininginfo["paused"] = _miner->paused();
|
|
mininginfo["pause_reason"] = _miner->paused() ? _miner->pausedString() : Json::Value::null;
|
|
|
|
/* Nonce infos */
|
|
auto segment_width = Farm::f().get_segment_width();
|
|
uint64_t gpustartnonce = Farm::f().get_nonce_scrambler() + ((uint64_t)_index << segment_width);
|
|
jsegment.append(toHex(uint64_t(gpustartnonce), HexPrefix::Add));
|
|
jsegment.append(toHex(uint64_t(gpustartnonce + (1LL << segment_width)), HexPrefix::Add));
|
|
mininginfo["segment"] = jsegment;
|
|
|
|
/* Hash & Share infos */
|
|
mininginfo["hashrate"] = toHex((uint32_t)_t.miners.at(_index).hashrate, HexPrefix::Add);
|
|
|
|
jRes["hardware"] = hwinfo;
|
|
jRes["mining"] = mininginfo;
|
|
|
|
return jRes;
|
|
}
|
|
|
|
std::string ApiConnection::getHttpMinerStatDetail()
|
|
{
|
|
Json::Value jStat = getMinerStatDetail();
|
|
uint64_t durationSeconds = jStat["host"]["runtime"].asUInt64();
|
|
int hours = (int)(durationSeconds / 3600);
|
|
durationSeconds -= (hours * 3600);
|
|
int minutes = (int)(durationSeconds / 60);
|
|
int hoursSize = (hours > 9 ? (hours > 99 ? 3 : 2) : 1);
|
|
|
|
/* Build up header*/
|
|
std::stringstream _ret;
|
|
_ret << "<!doctype html>"
|
|
<< "<html lang=en>"
|
|
<< "<head>"
|
|
<< "<meta charset=utf-8>"
|
|
<< "<meta http-equiv=\"refresh\" content=\"30\">"
|
|
<< "<title>" << jStat["host"]["name"].asString() << "</title>"
|
|
<< "<style>"
|
|
<< "body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,"
|
|
<< "\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5;"
|
|
<< "text-align:center;}"
|
|
<< "table,td,th{border:1px inset #000;}"
|
|
<< "table{border-spacing:0;}"
|
|
<< "td,th{padding:3px;}"
|
|
<< "tbody tr:nth-child(even){background-color:" << HTTP_ROW0_COLOR << ";}"
|
|
<< "tbody tr:nth-child(odd){background-color:" << HTTP_ROW1_COLOR << ";}"
|
|
<< ".mx-auto{margin-left:auto;margin-right:auto;}"
|
|
<< ".bg-header1{background-color:" << HTTP_HDR1_COLOR << ";}"
|
|
<< ".bg-header0{background-color:" << HTTP_HDR0_COLOR << ";}"
|
|
<< ".bg-red{color:" << HTTP_ROWRED_COLOR << ";}"
|
|
<< ".right{text-align: right;}"
|
|
<< "</style>"
|
|
<< "<meta http-equiv=refresh content=30>"
|
|
<< "</head>"
|
|
<< "<body>"
|
|
<< "<table class=mx-auto>"
|
|
<< "<thead>"
|
|
<< "<tr class=bg-header1>"
|
|
<< "<th colspan=9>" << jStat["host"]["version"].asString() << " - " << setw(hoursSize)
|
|
<< hours << ":" << setw(2) << setfill('0') << fixed << minutes
|
|
<< "<br>Pool: " << jStat["connection"]["uri"].asString() << "</th>"
|
|
<< "</tr>"
|
|
<< "<tr class=bg-header0>"
|
|
<< "<th>PCI</th>"
|
|
<< "<th>Device</th>"
|
|
<< "<th>Mode</th>"
|
|
<< "<th>Paused</th>"
|
|
<< "<th class=right>Hash Rate</th>"
|
|
<< "<th class=right>Solutions</th>"
|
|
<< "<th class=right>Temp.</th>"
|
|
<< "<th class=right>Fan %</th>"
|
|
<< "<th class=right>Power</th>"
|
|
<< "</tr>"
|
|
<< "</thead><tbody>";
|
|
|
|
/* Loop miners */
|
|
double total_hashrate = 0;
|
|
double total_power = 0;
|
|
unsigned int total_solutions = 0;
|
|
|
|
for (Json::Value::ArrayIndex i = 0; i != jStat["devices"].size(); i++)
|
|
{
|
|
Json::Value device = jStat["devices"][i];
|
|
double hashrate = std::stoul(device["mining"]["hashrate"].asString(), nullptr, 16);
|
|
double power = device["hardware"]["sensors"][2].asDouble();
|
|
unsigned int solutions = device["mining"]["shares"][0].asUInt();
|
|
total_hashrate += hashrate;
|
|
total_power += power;
|
|
total_solutions += solutions;
|
|
|
|
_ret << "<tr" << (device["mining"]["paused"].asBool() ? " class=\"bg-red\"" : "")
|
|
<< ">"; // Open row
|
|
|
|
_ret << "<td>" << device["hardware"]["pci"].asString() << "</td>";
|
|
_ret << "<td>" << device["hardware"]["name"].asString() << "</td>";
|
|
_ret << "<td>" << device["_mode"].asString() << "</td>";
|
|
|
|
_ret << "<td>"
|
|
<< (device["mining"]["paused"].asBool() ? device["mining"]["pause_reason"].asString() :
|
|
"No")
|
|
<< "</td>";
|
|
|
|
_ret << "<td class=right>" << dev::getFormattedHashes(hashrate) << "</td>";
|
|
|
|
_ret << "<td class=right>" << device["mining"]["shares"][0].asString() << "</td>";
|
|
_ret << "<td class=right>" << device["hardware"]["sensors"][0].asString() << "</td>";
|
|
_ret << "<td class=right>" << device["hardware"]["sensors"][1].asString() << "</td>";
|
|
_ret << "<td class=right>" << device["hardware"]["sensors"][2].asString() << "</td>";
|
|
|
|
_ret << "</tr>"; // Close row
|
|
}
|
|
_ret << "</tbody>";
|
|
|
|
/* Summarize */
|
|
_ret << "<tfoot><tr class=bg-header0><td colspan=4 class=right>Total</td><td class=right>"
|
|
<< dev::getFormattedHashes(total_hashrate) << "</td><td class=right>" << total_solutions
|
|
<< "</td><td colspan=3 class=right>" << setprecision(2) << total_power << "</td></tfoot>";
|
|
|
|
_ret << "</table></body></html>";
|
|
return _ret.str();
|
|
}
|
|
|
|
/**
|
|
* @brief Return a total and per GPU detailed list of current status
|
|
* As we return here difficulty and share counts (which are not getting resetted if we
|
|
* switch pool) the results may "lie".
|
|
* Eg: Calculating runtime, (current) difficulty and submitted shares must not match the hashrate.
|
|
* Inspired by Andrea Lanfranchi comment on issue 1232:
|
|
* https://github.com/gangnamtestnet/progminer/pull/1232#discussion_r193995891
|
|
* @return The json result
|
|
*/
|
|
Json::Value ApiConnection::getMinerStatDetail()
|
|
{
|
|
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
|
TelemetryType t = Farm::f().Telemetry();
|
|
|
|
auto runningTime = std::chrono::duration_cast<std::chrono::seconds>(
|
|
std::chrono::steady_clock::now() - t.start);
|
|
|
|
// ostringstream version;
|
|
Json::Value devices = Json::Value(Json::arrayValue);
|
|
Json::Value jRes;
|
|
|
|
/* Host Info */
|
|
Json::Value hostinfo;
|
|
hostinfo["version"] = progminer_get_buildinfo()->project_name_with_version; // miner version.
|
|
hostinfo["runtime"] = uint64_t(runningTime.count()); // running time, in seconds.
|
|
|
|
{
|
|
// Even the client should know which host was queried
|
|
char hostName[HOST_NAME_MAX + 1];
|
|
if (!gethostname(hostName, HOST_NAME_MAX + 1))
|
|
hostinfo["name"] = hostName;
|
|
else
|
|
hostinfo["name"] = Json::Value::null;
|
|
}
|
|
|
|
|
|
/* Connection info */
|
|
Json::Value connectioninfo;
|
|
auto connection = PoolManager::p().getActiveConnection();
|
|
connectioninfo["uri"] = connection->str();
|
|
connectioninfo["connected"] = PoolManager::p().isConnected();
|
|
connectioninfo["switches"] = PoolManager::p().getConnectionSwitches();
|
|
|
|
/* Mining Info */
|
|
Json::Value mininginfo;
|
|
Json::Value sharesinfo = Json::Value(Json::arrayValue);
|
|
|
|
mininginfo["hashrate"] = toHex(uint32_t(t.farm.hashrate), HexPrefix::Add);
|
|
mininginfo["epoch"] = PoolManager::p().getCurrentEpoch();
|
|
mininginfo["epoch_changes"] = PoolManager::p().getEpochChanges();
|
|
mininginfo["difficulty"] = PoolManager::p().getCurrentDifficulty();
|
|
|
|
sharesinfo.append(t.farm.solutions.accepted);
|
|
sharesinfo.append(t.farm.solutions.rejected);
|
|
sharesinfo.append(t.farm.solutions.failed);
|
|
auto solution_lastupdated =
|
|
std::chrono::duration_cast<std::chrono::seconds>(now - t.farm.solutions.tstamp);
|
|
sharesinfo.append(uint64_t(solution_lastupdated.count())); // interval in seconds from last
|
|
// found share
|
|
mininginfo["shares"] = sharesinfo;
|
|
|
|
/* Monitors Info */
|
|
Json::Value monitorinfo;
|
|
auto tstop = Farm::f().get_tstop();
|
|
if (tstop)
|
|
{
|
|
Json::Value tempsinfo = Json::Value(Json::arrayValue);
|
|
tempsinfo.append(Farm::f().get_tstart());
|
|
tempsinfo.append(tstop);
|
|
monitorinfo["temperatures"] = tempsinfo;
|
|
}
|
|
|
|
/* Devices related info */
|
|
for (shared_ptr<Miner> miner : Farm::f().getMiners())
|
|
devices.append(getMinerStatDetailPerMiner(t, miner));
|
|
|
|
jRes["devices"] = devices;
|
|
|
|
jRes["monitors"] = monitorinfo;
|
|
jRes["connection"] = connectioninfo;
|
|
jRes["host"] = hostinfo;
|
|
jRes["mining"] = mininginfo;
|
|
|
|
return jRes;
|
|
}
|