ApiServer.cpp 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127
  1. /* Copyright (C) 1883 Thomas Edison - All Rights Reserved
  2. * You may use, distribute and modify this code under the
  3. * terms of the GPLv3 license, which unfortunately won't be
  4. * written for another century.
  5. *
  6. * You should have received a copy of the LICENSE file with
  7. * this file.
  8. */
  9. #include "ApiServer.h"
  10. #include <nsfminer/buildinfo.h>
  11. #include <libeth/Farm.h>
  12. #ifndef HOST_NAME_MAX
  13. #define HOST_NAME_MAX 255
  14. #endif
  15. // Define grayscale palette
  16. #define HTTP_HDR0_COLOR "#e8e8e8"
  17. #define HTTP_HDR1_COLOR "#f0f0f0"
  18. #define HTTP_ROW0_COLOR "#f8f8f8"
  19. #define HTTP_ROW1_COLOR "#ffffff"
  20. #define HTTP_ROWRED_COLOR "#f46542"
  21. using namespace std;
  22. /* helper functions getting values from a JSON request */
  23. static bool getRequestValue(const char* membername, bool& refValue, Json::Value& jRequest, bool optional,
  24. Json::Value& jResponse) {
  25. if (!jRequest.isMember(membername)) {
  26. if (!optional) {
  27. jResponse["error"]["code"] = -32602;
  28. jResponse["error"]["message"] = string("Missing '") + string(membername) + string("'");
  29. }
  30. return optional;
  31. }
  32. if (!jRequest[membername].isBool()) {
  33. jResponse["error"]["code"] = -32602;
  34. jResponse["error"]["message"] = string("Invalid type of value '") + string(membername) + string("'");
  35. return false;
  36. }
  37. if (jRequest[membername].empty()) {
  38. jResponse["error"]["code"] = -32602;
  39. jResponse["error"]["message"] = string("Empty '") + string(membername) + string("'");
  40. return false;
  41. }
  42. refValue = jRequest[membername].asBool();
  43. return true;
  44. }
  45. static bool getRequestValue(const char* membername, unsigned& refValue, Json::Value& jRequest, bool optional,
  46. Json::Value& jResponse) {
  47. if (!jRequest.isMember(membername)) {
  48. if (!optional) {
  49. jResponse["error"]["code"] = -32602;
  50. jResponse["error"]["message"] = string("Missing '") + string(membername) + string("'");
  51. }
  52. return optional;
  53. }
  54. if (!jRequest[membername].isUInt()) {
  55. jResponse["error"]["code"] = -32602;
  56. jResponse["error"]["message"] = string("Invalid type of value '") + string(membername) + string("'");
  57. return false;
  58. }
  59. if (jRequest[membername].empty()) {
  60. jResponse["error"]["code"] = -32602;
  61. jResponse["error"]["message"] = string("Empty '") + string(membername) + string("'");
  62. return false;
  63. }
  64. refValue = jRequest[membername].asUInt();
  65. return true;
  66. }
  67. static bool getRequestValue(const char* membername, Json::Value& refValue, Json::Value& jRequest, bool optional,
  68. Json::Value& jResponse) {
  69. if (!jRequest.isMember(membername)) {
  70. if (!optional) {
  71. jResponse["error"]["code"] = -32602;
  72. jResponse["error"]["message"] = string("Missing '") + string(membername) + string("'");
  73. }
  74. return optional;
  75. }
  76. if (!jRequest[membername].isObject()) {
  77. jResponse["error"]["code"] = -32602;
  78. jResponse["error"]["message"] = string("Invalid type of value '") + string(membername) + string("'");
  79. return false;
  80. }
  81. if (jRequest[membername].empty()) {
  82. jResponse["error"]["code"] = -32602;
  83. jResponse["error"]["message"] = string("Empty '") + string(membername) + string("'");
  84. return false;
  85. }
  86. refValue = jRequest[membername];
  87. return true;
  88. }
  89. static bool getRequestValue(const char* membername, string& refValue, Json::Value& jRequest, bool optional,
  90. Json::Value& jResponse) {
  91. if (!jRequest.isMember(membername)) {
  92. if (!optional) {
  93. jResponse["error"]["code"] = -32602;
  94. jResponse["error"]["message"] = string("Missing '") + string(membername) + string("'");
  95. }
  96. return optional;
  97. }
  98. if (!jRequest[membername].isString()) {
  99. jResponse["error"]["code"] = -32602;
  100. jResponse["error"]["message"] = string("Invalid type of value '") + string(membername) + string("'");
  101. return false;
  102. }
  103. if (jRequest[membername].empty()) {
  104. jResponse["error"]["code"] = -32602;
  105. jResponse["error"]["message"] = string("Empty '") + string(membername) + string("'");
  106. return false;
  107. }
  108. refValue = jRequest[membername].asString();
  109. return true;
  110. }
  111. static bool checkApiWriteAccess(bool is_read_only, Json::Value& jResponse) {
  112. if (is_read_only) {
  113. jResponse["error"]["code"] = -32601;
  114. jResponse["error"]["message"] = "Method not available";
  115. }
  116. return !is_read_only;
  117. }
  118. static bool parseRequestId(Json::Value& jRequest, Json::Value& jResponse) {
  119. const char* membername = "id";
  120. // NOTE: all errors have the same code (-32600) indicating this is an invalid request
  121. // be sure id is there and it's not empty, otherwise raise an error
  122. if (!jRequest.isMember(membername) || jRequest[membername].empty()) {
  123. jResponse[membername] = Json::nullValue;
  124. jResponse["error"]["code"] = -32600;
  125. jResponse["error"]["message"] = "Invalid Request (missing or empty id)";
  126. return false;
  127. }
  128. // try to parse id as Uint
  129. if (jRequest[membername].isUInt()) {
  130. jResponse[membername] = jRequest[membername].asUInt();
  131. return true;
  132. }
  133. // try to parse id as String
  134. if (jRequest[membername].isString()) {
  135. jResponse[membername] = jRequest[membername].asString();
  136. return true;
  137. }
  138. // id has invalid type
  139. jResponse[membername] = Json::nullValue;
  140. jResponse["error"]["code"] = -32600;
  141. jResponse["error"]["message"] = "Invalid Request (id has invalid type)";
  142. return false;
  143. }
  144. ApiServer::ApiServer(string address, int portnum, string password)
  145. : m_password(move(password)), m_address(address), m_acceptor(g_io_service), m_io_strand(g_io_service) {
  146. if (portnum < 0) {
  147. m_portnumber = -portnum;
  148. m_readonly = true;
  149. } else {
  150. m_portnumber = portnum;
  151. m_readonly = false;
  152. }
  153. }
  154. void ApiServer::start() {
  155. // cnote << "ApiServer::start";
  156. if (m_portnumber == 0)
  157. return;
  158. tcp::endpoint endpoint(boost::asio::ip::address::from_string(m_address), m_portnumber);
  159. // Try to bind to port number
  160. // if exception occurs it may be due to the fact that
  161. // requested port is already in use by another service
  162. try {
  163. m_acceptor.open(endpoint.protocol());
  164. m_acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
  165. m_acceptor.bind(endpoint);
  166. m_acceptor.listen(64);
  167. } catch (const exception&) {
  168. cwarn << "Could not start API server on port: " + to_string(m_acceptor.local_endpoint().port());
  169. cwarn << "Ensure port is not in use by another service";
  170. return;
  171. }
  172. cnote << "Api server listening on port " + to_string(m_acceptor.local_endpoint().port())
  173. << (m_password.empty() ? "." : ". Authentication needed.");
  174. m_running.store(true, memory_order_relaxed);
  175. m_workThread = thread{boost::bind(&ApiServer::begin_accept, this)};
  176. }
  177. void ApiServer::stop() {
  178. // Exit if not started
  179. if (!m_running.load(memory_order_relaxed))
  180. return;
  181. m_acceptor.cancel();
  182. m_acceptor.close();
  183. m_workThread.join();
  184. m_running.store(false, memory_order_relaxed);
  185. // Dispose all sessions (if any)
  186. m_sessions.clear();
  187. }
  188. void ApiServer::begin_accept() {
  189. if (!isRunning())
  190. return;
  191. auto session = make_shared<ApiConnection>(m_io_strand, ++lastSessionId, m_readonly, m_password);
  192. m_acceptor.async_accept(session->socket(), m_io_strand.wrap(boost::bind(&ApiServer::handle_accept, this, session,
  193. boost::asio::placeholders::error)));
  194. }
  195. void ApiServer::handle_accept(shared_ptr<ApiConnection> session, boost::system::error_code ec) {
  196. // Start new connection
  197. // cnote << "ApiServer::handle_accept";
  198. if (!ec) {
  199. session->onDisconnected([&](int id) {
  200. // Destroy pointer to session
  201. auto it = find_if(m_sessions.begin(), m_sessions.end(),
  202. [&id](const shared_ptr<ApiConnection> session) { return session->getId() == id; });
  203. if (it != m_sessions.end()) {
  204. auto index = distance(m_sessions.begin(), it);
  205. m_sessions.erase(m_sessions.begin() + index);
  206. }
  207. });
  208. m_sessions.push_back(session);
  209. cnote << "New API session from " << session->socket().remote_endpoint();
  210. session->start();
  211. } else {
  212. session.reset();
  213. }
  214. // Resubmit new accept
  215. begin_accept();
  216. }
  217. void ApiConnection::disconnect() {
  218. // cnote << "ApiConnection::disconnect";
  219. // Cancel pending operations
  220. m_socket.cancel();
  221. if (m_socket.is_open()) {
  222. boost::system::error_code ec;
  223. m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
  224. m_socket.close(ec);
  225. }
  226. if (m_onDisconnected) {
  227. m_onDisconnected(this->getId());
  228. }
  229. }
  230. ApiConnection::ApiConnection(boost::asio::io_service::strand& _strand, int id, bool readonly, string password)
  231. : m_sessionId(id), m_socket(g_io_service), m_io_strand(_strand), m_readonly(readonly), m_password(move(password)) {
  232. m_jSwBuilder.settings_["indentation"] = "";
  233. if (!m_password.empty())
  234. m_is_authenticated = false;
  235. }
  236. void ApiConnection::start() {
  237. // cnote << "ApiConnection::start";
  238. recvSocketData();
  239. }
  240. void ApiConnection::processRequest(Json::Value& jRequest, Json::Value& jResponse) {
  241. jResponse["jsonrpc"] = "2.0";
  242. // Strict sanity checks over jsonrpc v2
  243. if (!parseRequestId(jRequest, jResponse))
  244. return;
  245. string jsonrpc;
  246. string _method;
  247. if (!getRequestValue("jsonrpc", jsonrpc, jRequest, false, jResponse) || jsonrpc != "2.0" ||
  248. !getRequestValue("method", _method, jRequest, false, jResponse)) {
  249. jResponse["error"]["code"] = -32600;
  250. jResponse["error"]["message"] = "Invalid Request";
  251. return;
  252. }
  253. // Check authentication
  254. if (!m_is_authenticated || _method == "api_authorize") {
  255. if (_method != "api_authorize") {
  256. // Use error code like http 403 Forbidden
  257. jResponse["error"]["code"] = -403;
  258. jResponse["error"]["message"] = "Authorization needed";
  259. return;
  260. }
  261. m_is_authenticated = false; /* we allow api_authorize method even if already authenticated */
  262. Json::Value jRequestParams;
  263. if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
  264. return;
  265. string psw;
  266. if (!getRequestValue("psw", psw, jRequestParams, false, jResponse))
  267. return;
  268. // max password length that we actually verify
  269. // (this limit can be removed by introducing a collision-resistant compressing hash,
  270. // like blake2b/sha3, but 500 should suffice and is much easier to implement)
  271. const int max_length = 500;
  272. char input_copy[max_length] = {0};
  273. char password_copy[max_length] = {0};
  274. // note: copy() is not O(1) , but i don't think it matters
  275. psw.copy(&input_copy[0], max_length);
  276. // ps, the following line can be optimized to only run once on startup and thus save a
  277. // minuscule amount of cpu cycles.
  278. m_password.copy(&password_copy[0], max_length);
  279. int result = 0;
  280. for (int i = 0; i < max_length; ++i)
  281. result |= input_copy[i] ^ password_copy[i];
  282. if (result == 0)
  283. m_is_authenticated = true;
  284. else {
  285. // Use error code like http 401 Unauthorized
  286. jResponse["error"]["code"] = -401;
  287. jResponse["error"]["message"] = "Invalid password";
  288. cwarn << "API : Invalid password provided.";
  289. // Should we close the connection in the outer function after invalid password ?
  290. }
  291. /*
  292. * possible wait here a fixed time of eg 10s before respond after 5 invalid
  293. authentications were submitted to prevent brute force password attacks.
  294. */
  295. return;
  296. }
  297. cnote << "API : Method " << _method << " requested";
  298. if (_method == "miner_getstat1") {
  299. jResponse["result"] = getMinerStat1();
  300. }
  301. else if (_method == "miner_getstatdetail") {
  302. jResponse["result"] = getMinerStatDetail();
  303. }
  304. else if (_method == "miner_ping") {
  305. // Replies back to (check for liveness)
  306. jResponse["result"] = "pong";
  307. }
  308. else if (_method == "miner_restart") {
  309. // Send response to client of success
  310. // and invoke an async restart
  311. // to prevent locking
  312. if (!checkApiWriteAccess(m_readonly, jResponse))
  313. return;
  314. jResponse["result"] = true;
  315. Farm::f().restart_async();
  316. }
  317. else if (_method == "miner_reboot") {
  318. if (!checkApiWriteAccess(m_readonly, jResponse))
  319. return;
  320. jResponse["result"] = Farm::f().reboot({{"api_miner_reboot"}});
  321. }
  322. else if (_method == "miner_getconnections") {
  323. // Returns a list of configured pools
  324. jResponse["result"] = PoolManager::p().getConnectionsJson();
  325. }
  326. else if (_method == "miner_addconnection") {
  327. if (!checkApiWriteAccess(m_readonly, jResponse))
  328. return;
  329. Json::Value jRequestParams;
  330. if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
  331. return;
  332. string sUri;
  333. if (!getRequestValue("uri", sUri, jRequestParams, false, jResponse))
  334. return;
  335. try {
  336. // If everything ok then add this new uri
  337. PoolManager::p().addConnection(sUri);
  338. jResponse["result"] = true;
  339. } catch (...) {
  340. jResponse["error"]["code"] = -422;
  341. jResponse["error"]["message"] = "Bad URI : " + sUri;
  342. }
  343. }
  344. else if (_method == "miner_setactiveconnection") {
  345. if (!checkApiWriteAccess(m_readonly, jResponse))
  346. return;
  347. Json::Value jRequestParams;
  348. if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
  349. return;
  350. if (jRequestParams.isMember("index")) {
  351. unsigned index;
  352. if (getRequestValue("index", index, jRequestParams, false, jResponse)) {
  353. try {
  354. PoolManager::p().setActiveConnection(index);
  355. } catch (const exception& _ex) {
  356. string what = _ex.what();
  357. jResponse["error"]["code"] = -422;
  358. jResponse["error"]["message"] = what;
  359. return;
  360. }
  361. } else {
  362. jResponse["error"]["code"] = -422;
  363. jResponse["error"]["message"] = "Invalid index";
  364. return;
  365. }
  366. } else {
  367. string uri;
  368. if (getRequestValue("URI", uri, jRequestParams, false, jResponse)) {
  369. try {
  370. PoolManager::p().setActiveConnection(uri);
  371. } catch (const exception& _ex) {
  372. string what = _ex.what();
  373. jResponse["error"]["code"] = -422;
  374. jResponse["error"]["message"] = what;
  375. return;
  376. }
  377. } else {
  378. jResponse["error"]["code"] = -422;
  379. jResponse["error"]["message"] = "Invalid index";
  380. return;
  381. }
  382. }
  383. jResponse["result"] = true;
  384. }
  385. else if (_method == "miner_removeconnection") {
  386. if (!checkApiWriteAccess(m_readonly, jResponse))
  387. return;
  388. Json::Value jRequestParams;
  389. if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
  390. return;
  391. unsigned index;
  392. if (!getRequestValue("index", index, jRequestParams, false, jResponse))
  393. return;
  394. try {
  395. PoolManager::p().removeConnection(index);
  396. jResponse["result"] = true;
  397. } catch (const exception& _ex) {
  398. string what = _ex.what();
  399. jResponse["error"]["code"] = -422;
  400. jResponse["error"]["message"] = what;
  401. return;
  402. }
  403. }
  404. else if (_method == "miner_pausegpu") {
  405. if (!checkApiWriteAccess(m_readonly, jResponse))
  406. return;
  407. Json::Value jRequestParams;
  408. if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
  409. return;
  410. unsigned index;
  411. if (!getRequestValue("index", index, jRequestParams, false, jResponse))
  412. return;
  413. bool pause;
  414. if (!getRequestValue("pause", pause, jRequestParams, false, jResponse))
  415. return;
  416. auto const& miner = Farm::f().getMiner(index);
  417. if (miner) {
  418. if (pause)
  419. miner->pause(MinerPauseEnum::PauseDueToAPIRequest);
  420. else
  421. miner->resume(MinerPauseEnum::PauseDueToAPIRequest);
  422. jResponse["result"] = true;
  423. } else {
  424. jResponse["error"]["code"] = -422;
  425. jResponse["error"]["message"] = "Index out of bounds";
  426. return;
  427. }
  428. }
  429. else if (_method == "miner_setverbosity") {
  430. if (!checkApiWriteAccess(m_readonly, jResponse))
  431. return;
  432. Json::Value jRequestParams;
  433. if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
  434. return;
  435. unsigned verbosity;
  436. if (!getRequestValue("verbosity", verbosity, jRequestParams, false, jResponse))
  437. return;
  438. if (verbosity >= LOG_NEXT) {
  439. jResponse["error"]["code"] = -422;
  440. jResponse["error"]["message"] = "Verbosity out of bounds (0-" + to_string(LOG_NEXT - 1) + ")";
  441. return;
  442. }
  443. cnote << "Setting verbosity level to " << verbosity;
  444. g_logOptions = verbosity;
  445. jResponse["result"] = true;
  446. }
  447. else if (_method == "miner_setnonce") {
  448. if (!checkApiWriteAccess(m_readonly, jResponse))
  449. return;
  450. Json::Value jRequestParams;
  451. if (!getRequestValue("params", jRequestParams, jRequest, false, jResponse))
  452. return;
  453. if (jRequestParams.isMember("nonce")) {
  454. string nonce;
  455. if (getRequestValue("nonce", nonce, jRequestParams, false, jResponse)) {
  456. for (const auto& c : nonce)
  457. if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) {
  458. jResponse["error"]["code"] = -422;
  459. jResponse["error"]["message"] = "Invalid nonce";
  460. return;
  461. }
  462. cnote << "API: Setting start nonce to '" << nonce << "'";
  463. Farm::f().set_nonce(nonce);
  464. } else {
  465. jResponse["error"]["code"] = -422;
  466. jResponse["error"]["message"] = "Invalid nonce";
  467. return;
  468. }
  469. }
  470. jResponse["result"] = true;
  471. }
  472. else if (_method == "miner_getnonce") {
  473. jResponse["result"] = Farm::f().get_nonce();
  474. }
  475. else {
  476. // Any other method not found
  477. jResponse["error"]["code"] = -32601;
  478. jResponse["error"]["message"] = "Method not found";
  479. }
  480. }
  481. void ApiConnection::recvSocketData() {
  482. boost::asio::async_read(
  483. m_socket, m_recvBuffer, boost::asio::transfer_at_least(1),
  484. m_io_strand.wrap(boost::bind(&ApiConnection::onRecvSocketDataCompleted, this, boost::asio::placeholders::error,
  485. boost::asio::placeholders::bytes_transferred)));
  486. }
  487. void ApiConnection::onRecvSocketDataCompleted(const boost::system::error_code& ec, size_t bytes_transferred) {
  488. /*
  489. Standard http request detection pattern
  490. 1st group : any UPPERCASE word
  491. 2nd group : the path
  492. 3rd group : HTTP version
  493. */
  494. static regex http_pattern("^([A-Z]{1,6}) (\\/[\\S]*) (HTTP\\/1\\.[0-9]{1})");
  495. smatch http_matches;
  496. if (!ec && bytes_transferred > 0) {
  497. // Extract received message and free the buffer
  498. string rx_message(boost::asio::buffer_cast<const char*>(m_recvBuffer.data()), bytes_transferred);
  499. m_recvBuffer.consume(bytes_transferred);
  500. m_message.append(rx_message);
  501. string line;
  502. string linedelimiter;
  503. size_t linedelimiteroffset;
  504. if (m_message.size() < 4)
  505. return; // Wait for other data to come in
  506. if (regex_search(m_message, http_matches, http_pattern, regex_constants::match_default)) {
  507. // We got an HTTP request
  508. string http_method = http_matches[1].str();
  509. string http_path = http_matches[2].str();
  510. string http_ver = http_matches[3].str();
  511. // Do we support method ?
  512. if (http_method != "GET") {
  513. string what = "Method " + http_method + " not allowed";
  514. stringstream ss;
  515. ss << http_ver << " "
  516. << "405 Method not allowed\r\n"
  517. << "Server: " << nsfminer_get_buildinfo()->project_name_with_version << "\r\n"
  518. << "Content-Type: text/plain\r\n"
  519. << "Content-Length: " << what.size() << "\r\n\r\n"
  520. << what;
  521. sendSocketData(ss.str(), true);
  522. m_message.clear();
  523. cnote << "HTTP Request " << http_method << " " << http_path << " not supported (405).";
  524. return;
  525. }
  526. // Do we support path ?
  527. if (http_path != "/" && http_path != "/getstat1" && http_path != "/metrics") {
  528. string what = "The requested resource " + http_path + " not found on this server";
  529. stringstream ss;
  530. ss << http_ver << " "
  531. << "404 Not Found\r\n"
  532. << "Server: " << nsfminer_get_buildinfo()->project_name_with_version << "\r\n"
  533. << "Content-Type: text/plain\r\n"
  534. << "Content-Length: " << what.size() << "\r\n\r\n"
  535. << what;
  536. sendSocketData(ss.str(), true);
  537. m_message.clear();
  538. cnote << "HTTP Request " << http_method << " " << http_path << " not found (404).";
  539. return;
  540. }
  541. //// Get all the lines - we actually don't care much
  542. //// until we support other http methods or paths
  543. //// Keep this for future use (if any)
  544. //// Remember to #include <boost/algorithm/string.hpp>
  545. // vector<string> lines;
  546. // boost::split(lines, m_message, [](char _c) { return _c == '\n'; });
  547. stringstream ss; // Builder of the response
  548. if (http_method == "GET" && (http_path == "/" || http_path == "/getstat1" || http_path == "/metrics")) {
  549. try {
  550. string body, content_type;
  551. if (http_path == "/metrics") {
  552. body = getHttpMinerMetrics();
  553. content_type = "text/plain";
  554. } else {
  555. body = getHttpMinerStatDetail();
  556. content_type = "text/html";
  557. }
  558. ss.clear();
  559. ss << http_ver << " "
  560. << "200 OK\r\n"
  561. << "Server: " << nsfminer_get_buildinfo()->project_name_with_version << "\r\n"
  562. << "Content-Type: " << content_type << "; charset=utf-8\r\n"
  563. << "Content-Length: " << body.size() << "\r\n\r\n"
  564. << body;
  565. cnote << "HTTP Request " << http_method << " " << http_path << " 200 OK (" << ss.str().size() << " bytes).";
  566. } catch (const exception& _ex) {
  567. string what = "Internal error : " + string(_ex.what());
  568. ss.clear();
  569. ss << http_ver << " "
  570. << "500 Internal Server Error\r\n"
  571. << "Server: " << nsfminer_get_buildinfo()->project_name_with_version << "\r\n"
  572. << "Content-Type: text/plain\r\n"
  573. << "Content-Length: " << what.size() << "\r\n\r\n"
  574. << what;
  575. cnote << "HTTP Request " << http_method << " " << http_path << " 500 Error (" << _ex.what() << ").";
  576. }
  577. }
  578. sendSocketData(ss.str(), true);
  579. m_message.clear();
  580. } else {
  581. // We got a Json request
  582. // Process each line in the transmission
  583. linedelimiter = "\n";
  584. linedelimiteroffset = m_message.find(linedelimiter);
  585. while (linedelimiteroffset != string::npos) {
  586. if (linedelimiteroffset > 0) {
  587. line = m_message.substr(0, linedelimiteroffset);
  588. boost::trim(line);
  589. if (!line.empty()) {
  590. // Test validity of chunk and process
  591. Json::Value jMsg;
  592. Json::Value jRes;
  593. Json::Reader jRdr;
  594. if (jRdr.parse(line, jMsg)) {
  595. try {
  596. // Run in sync so no 2 different async reads may overlap
  597. processRequest(jMsg, jRes);
  598. } catch (const exception& _ex) {
  599. jRes = Json::Value();
  600. jRes["jsonrpc"] = "2.0";
  601. jRes["id"] = Json::Value::null;
  602. jRes["error"]["errorcode"] = "500";
  603. jRes["error"]["message"] = _ex.what();
  604. }
  605. } else {
  606. jRes = Json::Value();
  607. jRes["jsonrpc"] = "2.0";
  608. jRes["id"] = Json::Value::null;
  609. jRes["error"]["errorcode"] = "-32700";
  610. string what = jRdr.getFormattedErrorMessages();
  611. boost::replace_all(what, "\n", " ");
  612. cwarn << "API : Got invalid Json message " << what;
  613. jRes["error"]["message"] = "Json parse error : " + what;
  614. }
  615. // Send response to client
  616. sendSocketData(jRes);
  617. }
  618. }
  619. // Next line (if any)
  620. m_message.erase(0, linedelimiteroffset + 1);
  621. linedelimiteroffset = m_message.find(linedelimiter);
  622. }
  623. // Eventually keep reading from socket
  624. if (m_socket.is_open())
  625. recvSocketData();
  626. }
  627. } else {
  628. disconnect();
  629. }
  630. }
  631. void ApiConnection::sendSocketData(Json::Value const& jReq, bool _disconnect) {
  632. if (!m_socket.is_open())
  633. return;
  634. stringstream line;
  635. line << Json::writeString(m_jSwBuilder, jReq) << endl;
  636. sendSocketData(line.str(), _disconnect);
  637. }
  638. void ApiConnection::sendSocketData(string const& _s, bool _disconnect) {
  639. if (!m_socket.is_open())
  640. return;
  641. ostream os(&m_sendBuffer);
  642. os << _s;
  643. async_write(m_socket, m_sendBuffer,
  644. m_io_strand.wrap(boost::bind(&ApiConnection::onSendSocketDataCompleted, this,
  645. boost::asio::placeholders::error, _disconnect)));
  646. }
  647. void ApiConnection::onSendSocketDataCompleted(const boost::system::error_code& ec, bool _disconnect) {
  648. if (ec || _disconnect)
  649. disconnect();
  650. }
  651. Json::Value ApiConnection::getMinerStat1() {
  652. auto connection = PoolManager::p().getActiveConnection();
  653. TelemetryType t = Farm::f().Telemetry();
  654. auto runningTime = chrono::duration_cast<chrono::minutes>(steady_clock::now() - t.start);
  655. ostringstream totalMhEth;
  656. ostringstream totalMhDcr;
  657. ostringstream detailedMhEth;
  658. ostringstream detailedMhDcr;
  659. ostringstream tempAndFans;
  660. ostringstream memTemps;
  661. ostringstream poolAddresses;
  662. ostringstream invalidStats;
  663. totalMhEth << fixed << setprecision(0) << t.farm.hashrate / 1000.0f << ";" << t.farm.solutions.accepted << ";"
  664. << t.farm.solutions.rejected;
  665. totalMhDcr << "0;0;0"; // DualMining not supported
  666. invalidStats << t.farm.solutions.failed << ";0"; // Invalid + Pool switches
  667. poolAddresses << connection->Host() << ':' << connection->Port();
  668. invalidStats << ";0;0"; // DualMining not supported
  669. int gpuIndex;
  670. int numGpus = t.miners.size();
  671. for (gpuIndex = 0; gpuIndex < numGpus; gpuIndex++) {
  672. detailedMhEth << fixed << setprecision(0) << t.miners.at(gpuIndex).hashrate / 1000.0f
  673. << (((numGpus - 1) > gpuIndex) ? ";" : "");
  674. detailedMhDcr << "off" << (((numGpus - 1) > gpuIndex) ? ";" : ""); // DualMining not supported
  675. tempAndFans << t.miners.at(gpuIndex).sensors.tempC << ";" << t.miners.at(gpuIndex).sensors.fanP
  676. << (((numGpus - 1) > gpuIndex) ? ";" : ""); // Fetching Temp and Fans
  677. memTemps << t.miners.at(gpuIndex).sensors.memtempC
  678. << (((numGpus - 1) > gpuIndex) ? ";" : ""); // Fetching Temp and Fans
  679. }
  680. Json::Value jRes;
  681. jRes[0] = nsfminer_get_buildinfo()->project_name_with_version; // miner version.
  682. jRes[1] = toString(runningTime.count()); // running time, in minutes.
  683. jRes[2] = totalMhEth.str(); // total ETH hashrate in MH/s, number of ETH shares, number of ETH
  684. // rejected shares.
  685. jRes[3] = detailedMhEth.str(); // detailed ETH hashrate for all GPUs.
  686. jRes[4] = totalMhDcr.str(); // total DCR hashrate in MH/s, number of DCR shares, number of DCR
  687. // rejected shares.
  688. jRes[5] = detailedMhDcr.str(); // detailed DCR hashrate for all GPUs.
  689. jRes[6] = tempAndFans.str(); // Temperature and Fan speed(%) pairs for all GPUs.
  690. jRes[7] = poolAddresses.str(); // current mining pool. For dual mode, there will be two pools here.
  691. jRes[8] = invalidStats.str(); // number of ETH invalid shares, number of ETH pool switches,
  692. // number of DCR invalid shares, number of DCR pool switches.
  693. jRes[9] = memTemps.str(); // Memory temps
  694. return jRes;
  695. }
  696. Json::Value ApiConnection::getMinerStatDetailPerMiner(const TelemetryType& _t, shared_ptr<Miner> _miner) {
  697. unsigned _index = _miner->Index();
  698. chrono::steady_clock::time_point _now = chrono::steady_clock::now();
  699. Json::Value jRes;
  700. DeviceDescriptor minerDescriptor = _miner->getDescriptor();
  701. jRes["_index"] = _index;
  702. jRes["_mode"] = (minerDescriptor.subscriptionType == DeviceSubscriptionTypeEnum::Cuda ? "CUDA" : "OpenCL");
  703. /* Hardware Info */
  704. Json::Value hwinfo;
  705. if (minerDescriptor.uniqueId.substr(0, 5) == "0000:")
  706. hwinfo["pci"] = minerDescriptor.uniqueId.substr(5);
  707. else
  708. hwinfo["pci"] = minerDescriptor.uniqueId;
  709. hwinfo["type"] = (minerDescriptor.type == DeviceTypeEnum::Gpu
  710. ? "GPU"
  711. : (minerDescriptor.type == DeviceTypeEnum::Accelerator ? "ACCELERATOR" : "CPU"));
  712. ostringstream ss;
  713. ss << minerDescriptor.boardName << " " << dev::getFormattedMemory((double)minerDescriptor.totalMemory);
  714. hwinfo["name"] = ss.str();
  715. /* Hardware Sensors*/
  716. Json::Value sensors = Json::Value(Json::arrayValue);
  717. sensors.append(_t.miners.at(_index).sensors.tempC);
  718. sensors.append(_t.miners.at(_index).sensors.fanP);
  719. sensors.append(_t.miners.at(_index).sensors.powerW);
  720. sensors.append(_t.miners.at(_index).sensors.memtempC);
  721. hwinfo["sensors"] = sensors;
  722. /* Mining Info */
  723. Json::Value mininginfo;
  724. Json::Value jshares = Json::Value(Json::arrayValue);
  725. Json::Value jsegment = Json::Value(Json::arrayValue);
  726. jshares.append(_t.miners.at(_index).solutions.accepted);
  727. jshares.append(_t.miners.at(_index).solutions.rejected);
  728. jshares.append(_t.miners.at(_index).solutions.failed);
  729. auto solution_lastupdated = chrono::duration_cast<chrono::seconds>(_now - _t.miners.at(_index).solutions.tstamp);
  730. jshares.append(uint64_t(solution_lastupdated.count())); // interval in seconds from last found
  731. // share
  732. mininginfo["shares"] = jshares;
  733. mininginfo["paused"] = _miner->paused();
  734. mininginfo["pause_reason"] = _miner->paused() ? _miner->pausedString() : Json::Value::null;
  735. /* Hash & Share infos */
  736. mininginfo["hashrate"] = toHex((uint32_t)_t.miners.at(_index).hashrate, HexPrefix::Add);
  737. jRes["hardware"] = hwinfo;
  738. jRes["mining"] = mininginfo;
  739. return jRes;
  740. }
  741. string ApiConnection::getHttpMinerMetrics() {
  742. Json::Value jStat = getMinerStatDetail();
  743. Json::StreamWriterBuilder builder;
  744. ostringstream ss;
  745. ss << "host=" << jStat["host"]["name"] << ",version=" << jStat["host"]["version"];
  746. string labels = ss.str();
  747. stringstream _ret;
  748. _ret
  749. << "# HELP miner_process_runtime Number of seconds miner process has been running.\n"
  750. << "# TYPE miner_process_runtime gauge\n"
  751. << "miner_process_runtime{" << labels << "} " << jStat["host"]["runtime"] << "\n"
  752. << "# HELP miner_process_connected Connection status.\n"
  753. << "# TYPE miner_process_connected gauge\n"
  754. << "miner_process_connected{" << labels << ",uri=" << jStat["connection"]["uri"] << "} " << jStat["connection"]["connected"].asUInt() << "\n"
  755. << "# HELP miner_process_connection_switches Connection switches.\n"
  756. << "# TYPE miner_process_connection_switches gauge\n"
  757. << "miner_process_connection_switches{" << labels << "} " << jStat["connection"]["switches"] << "\n";
  758. // Per device help/type info.
  759. double total_power = 0;
  760. for (Json::Value::ArrayIndex i = 0; i != jStat["devices"].size(); i++) {
  761. Json::Value device = jStat["devices"][i];
  762. ostringstream ss;
  763. ss << labels
  764. << ",id=\"" << device["_index"] << "\""
  765. << ",name=" << device["hardware"]["name"]
  766. << ",pci=" << device["hardware"]["pci"]
  767. << ",device_type=" << device["hardware"]["type"]
  768. << ",mode=" << device["_mode"];
  769. string device_labels = ss.str();
  770. double hashrate = stoul(device["mining"]["hashrate"].asString(), nullptr, 16);
  771. double power = device["hardware"]["sensors"][2].asDouble();
  772. _ret
  773. << "# HELP miner_device_hashrate Device hash rate in hashes/sec.\n"
  774. << "# TYPE miner_device_hashrate gauge\n"
  775. << "miner_device_hashrate{" << device_labels << "} " << hashrate << "\n"
  776. << "# HELP miner_device_power_watts Device power draw in watts.\n"
  777. << "# TYPE miner_device_power_watts gauge\n"
  778. << "miner_device_power_watts{" << device_labels << "} " << power << "\n"
  779. << "# HELP miner_device_temp_celsius Device temperature in degrees celsius.\n"
  780. << "# TYPE miner_device_temp_celsius gauge\n"
  781. << "miner_device_temp_celsius{" << device_labels << "} " << device["hardware"]["sensors"][0].asDouble() << "\n"
  782. << "# HELP miner_device_memory_temp_celsius Memory temperature in degrees celsius.\n"
  783. << "# TYPE miner_device_memory_temp_celsius gauge\n"
  784. << "miner_device_memory_temp_celsius{" << device_labels << "} " << device["hardware"]["sensors"][3].asDouble() << "\n"
  785. << "# HELP miner_device_fanspeed Device fanspeed (percentage 0-100).\n"
  786. << "# TYPE miner_device_fanspeed gauge\n"
  787. << "miner_device_fanspeed{" << device_labels << "} " << device["hardware"]["sensors"][1].asUInt() << "\n"
  788. << "# HELP miner_device_shares_total Number of shares processed by devices and status (failed, found, or rejected).\n"
  789. << "# TYPE miner_device_shares_total counter\n"
  790. << "miner_device_shares_total{" << device_labels << ",status=\"found\"} " << device["mining"]["shares"][0].asUInt() << "\n"
  791. << "miner_device_shares_total{" << device_labels << ",status=\"rejected\"} " << device["mining"]["shares"][1].asUInt() << "\n"
  792. << "miner_device_shares_total{" << device_labels << ",status=\"failed\"} " << device["mining"]["shares"][2].asUInt() << "\n"
  793. << "# HELP miner_device_shares_last_found_seconds Time since device last found share (seconds).\n"
  794. << "# TYPE miner_device_shares_last_found_seconds gauge\n"
  795. << "miner_device_shares_last_found_seconds{" << device_labels << "} " << device["mining"]["shares"][3].asUInt() << "\n"
  796. << "# HELP miner_device_paused True if device is paused.\n"
  797. << "# TYPE miner_device_paused gauge\n"
  798. << "miner_device_paused{" << device_labels << "} " << (device["mining"]["paused"].asBool() ? 1 : 0) << "\n";
  799. total_power += power;
  800. }
  801. double total_hashrate = stoul(jStat["mining"]["hashrate"].asString(), nullptr, 16);
  802. _ret << "# HELP miner_total_hashrate Total miner process hashrate across all devices (hashes/sec).\n"
  803. << "# TYPE miner_total_hashrate gauge\n"
  804. << "miner_total_hashrate{" << labels << "} " << total_hashrate << "\n"
  805. << "# HELP miner_total_power_watts Total power consumption across all devices (watts).\n"
  806. << "# TYPE miner_total_power_watts gauge\n"
  807. << "miner_total_power_watts{" << labels << "} " << total_power << "\n"
  808. << "# HELP miner_shares_total Total number of shares across all devices.\n"
  809. << "# TYPE miner_shares_total counter\n"
  810. << "miner_shares_total{" << labels << ",status=\"found\"} " << jStat["mining"]["shares"][0].asUInt() << "\n"
  811. << "miner_shares_total{" << labels << ",status=\"rejected\"} " << jStat["mining"]["shares"][1].asUInt() << "\n"
  812. << "miner_shares_total{" << labels << ",status=\"failed\"} " << jStat["mining"]["shares"][2].asUInt() << "\n"
  813. << "# HELP miner_difficulty Difficulty mining.\n"
  814. << "# TYPE miner_difficulty gauge\n"
  815. << "miner_difficulty{" << labels << "} " << jStat["mining"]["difficulty"].asDouble() << "\n"
  816. << "# HELP miner_shares_last_found_seconds Time since last found share across all devices (seconds).\n"
  817. << "# TYPE miner_shares_last_found_seconds gauge\n"
  818. << "miner_shares_last_found_secs{" << labels << "} " << jStat["mining"]["shares"][3].asUInt() << "\n";
  819. // For debugging
  820. //_ret << Json::writeString(builder, jStat);
  821. _ret << "# EOF\n";
  822. return _ret.str();
  823. }
  824. string ApiConnection::getHttpMinerStatDetail() {
  825. Json::Value jStat = getMinerStatDetail();
  826. uint64_t durationSeconds = jStat["host"]["runtime"].asUInt64();
  827. int hours = (int)(durationSeconds / 3600);
  828. durationSeconds -= (hours * 3600);
  829. int minutes = (int)(durationSeconds / 60);
  830. int hoursSize = (hours > 9 ? (hours > 99 ? 3 : 2) : 1);
  831. /* Build up header*/
  832. stringstream _ret;
  833. _ret << "<!doctype html>"
  834. << "<html lang=en>"
  835. << "<head>"
  836. << "<meta charset=utf-8>"
  837. << "<meta http-equiv=\"refresh\" content=\"30\">"
  838. << "<title>" << jStat["host"]["name"].asString() << "</title>"
  839. << "<style>"
  840. << "body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,"
  841. << "\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5;"
  842. << "text-align:center;}"
  843. << "table,td,th{border:1px inset #000;}"
  844. << "table{border-spacing:0;}"
  845. << "td,th{padding:3px;}"
  846. << "tbody tr:nth-child(even){background-color:" << HTTP_ROW0_COLOR << ";}"
  847. << "tbody tr:nth-child(odd){background-color:" << HTTP_ROW1_COLOR << ";}"
  848. << ".mx-auto{margin-left:auto;margin-right:auto;}"
  849. << ".bg-header1{background-color:" << HTTP_HDR1_COLOR << ";}"
  850. << ".bg-header0{background-color:" << HTTP_HDR0_COLOR << ";}"
  851. << ".bg-red{color:" << HTTP_ROWRED_COLOR << ";}"
  852. << ".right{text-align: right;}"
  853. << "</style>"
  854. << "<meta http-equiv=refresh content=30>"
  855. << "</head>"
  856. << "<body>"
  857. << "<table class=mx-auto>"
  858. << "<thead>"
  859. << "<tr class=bg-header1>"
  860. << "<th colspan=9>" << jStat["host"]["version"].asString() << " - " << setw(hoursSize) << hours << ":"
  861. << setw(2) << setfill('0') << fixed << minutes << "<br>Pool: " << jStat["connection"]["uri"].asString()
  862. << "</th>"
  863. << "</tr>"
  864. << "<tr class=bg-header0>"
  865. << "<th>PCI</th>"
  866. << "<th>Device</th>"
  867. << "<th>Mode</th>"
  868. << "<th>Paused</th>"
  869. << "<th class=right>Hash Rate</th>"
  870. << "<th class=right>Solutions</th>"
  871. << "<th class=right>Temp.</th>"
  872. << "<th class=right>Fan %</th>"
  873. << "<th class=right>Power</th>"
  874. << "</tr>"
  875. << "</thead><tbody>";
  876. /* Loop miners */
  877. double total_hashrate = 0;
  878. double total_power = 0;
  879. unsigned int total_solutions = 0;
  880. for (Json::Value::ArrayIndex i = 0; i != jStat["devices"].size(); i++) {
  881. Json::Value device = jStat["devices"][i];
  882. double hashrate = stoul(device["mining"]["hashrate"].asString(), nullptr, 16);
  883. double power = device["hardware"]["sensors"][2].asDouble();
  884. unsigned int solutions = device["mining"]["shares"][0].asUInt();
  885. total_hashrate += hashrate;
  886. total_power += power;
  887. total_solutions += solutions;
  888. _ret << "<tr" << (device["mining"]["paused"].asBool() ? " class=\"bg-red\"" : "") << ">"; // Open row
  889. _ret << "<td>" << device["hardware"]["pci"].asString() << "</td>";
  890. _ret << "<td>" << device["hardware"]["name"].asString() << "</td>";
  891. _ret << "<td>" << device["_mode"].asString() << "</td>";
  892. _ret << "<td>" << (device["mining"]["paused"].asBool() ? device["mining"]["pause_reason"].asString() : "No")
  893. << "</td>";
  894. _ret << "<td class=right>" << dev::getFormattedHashes(hashrate) << "</td>";
  895. string solString = "A" + device["mining"]["shares"][0].asString() + ":R" +
  896. device["mining"]["shares"][1].asString() + ":F" + device["mining"]["shares"][2].asString();
  897. _ret << "<td class=right>" << solString << "</td>";
  898. _ret << "<td class=right>" << device["hardware"]["sensors"][0].asString() << "</td>";
  899. _ret << "<td class=right>" << device["hardware"]["sensors"][1].asString() << "</td>";
  900. stringstream powerStream; // Round the power to 2 decimal places to remove floating point
  901. // garbage
  902. powerStream << fixed << setprecision(2) << device["hardware"]["sensors"][2].asDouble();
  903. _ret << "<td class=right>" << powerStream.str() << "</td>";
  904. _ret << "</tr>"; // Close row
  905. }
  906. _ret << "</tbody>";
  907. /* Summarize */
  908. _ret << "<tfoot><tr class=bg-header0><td colspan=4 class=right>Total</td><td class=right>"
  909. << dev::getFormattedHashes(total_hashrate) << "</td><td class=right>" << total_solutions
  910. << "</td><td colspan=3 class=right>" << setprecision(2) << total_power << "</td></tfoot>";
  911. _ret << "</table></body></html>";
  912. return _ret.str();
  913. }
  914. Json::Value ApiConnection::getMinerStatDetail() {
  915. const chrono::steady_clock::time_point now = chrono::steady_clock::now();
  916. TelemetryType t = Farm::f().Telemetry();
  917. auto runningTime = chrono::duration_cast<chrono::seconds>(chrono::steady_clock::now() - t.start);
  918. // ostringstream version;
  919. Json::Value devices = Json::Value(Json::arrayValue);
  920. Json::Value jRes;
  921. /* Host Info */
  922. Json::Value hostinfo;
  923. hostinfo["version"] = nsfminer_get_buildinfo()->project_name_with_version; // miner version.
  924. hostinfo["runtime"] = uint64_t(runningTime.count()); // running time, in seconds.
  925. {
  926. // Even the client should know which host was queried
  927. char hostName[HOST_NAME_MAX + 1];
  928. if (!gethostname(hostName, HOST_NAME_MAX + 1))
  929. hostinfo["name"] = hostName;
  930. else
  931. hostinfo["name"] = Json::Value::null;
  932. }
  933. /* Connection info */
  934. Json::Value connectioninfo;
  935. auto connection = PoolManager::p().getActiveConnection();
  936. connectioninfo["uri"] = connection->str();
  937. connectioninfo["connected"] = PoolManager::p().isConnected();
  938. connectioninfo["switches"] = PoolManager::p().getConnectionSwitches();
  939. /* Mining Info */
  940. Json::Value mininginfo;
  941. Json::Value sharesinfo = Json::Value(Json::arrayValue);
  942. mininginfo["hashrate"] = toHex(uint32_t(t.farm.hashrate), HexPrefix::Add);
  943. mininginfo["epoch"] = PoolManager::p().getCurrentEpoch();
  944. mininginfo["epoch_changes"] = PoolManager::p().getEpochChanges();
  945. mininginfo["difficulty"] = PoolManager::p().getPoolDifficulty();
  946. sharesinfo.append(t.farm.solutions.accepted);
  947. sharesinfo.append(t.farm.solutions.rejected);
  948. sharesinfo.append(t.farm.solutions.failed);
  949. auto solution_lastupdated = chrono::duration_cast<chrono::seconds>(now - t.farm.solutions.tstamp);
  950. sharesinfo.append(uint64_t(solution_lastupdated.count())); // interval in seconds from last
  951. // found share
  952. mininginfo["shares"] = sharesinfo;
  953. /* Monitors Info */
  954. Json::Value monitorinfo;
  955. auto tstop = Farm::f().get_tstop();
  956. if (tstop) {
  957. Json::Value tempsinfo = Json::Value(Json::arrayValue);
  958. tempsinfo.append(Farm::f().get_tstart());
  959. tempsinfo.append(tstop);
  960. monitorinfo["temperatures"] = tempsinfo;
  961. }
  962. /* Devices related info */
  963. for (shared_ptr<Miner> miner : Farm::f().getMiners())
  964. devices.append(getMinerStatDetailPerMiner(t, miner));
  965. jRes["devices"] = devices;
  966. jRes["monitors"] = monitorinfo;
  967. jRes["connection"] = connectioninfo;
  968. jRes["host"] = hostinfo;
  969. jRes["mining"] = mininginfo;
  970. return jRes;
  971. }