feat(lite): make the Console tab interactive (run backend commands)
The lite backend's litelib_execute() is the same command interface as silentdragonxlite-cli (balance, info, height, list, notes, addresses, sync, syncstatus, new, send, shield, encrypt, …), so the lite Console can be a real interactive console — like the full-node RPC console — instead of a read-only diagnostics log. Controller: add an async arbitrary-command runner mirroring the broadcast pattern — runConsoleCommand() splits "<command> [args]" (the first token is the command, the remainder is passed as the single arg string litelib_execute expects, since it does NOT whitespace-split), runs the bridge call on a detached thread that captures the shared bridge (never `this`), and delivers the result to a main-thread slot drained by takeConsoleResult(). Results are NEVER routed through LiteDiagnostics (seed/export can return secrets). Console tab: a command input (Enter to run, Up/Down history via the shared console_input_model helpers) over a unified scroll buffer that interleaves the automatic diagnostics events with user command I/O, colour-coded, with the live status header preserved. The input is disabled while a command runs. Two backend footguns are intercepted at the UI layer before forwarding: `clear` (the backend command WIPES wallet tx history — re-bound to clearing the view, what the user expects) and `quit`/`exit` (would only save; the embedded backend must stay running with the app). Test: runConsoleCommand drives the fake backend (info -> raw response; "new zs" -> exercises the command/arg split; blank line rejected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -272,6 +272,8 @@ LiteWalletController::~LiteWalletController()
|
||||
// Likewise the broadcast thread (send/shield proving): it captures shared refs (bridge +
|
||||
// running flag + result slot), never `this`, so detaching is safe.
|
||||
if (broadcastThread_.joinable()) broadcastThread_.detach();
|
||||
// The interactive-console command thread follows the same shared-lifetime pattern.
|
||||
if (consoleThread_.joinable()) consoleThread_.detach();
|
||||
// The async-open failover thread captures only shared refs (bridge + running flag + result
|
||||
// slot), never `this`, so detaching is safe if it's still trying servers at shutdown.
|
||||
if (openThread_.joinable()) openThread_.detach();
|
||||
@@ -753,6 +755,69 @@ bool LiteWalletController::takeBroadcastResult(LiteBroadcastResult& out)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LiteWalletController::runConsoleCommand(std::string commandLine)
|
||||
{
|
||||
if (!bridge_ || consoleRunning_->load()) return false;
|
||||
|
||||
// Split into command (first token) + the remainder as a single arg string: litelib_execute
|
||||
// passes args through as ONE element (it does not whitespace-split), matching how send uses
|
||||
// the JSON-array form.
|
||||
const size_t cmdBegin = commandLine.find_first_not_of(" \t");
|
||||
if (cmdBegin == std::string::npos) return false; // blank line
|
||||
const size_t cmdEnd = commandLine.find_first_of(" \t", cmdBegin);
|
||||
std::string command = commandLine.substr(
|
||||
cmdBegin, cmdEnd == std::string::npos ? std::string::npos : cmdEnd - cmdBegin);
|
||||
std::string args;
|
||||
if (cmdEnd != std::string::npos) {
|
||||
const size_t argBegin = commandLine.find_first_not_of(" \t", cmdEnd);
|
||||
if (argBegin != std::string::npos) args = commandLine.substr(argBegin);
|
||||
}
|
||||
|
||||
if (consoleThread_.joinable()) consoleThread_.join(); // a prior command has fully finished
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(*consoleResultMutex_);
|
||||
consoleResult_->reset();
|
||||
}
|
||||
consoleRunning_->store(true);
|
||||
|
||||
// Capture only the shared bridge + flags/slot (never `this`) so the thread can outlive us.
|
||||
auto bridge = bridge_;
|
||||
auto running = consoleRunning_;
|
||||
auto mutex = consoleResultMutex_;
|
||||
auto slot = consoleResult_;
|
||||
consoleThread_ = std::thread(
|
||||
[bridge, command, args, echo = std::move(commandLine), running, mutex, slot]() {
|
||||
LiteConsoleResult r;
|
||||
r.command = echo;
|
||||
if (bridge) {
|
||||
const auto call = bridge->execute(command, args);
|
||||
r.ok = call.ok;
|
||||
r.response = call.ok ? call.value
|
||||
: (call.error.empty() ? "command failed" : call.error);
|
||||
} else {
|
||||
r.response = "lite backend unavailable";
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(*mutex);
|
||||
*slot = std::move(r);
|
||||
}
|
||||
running->store(false);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LiteWalletController::takeConsoleResult(LiteConsoleResult& out)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(*consoleResultMutex_);
|
||||
if (!consoleResult_->has_value()) return false;
|
||||
out = std::move(**consoleResult_);
|
||||
consoleResult_->reset();
|
||||
}
|
||||
if (consoleThread_.joinable()) consoleThread_.join(); // producer set its result, then exits
|
||||
return true;
|
||||
}
|
||||
|
||||
LiteImportResult LiteWalletController::importKey(std::string spendingOrViewingKey)
|
||||
{
|
||||
LiteImportResult out;
|
||||
|
||||
@@ -64,6 +64,15 @@ struct LiteNewAddressResult {
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// Result of an interactive console command (lite Console tab). `response` is the raw backend
|
||||
// output (JSON or text). It may contain SECRET material (e.g. seed/export) because the user can
|
||||
// run any command, so it is shown only in the console view and NEVER routed through LiteDiagnostics.
|
||||
struct LiteConsoleResult {
|
||||
std::string command; // the command line that was run, echoed back
|
||||
std::string response; // raw backend response (or an error message)
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
// A single send recipient. amountZatoshis is in zatoshis (1e-8 DRGX); memo is ignored by the
|
||||
// backend for transparent destination addresses.
|
||||
struct LiteSendRecipient {
|
||||
@@ -227,6 +236,21 @@ public:
|
||||
bool broadcastInProgress() const { return broadcastRunning_ && broadcastRunning_->load(); }
|
||||
bool takeBroadcastResult(LiteBroadcastResult& out);
|
||||
|
||||
// --- Interactive console (lite Console tab) ---------------------------------------------
|
||||
// Run an arbitrary backend command — the same verbs as silentdragonxlite-cli (balance, info,
|
||||
// height, list, notes, addresses, sync, syncstatus, new, send, shield, encrypt, …). ASYNC:
|
||||
// some commands (sync/rescan/send/shield/import) block, so the bridge call runs on a detached
|
||||
// thread (captures the shared bridge, never `this`) and the result is delivered to a main-
|
||||
// thread slot drained by takeConsoleResult(). `commandLine` is "<command> [args]" — the first
|
||||
// whitespace-delimited token is the command and the remainder is passed as the single arg
|
||||
// string the backend expects (litelib_execute does not split args; use the JSON form for send).
|
||||
// Returns false if a console command is already running or no backend is linked. The caller
|
||||
// (tab) intercepts `clear`/`quit` BEFORE calling this — backend `clear` wipes wallet history.
|
||||
// Responses may contain secrets and are NEVER logged to LiteDiagnostics.
|
||||
bool runConsoleCommand(std::string commandLine);
|
||||
bool consoleCommandInProgress() const { return consoleRunning_ && consoleRunning_->load(); }
|
||||
bool takeConsoleResult(LiteConsoleResult& out);
|
||||
|
||||
// Synchronous cores for send/shield (block on the backend; safe off the UI thread). Used by
|
||||
// the async entry points above and directly by tests.
|
||||
LiteBroadcastResult sendTransactionBlocking(const LiteSendRequest& request);
|
||||
@@ -317,6 +341,14 @@ private:
|
||||
std::shared_ptr<std::optional<LiteBroadcastResult>> broadcastResult_ =
|
||||
std::make_shared<std::optional<LiteBroadcastResult>>();
|
||||
|
||||
// Detached interactive-console command runner (same shared-lifetime pattern as broadcast:
|
||||
// captures the shared bridge + running flag + result slot, never `this`).
|
||||
std::thread consoleThread_;
|
||||
std::shared_ptr<std::atomic<bool>> consoleRunning_ = std::make_shared<std::atomic<bool>>(false);
|
||||
std::shared_ptr<std::mutex> consoleResultMutex_ = std::make_shared<std::mutex>();
|
||||
std::shared_ptr<std::optional<LiteConsoleResult>> consoleResult_ =
|
||||
std::make_shared<std::optional<LiteConsoleResult>>();
|
||||
|
||||
// Asynchronous open with server failover (mirrors the sync/broadcast shared-lifetime pattern:
|
||||
// the detached thread captures only shared_ptrs + value copies, never `this`, so it can
|
||||
// safely outlive the controller). pumpAsyncOpen() finalizes the result on the main thread.
|
||||
|
||||
Reference in New Issue
Block a user