Make ConsoleChannel the canonical semantic classification of every console
line. Producers (command executor, app log forwarders, result formatter) tag
each line with a channel; the UI derives both the text color and the left
accent-bar color from it at draw time, and the output filter keys off it.
This replaces the prior scheme of storing an ImU32 color per line and then
reverse-engineering the source from color-equality plus a "[daemon] "/"[xmrig] "
/"[app] "/"[rpc] " text prefix. Consequences:
- ConsoleLine now stores {text, ConsoleChannel} — no per-line color.
- addLine()/addRpcTraceLine() take a channel; the redundant text prefixes are
gone (the accent bar carries the origin).
- The executor's ConsoleAddLineFn hands a channel, not an ImU32, so the backend
no longer depends on ConsoleTab::COLOR_* — FullNode/Lite executors emit clean
text + channel.
- The theme-remap loop that rewrote stored colors on light/dark flip is deleted;
colors are resolved per-channel each frame (refreshColors on flip only).
- ConsoleOutputFilter drops its color fields; consoleLinePassesFilter() keys off
the channel. Test updated to the channel-based API.
- Added a Warning channel (amber severity peer of Error/Success) so the
notification + logger warning forwarders keep their color.
Full-node + lite build clean; ctest green; source hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
314 lines
12 KiB
C++
314 lines
12 KiB
C++
// 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 <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("", 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<int>(DState::Running)) {
|
|
add(TR("console_daemon_started"), ConsoleChannel::Info);
|
|
} else if (cur == DState::Stopped && prev == static_cast<int>(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<int>(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<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], 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<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
|