refactor(console): unify lite + full-node console behind a pluggable executor

The two consoles were separate implementations (the rich full-node ConsoleTab and a
minimal RenderLiteConsoleTab). Introduce ConsoleCommandExecutor to abstract the only
real differences — command execution + log source — so both variants share the one rich
terminal (the lite console now gains selection, zoom, copy, and JSON-colored output).

- console_command_executor.{h,cpp}: interface + FullNodeConsoleExecutor (dragonxd log
  ingestion + RPC command execution, result queue) and LiteConsoleExecutor (lite
  diagnostics ring + backend runConsoleCommand/takeConsoleResult, connection/sync status).
- ConsoleTab::render() now takes a ConsoleCommandExecutor&; log ingestion, command
  submission, and command results flow through it. The RPC command-reference popup and
  the daemon/errors/rpc-trace/app filter toggles are gated on the executor's capabilities;
  the toolbar status dot + a status header come from the executor.
- Safety preserved: `clear`/`cls` is intercepted as a view-only clear in the shared UI so
  it is NEVER forwarded (the lite backend's `clear` wipes tx history); command output is
  not persisted to diagnostics.
- App owns the executor (created per variant) and routes both NavPages to console_tab_;
  lite_console_tab.{h,cpp} deleted.

Needs runtime verification of BOTH consoles (full-node RPC + lite: clear, seed/export
secrecy, sync status, command results).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 15:59:17 -05:00
parent 1266b6e68e
commit b5001ad4db
10 changed files with 594 additions and 570 deletions

View File

@@ -0,0 +1,313 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "console_command_executor.h"
#include "console_tab.h" // ConsoleTab::COLOR_* statics
#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 <nlohmann/json.hpp>
#include <algorithm>
#include <cstdio>
#include <sstream>
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<std::mutex> 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<std::mutex> 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<std::mutex> lk(results_mutex_);
results_.push_back({result_str, is_error});
}
}
bool FullNodeConsoleExecutor::pollResult(std::string& result, bool& isError)
{
std::lock_guard<std::mutex> 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<int>(DState::Stopped)) {
add("", ConsoleTab::COLOR_RESULT);
add(TR("console_starting_node"), ConsoleTab::COLOR_INFO);
add(TR("console_capturing_output"), ConsoleTab::COLOR_INFO);
add("", ConsoleTab::COLOR_RESULT);
} else if (cur == DState::Running && prev != static_cast<int>(DState::Running)) {
add(TR("console_daemon_started"), ConsoleTab::COLOR_INFO);
} else if (cur == DState::Stopped && prev == static_cast<int>(DState::Running)) {
add("", ConsoleTab::COLOR_RESULT);
add(TR("console_daemon_stopped"), ConsoleTab::COLOR_INFO);
} else if (cur == DState::Error) {
add(std::string(TR("console_daemon_error")) + d->getLastError() + " ===", ConsoleTab::COLOR_ERROR);
}
last_daemon_state_ = static_cast<int>(cur);
}
rpc::RPCClient* rpc = app_->consoleRpc();
if (rpc) {
const bool now = rpc->isConnected();
if (now && !last_rpc_connected_) add(TR("console_connected"), ConsoleTab::COLOR_INFO);
else if (!now && last_rpc_connected_) add(TR("console_disconnected"), ConsoleTab::COLOR_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;
ImU32 c = ConsoleTab::COLOR_DAEMON;
if (line.find("[ERROR]") != std::string::npos ||
line.find("error:") != std::string::npos ||
line.find("Error:") != std::string::npos)
c = ConsoleTab::COLOR_ERROR;
add("[daemon] " + line, c);
}
}
}
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;
ImU32 c = ConsoleTab::COLOR_DAEMON;
if (line.find("error") != std::string::npos ||
line.find("ERROR") != std::string::npos ||
line.find("failed") != std::string::npos)
c = ConsoleTab::COLOR_ERROR;
else if (line.find("accepted") != std::string::npos)
c = ConsoleTab::COLOR_INFO;
add("[xmrig] " + line, c);
}
}
} 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"), ConsoleTab::COLOR_INFO);
add(TR("console_help_clear"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_help"), ConsoleTab::COLOR_RESULT);
add("", ConsoleTab::COLOR_RESULT);
add(TR("console_common_rpc"), ConsoleTab::COLOR_INFO);
add(TR("console_help_getinfo"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_getbalance"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_gettotalbalance"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_getblockcount"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_getpeerinfo"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_setgenerate"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_getmininginfo"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_stop"), ConsoleTab::COLOR_RESULT);
add("", ConsoleTab::COLOR_RESULT);
add(TR("console_click_commands"), ConsoleTab::COLOR_INFO);
add(TR("console_tab_completion"), ConsoleTab::COLOR_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 {
// Colour error/success lines from the lite diagnostics stream for at-a-glance scanning.
ImU32 liteLogColor(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 Error();
if (has(": connected") || has("opened") || has("wallet ready") || has("Ready"))
return Success();
return OnSurfaceMedium();
}
} // 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<std::uint64_t>(-1)) {
const std::uint64_t added = gen - diag_gen_;
startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast<std::size_t>(added);
}
for (std::size_t i = startIdx; i < snap.size(); ++i)
add(snap[i], liteLogColor(snap[i]));
diag_gen_ = gen;
}
void LiteConsoleExecutor::printHelp(const ConsoleAddLineFn& add)
{
add(TR("console_available_commands"), ConsoleTab::COLOR_INFO);
add(TR("console_help_clear"), ConsoleTab::COLOR_RESULT);
add(TR("console_help_help"), ConsoleTab::COLOR_RESULT);
add(TR("lite_console_help_passthrough"), ConsoleTab::COLOR_INFO);
}
std::vector<ConsoleStatusLine> LiteConsoleExecutor::statusLines() const
{
std::vector<ConsoleStatusLine> 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