#include "ApiServer.h" #include #include #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(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 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 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(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 // std::vector 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(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) { 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( _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 << "" << "" << "" << "" << "" << "" << jStat["host"]["name"].asString() << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << ""; /* 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 << ""; // Open row _ret << ""; _ret << ""; _ret << ""; _ret << ""; _ret << ""; _ret << ""; _ret << ""; _ret << ""; _ret << ""; _ret << ""; // Close row } _ret << ""; /* Summarize */ _ret << ""; _ret << "
" << jStat["host"]["version"].asString() << " - " << setw(hoursSize) << hours << ":" << setw(2) << setfill('0') << fixed << minutes << "
Pool: " << jStat["connection"]["uri"].asString() << "
PCIDeviceModePausedHash RateSolutionsTemp.Fan %Power
" << device["hardware"]["pci"].asString() << "" << device["hardware"]["name"].asString() << "" << device["_mode"].asString() << "" << (device["mining"]["paused"].asBool() ? device["mining"]["pause_reason"].asString() : "No") << "" << dev::getFormattedHashes(hashrate) << "" << device["mining"]["shares"][0].asString() << "" << device["hardware"]["sensors"][0].asString() << "" << device["hardware"]["sensors"][1].asString() << "" << device["hardware"]["sensors"][2].asString() << "
Total" << dev::getFormattedHashes(total_hashrate) << "" << total_solutions << "" << setprecision(2) << total_power << "
"; 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::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(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 : 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; }