Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed refresh results, ordered RPC collectors, applicators, and price parsing. - Add daemon lifecycle and wallet security workflow helpers while preserving App-owned command RPC, decrypt, cancellation, and UI handoff behavior. - Split balance, console, mining, amount formatting, and async task logic into focused modules with expanded Phase 4 test coverage. - Fix market price loading by triggering price refresh immediately, avoiding queue-pressure drops, tracking loading/error state, and adding translations. - Polish send, explorer, peers, settings, theme/schema, and related tab UI. - Replace checked-in generated language headers with build-generated resources. - Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
20
src/util/amount_format.cpp
Normal file
20
src/util/amount_format.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "amount_format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <locale>
|
||||
#include <sstream>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
std::string formatAmountFixed(double amount, int decimals)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out.imbue(std::locale::classic());
|
||||
out << std::fixed << std::setprecision(std::max(0, decimals)) << amount;
|
||||
return out.str();
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
11
src/util/amount_format.h
Normal file
11
src/util/amount_format.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
std::string formatAmountFixed(double amount, int decimals = 8);
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
118
src/util/async_task_manager.cpp
Normal file
118
src/util/async_task_manager.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include "async_task_manager.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
AsyncTaskManager::~AsyncTaskManager()
|
||||
{
|
||||
cancelAll();
|
||||
joinAll();
|
||||
}
|
||||
|
||||
void AsyncTaskManager::submit(std::string name, Task task)
|
||||
{
|
||||
if (!task) return;
|
||||
|
||||
reapCompleted();
|
||||
join(name);
|
||||
|
||||
auto cancelled = std::make_shared<std::atomic<bool>>(false);
|
||||
auto done = std::make_shared<std::atomic<bool>>(false);
|
||||
Token token(cancelled);
|
||||
std::string taskName = name;
|
||||
|
||||
std::thread worker([taskName, token, done, task = std::move(task)]() mutable {
|
||||
try {
|
||||
task(token);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("[AsyncTask:%s] failed: %s\n", taskName.c_str(), e.what());
|
||||
} catch (...) {
|
||||
DEBUG_LOGF("[AsyncTask:%s] failed with unknown exception\n", taskName.c_str());
|
||||
}
|
||||
done->store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
tasks_.push_back({std::move(name), std::move(cancelled), std::move(done), std::move(worker)});
|
||||
}
|
||||
|
||||
void AsyncTaskManager::cancelAll()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (auto& task : tasks_) {
|
||||
task.cancelled->store(true, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncTaskManager::join(const std::string& name)
|
||||
{
|
||||
std::vector<TaskEntry> toJoin;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = tasks_.begin();
|
||||
while (it != tasks_.end()) {
|
||||
if (it->name == name) {
|
||||
toJoin.push_back(std::move(*it));
|
||||
it = tasks_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& task : toJoin) {
|
||||
if (task.worker.joinable()) task.worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncTaskManager::joinAll()
|
||||
{
|
||||
std::vector<TaskEntry> toJoin;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
toJoin.swap(tasks_);
|
||||
}
|
||||
|
||||
for (auto& task : toJoin) {
|
||||
if (task.worker.joinable()) task.worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncTaskManager::reapCompleted()
|
||||
{
|
||||
std::vector<TaskEntry> toJoin;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = tasks_.begin();
|
||||
while (it != tasks_.end()) {
|
||||
if (it->done->load(std::memory_order_acquire)) {
|
||||
toJoin.push_back(std::move(*it));
|
||||
it = tasks_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& task : toJoin) {
|
||||
if (task.worker.joinable()) task.worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
bool AsyncTaskManager::isRunning(const std::string& name) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (const auto& task : tasks_) {
|
||||
if (task.name == name && !task.done->load(std::memory_order_acquire)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
58
src/util/async_task_manager.h
Normal file
58
src/util/async_task_manager.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
class AsyncTaskManager {
|
||||
public:
|
||||
class Token {
|
||||
public:
|
||||
Token() = default;
|
||||
explicit Token(std::shared_ptr<std::atomic<bool>> cancelled)
|
||||
: cancelled_(std::move(cancelled)) {}
|
||||
|
||||
bool cancelled() const {
|
||||
return cancelled_ && cancelled_->load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<std::atomic<bool>> cancelled_;
|
||||
};
|
||||
|
||||
using Task = std::function<void(const Token&)>;
|
||||
|
||||
AsyncTaskManager() = default;
|
||||
~AsyncTaskManager();
|
||||
|
||||
AsyncTaskManager(const AsyncTaskManager&) = delete;
|
||||
AsyncTaskManager& operator=(const AsyncTaskManager&) = delete;
|
||||
|
||||
void submit(std::string name, Task task);
|
||||
void cancelAll();
|
||||
void join(const std::string& name);
|
||||
void joinAll();
|
||||
void reapCompleted();
|
||||
bool isRunning(const std::string& name) const;
|
||||
|
||||
private:
|
||||
struct TaskEntry {
|
||||
std::string name;
|
||||
std::shared_ptr<std::atomic<bool>> cancelled;
|
||||
std::shared_ptr<std::atomic<bool>> done;
|
||||
std::thread worker;
|
||||
};
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::vector<TaskEntry> tasks_;
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
@@ -68,6 +68,7 @@ Bootstrap::~Bootstrap() {
|
||||
|
||||
void Bootstrap::start(const std::string& dataDir, const std::string& url) {
|
||||
if (worker_running_) return; // already running
|
||||
if (worker_.joinable()) worker_.join();
|
||||
|
||||
cancel_requested_ = false;
|
||||
worker_running_ = true;
|
||||
@@ -175,7 +176,6 @@ void Bootstrap::start(const std::string& dataDir, const std::string& url) {
|
||||
setProgress(State::Completed, "Bootstrap complete!");
|
||||
worker_running_ = false;
|
||||
});
|
||||
worker_.detach();
|
||||
}
|
||||
|
||||
void Bootstrap::cancel() {
|
||||
|
||||
@@ -917,6 +917,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["market_now"] = "Now";
|
||||
strings_["market_pct_shielded"] = "%.0f%% Shielded";
|
||||
strings_["market_portfolio"] = "PORTFOLIO";
|
||||
strings_["market_price_loading"] = "Loading price data...";
|
||||
strings_["market_price_unavailable"] = "Price data unavailable";
|
||||
strings_["market_refresh_price"] = "Refresh price data";
|
||||
strings_["market_trade_on"] = "Trade on %s";
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -31,6 +33,8 @@
|
||||
#include <pwd.h>
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#ifdef __APPLE__
|
||||
#include <mach-o/dyld.h>
|
||||
#include <sys/sysctl.h>
|
||||
@@ -40,25 +44,83 @@
|
||||
|
||||
#include "../util/logger.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
extern char **environ;
|
||||
#endif
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
namespace {
|
||||
|
||||
bool hasAllowedUrlScheme(const std::string& url)
|
||||
{
|
||||
auto startsWithNoCase = [&url](const char* prefix) {
|
||||
for (size_t i = 0; prefix[i] != '\0'; ++i) {
|
||||
if (i >= url.size()) return false;
|
||||
unsigned char lhs = static_cast<unsigned char>(url[i]);
|
||||
unsigned char rhs = static_cast<unsigned char>(prefix[i]);
|
||||
if (std::tolower(lhs) != std::tolower(rhs)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return startsWithNoCase("http://") || startsWithNoCase("https://");
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
bool launchOpener(const char* opener, const std::string& target)
|
||||
{
|
||||
char* const argv[] = {
|
||||
const_cast<char*>(opener),
|
||||
const_cast<char*>(target.c_str()),
|
||||
nullptr
|
||||
};
|
||||
|
||||
pid_t pid = 0;
|
||||
int rc = posix_spawnp(&pid, opener, nullptr, nullptr, argv, environ);
|
||||
if (rc != 0) {
|
||||
DEBUG_LOGF("Failed to launch %s: %s\n", opener, std::strerror(rc));
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
pid_t waited = 0;
|
||||
do {
|
||||
waited = waitpid(pid, &status, 0);
|
||||
} while (waited < 0 && errno == EINTR);
|
||||
|
||||
if (waited < 0) {
|
||||
DEBUG_LOGF("Failed waiting for %s launcher: %s\n", opener, std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
DEBUG_LOGF("Launcher %s exited with status %d\n", opener,
|
||||
WIFEXITED(status) ? WEXITSTATUS(status) : -1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Platform::openUrl(const std::string& url)
|
||||
{
|
||||
if (url.empty()) return false;
|
||||
if (!hasAllowedUrlScheme(url)) {
|
||||
DEBUG_LOGF("Refusing to open URL with unsupported scheme: %s\n", url.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows: Use ShellExecute
|
||||
HINSTANCE result = ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
return (reinterpret_cast<intptr_t>(result) > 32);
|
||||
#elif defined(__APPLE__)
|
||||
// macOS: Use 'open' command
|
||||
std::string cmd = "open \"" + url + "\" &";
|
||||
return (system(cmd.c_str()) == 0);
|
||||
return launchOpener("open", url);
|
||||
#else
|
||||
// Linux: Use xdg-open
|
||||
std::string cmd = "xdg-open \"" + url + "\" >/dev/null 2>&1 &";
|
||||
return (system(cmd.c_str()) == 0);
|
||||
return launchOpener("xdg-open", url);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -68,15 +130,12 @@ bool Platform::openFolder(const std::string& path, bool createIfMissing)
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (createIfMissing) {
|
||||
#ifdef _WIN32
|
||||
// Windows: Create directory recursively
|
||||
std::string cmd = "mkdir \"" + path + "\" 2>nul";
|
||||
(void)system(cmd.c_str()); // Ignore return value - dir may already exist
|
||||
#else
|
||||
// Linux/macOS: Create directory with parents
|
||||
std::string cmd = "mkdir -p \"" + path + "\" 2>/dev/null";
|
||||
(void)system(cmd.c_str()); // Ignore return value - dir may already exist
|
||||
#endif
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path, ec);
|
||||
if (ec) {
|
||||
DEBUG_LOGF("Failed to create folder %s: %s\n", path.c_str(), ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -84,13 +143,9 @@ bool Platform::openFolder(const std::string& path, bool createIfMissing)
|
||||
HINSTANCE result = ShellExecuteA(nullptr, "explore", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
return (reinterpret_cast<intptr_t>(result) > 32);
|
||||
#elif defined(__APPLE__)
|
||||
// macOS: Use 'open' command (works for folders too)
|
||||
std::string cmd = "open \"" + path + "\" &";
|
||||
return (system(cmd.c_str()) == 0);
|
||||
return launchOpener("open", path);
|
||||
#else
|
||||
// Linux: Use xdg-open (works for folders too)
|
||||
std::string cmd = "xdg-open \"" + path + "\" >/dev/null 2>&1 &";
|
||||
return (system(cmd.c_str()) == 0);
|
||||
return launchOpener("xdg-open", path);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user