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:
313
src/ui/windows/console_command_executor.cpp
Normal file
313
src/ui/windows/console_command_executor.cpp
Normal 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
|
||||
Reference in New Issue
Block a user