// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "console_command_executor.h" #include "console_channel.h" #include "console_input_model.h" // BuildConsoleRpcCall #include "../../app.h" #include "../../data/wallet_state.h" #include "../../rpc/rpc_client.h" #include "../../rpc/rpc_worker.h" #include "../../daemon/embedded_daemon.h" #include "../../daemon/xmrig_manager.h" #include "../../wallet/lite_wallet_controller.h" #include "../../wallet/lite_diagnostics.h" #include "../../util/i18n.h" #include "../material/colors.h" #include #include #include #include namespace dragonx { namespace ui { using namespace material; // ============================================================================ // FullNodeConsoleExecutor // ============================================================================ bool FullNodeConsoleExecutor::isReady() const { rpc::RPCClient* rpc = app_->consoleRpc(); return rpc && rpc->isConnected(); } void FullNodeConsoleExecutor::submit(const std::string& cmd) { rpc::RPCClient* rpc = app_->consoleRpc(); if (!rpc || !rpc->isConnected()) { std::lock_guard lk(results_mutex_); results_.push_back({std::string(TR("console_not_connected")), true}); return; } auto call = BuildConsoleRpcCall(cmd); if (!call.valid) return; const std::string method = call.method; const nlohmann::json params = call.params; rpc::RPCWorker* worker = app_->consoleWorker(); if (worker) { worker->post([rpc, method, params, this]() -> rpc::RPCWorker::MainCb { std::string result_str; bool is_error = false; try { rpc::RPCClient::TraceScope trace("Console tab / User command"); result_str = rpc->callRaw(method, params); } catch (const std::exception& e) { result_str = e.what(); is_error = true; } return [this, result_str, is_error]() { std::lock_guard lk(results_mutex_); results_.push_back({result_str, is_error}); }; }); } else { std::string result_str; bool is_error = false; try { rpc::RPCClient::TraceScope trace("Console tab / User command"); result_str = rpc->callRaw(method, params); } catch (const std::exception& e) { result_str = e.what(); is_error = true; } std::lock_guard lk(results_mutex_); results_.push_back({result_str, is_error}); } } bool FullNodeConsoleExecutor::pollResult(std::string& result, bool& isError) { std::lock_guard lk(results_mutex_); if (results_.empty()) return false; result = std::move(results_.front().first); isError = results_.front().second; results_.pop_front(); return true; } void FullNodeConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) { using DState = daemon::EmbeddedDaemon::State; daemon::EmbeddedDaemon* d = app_->consoleDaemon(); if (d) { const DState cur = d->getState(); const int prev = last_daemon_state_; if (cur == DState::Starting && prev == static_cast(DState::Stopped)) { add("", ConsoleChannel::None); add(TR("console_starting_node"), ConsoleChannel::Info); add(TR("console_capturing_output"), ConsoleChannel::Info); add("", ConsoleChannel::None); } else if (cur == DState::Running && prev != static_cast(DState::Running)) { add(TR("console_daemon_started"), ConsoleChannel::Info); } else if (cur == DState::Stopped && prev == static_cast(DState::Running)) { add("", ConsoleChannel::None); add(TR("console_daemon_stopped"), ConsoleChannel::Info); } else if (cur == DState::Error) { add(std::string(TR("console_daemon_error")) + d->getLastError() + " ===", ConsoleChannel::Error); } last_daemon_state_ = static_cast(cur); } rpc::RPCClient* rpc = app_->consoleRpc(); if (rpc) { const bool now = rpc->isConnected(); if (now && !last_rpc_connected_) add(TR("console_connected"), ConsoleChannel::Info); else if (!now && last_rpc_connected_) add(TR("console_disconnected"), ConsoleChannel::Error); last_rpc_connected_ = now; } if (d) { std::string out = d->getOutputSince(last_daemon_output_size_); if (!out.empty()) { std::istringstream ss(out); std::string line; while (std::getline(ss, line)) { if (line.empty()) continue; ConsoleChannel c = ConsoleChannel::Daemon; if (line.find("[ERROR]") != std::string::npos || line.find("error:") != std::string::npos || line.find("Error:") != std::string::npos) c = ConsoleChannel::Error; add(line, c); // channel conveys the source; no "[daemon] " prefix } } } daemon::XmrigManager* x = app_->consoleXmrig(); if (x && x->isRunning()) { std::string out = x->getOutputSince(last_xmrig_output_size_); if (!out.empty()) { std::istringstream ss(out); std::string line; while (std::getline(ss, line)) { if (line.empty()) continue; ConsoleChannel c = ConsoleChannel::Xmrig; if (line.find("error") != std::string::npos || line.find("ERROR") != std::string::npos || line.find("failed") != std::string::npos) c = ConsoleChannel::Error; else if (line.find("accepted") != std::string::npos) c = ConsoleChannel::Success; add(line, c); // channel conveys the source; no "[xmrig] " prefix } } } else { last_xmrig_output_size_ = 0; // reset so we get fresh output when it restarts } } void FullNodeConsoleExecutor::printHelp(const ConsoleAddLineFn& add) { add(TR("console_available_commands"), ConsoleChannel::Info); add(TR("console_help_clear"), ConsoleChannel::None); add(TR("console_help_help"), ConsoleChannel::None); add("", ConsoleChannel::None); add(TR("console_common_rpc"), ConsoleChannel::Info); add(TR("console_help_getinfo"), ConsoleChannel::None); add(TR("console_help_getbalance"), ConsoleChannel::None); add(TR("console_help_gettotalbalance"), ConsoleChannel::None); add(TR("console_help_getblockcount"), ConsoleChannel::None); add(TR("console_help_getpeerinfo"), ConsoleChannel::None); add(TR("console_help_setgenerate"), ConsoleChannel::None); add(TR("console_help_getmininginfo"), ConsoleChannel::None); add(TR("console_help_stop"), ConsoleChannel::None); add("", ConsoleChannel::None); add(TR("console_click_commands"), ConsoleChannel::Info); add(TR("console_tab_completion"), ConsoleChannel::Info); } ConsoleStatusLine FullNodeConsoleExecutor::toolbarStatus() const { ConsoleStatusLine s; daemon::EmbeddedDaemon* d = app_->consoleDaemon(); if (!d) return s; // empty text -> toolbar shows the generic "no daemon" label using DState = daemon::EmbeddedDaemon::State; switch (d->getState()) { case DState::Stopped: s.text = TR("console_status_stopped"); s.color = IM_COL32(150,150,150,255); break; case DState::Starting: s.text = TR("console_status_starting"); s.color = Warning(); s.pulse = true; break; case DState::Running: s.text = TR("console_status_running"); s.color = Success(); break; case DState::Stopping: s.text = TR("console_status_stopping"); s.color = Warning(); s.pulse = true; break; case DState::Error: s.text = TR("console_status_error"); s.color = Error(); break; } return s; } // ============================================================================ // LiteConsoleExecutor // ============================================================================ namespace { // Classify a lite diagnostics line into a channel for at-a-glance scanning. ConsoleChannel liteLogChannel(const std::string& line) { const auto has = [&line](const char* s) { return line.find(s) != std::string::npos; }; if (has("failed") || has(" unreachable") || has("blocked") || has("could not") || has("Error") || has("error")) return ConsoleChannel::Error; if (has(": connected") || has("opened") || has("wallet ready") || has("Ready")) return ConsoleChannel::Success; return ConsoleChannel::App; // lite diagnostics / wallet log } } // namespace bool LiteConsoleExecutor::isReady() const { wallet::LiteWalletController* lw = app_->liteWallet(); return lw && lw->walletOpen(); } bool LiteConsoleExecutor::busy() const { wallet::LiteWalletController* lw = app_->liteWallet(); return lw && lw->consoleCommandInProgress(); } void LiteConsoleExecutor::submit(const std::string& cmd) { wallet::LiteWalletController* lw = app_->liteWallet(); if (!lw) return; lw->runConsoleCommand(cmd); // rejection (busy) surfaces via the busy() indicator } bool LiteConsoleExecutor::pollResult(std::string& result, bool& isError) { wallet::LiteWalletController* lw = app_->liteWallet(); if (!lw) return false; wallet::LiteConsoleResult res; if (!lw->takeConsoleResult(res)) return false; result = res.response.empty() ? std::string("(no output)") : res.response; isError = !res.ok; return true; } void LiteConsoleExecutor::pollLogLines(const ConsoleAddLineFn& add) { auto& diag = wallet::LiteDiagnostics::instance(); const std::uint64_t gen = diag.generation(); if (gen == diag_gen_) return; const auto snap = diag.snapshot(); std::size_t startIdx = 0; if (diag_gen_ != static_cast(-1)) { const std::uint64_t added = gen - diag_gen_; startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast(added); } for (std::size_t i = startIdx; i < snap.size(); ++i) add(snap[i], liteLogChannel(snap[i])); diag_gen_ = gen; } void LiteConsoleExecutor::printHelp(const ConsoleAddLineFn& add) { add(TR("console_available_commands"), ConsoleChannel::Info); add(TR("console_help_clear"), ConsoleChannel::None); add(TR("console_help_help"), ConsoleChannel::None); add(TR("lite_console_help_passthrough"), ConsoleChannel::Info); } std::vector LiteConsoleExecutor::statusLines() const { std::vector out; wallet::LiteWalletController* lw = app_->liteWallet(); if (lw && lw->walletOpen()) { const SyncInfo& sync = app_->state().sync; char buf[96]; if (sync.syncing && !sync.isSynced()) { double vp = sync.verification_progress; if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0; std::snprintf(buf, sizeof(buf), "Syncing %.1f%% (block %d / %d)", vp * 100.0, sync.blocks, sync.headers); } else { std::snprintf(buf, sizeof(buf), "Synced (block %d)", sync.blocks); } out.push_back({std::string(buf), OnSurfaceMedium(), false}); } const std::string& err = app_->liteOpenError(); if (!err.empty() && (!lw || !lw->walletOpen())) out.push_back({std::string("Last error: ") + err, Error(), false}); return out; } ConsoleStatusLine LiteConsoleExecutor::toolbarStatus() const { ConsoleStatusLine s; wallet::LiteWalletController* lw = app_->liteWallet(); if (!lw) { s.text = "No backend"; s.color = Error(); return s; } if (lw->walletOpen()) { s.text = "Connected"; s.color = Success(); } else if (lw->openInProgress()) { s.text = "Connecting"; s.color = Warning(); s.pulse = true; } else { s.text = "Disconnected"; s.color = Error(); } return s; } } // namespace ui } // namespace dragonx