ui.toml and the default banlist were embedded by a CMake function that read each file and emitted a C array header one byte at a time in a configure-time loop. That is slow to configure, and because the result is a header, editing either resource recompiled every translation unit that included it. - Both files are now embedded with INCBIN from a small generated translation unit, the same pattern already used for the fonts. There is no generated header, so editing a resource re-assembles one tiny object file instead of triggering a wide recompile, and configure does no per-byte work. - Consumers include a hand-written header declaring the symbols and always use the embedded data (the __has_include fallback is gone). The data is not NUL-terminated, so every consumer passes an explicit size. - The old EmbedResources.cmake script is removed, and the generated unit is added to the app and test builds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1097 lines
38 KiB
C++
1097 lines
38 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "platform.h"
|
|
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <cerrno>
|
|
#include <cctype>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <filesystem>
|
|
#include <system_error>
|
|
#include <thread>
|
|
|
|
// Embedded ui.toml (INCBIN)
|
|
#include "embedded/embedded_text.h"
|
|
#include <sys/stat.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <shellapi.h>
|
|
#include <shlobj.h>
|
|
#include <psapi.h>
|
|
#include <tlhelp32.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#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>
|
|
#include <mach/mach.h>
|
|
#endif
|
|
#endif
|
|
|
|
#include "../util/logger.h"
|
|
#include "../config/version.h" // DRAGONX_APP_NAME — variant-specific config folder name
|
|
|
|
#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
|
|
};
|
|
|
|
// The opener must not share the wallet's stdio (a text-mode browser fallback such as lynx/w3m would
|
|
// grab the terminal); the old `sh -c '... >/dev/null 2>&1 &'` redirected these.
|
|
posix_spawn_file_actions_t fa;
|
|
const bool haveFa = (posix_spawn_file_actions_init(&fa) == 0);
|
|
if (haveFa) {
|
|
posix_spawn_file_actions_addopen(&fa, STDIN_FILENO, "/dev/null", O_RDONLY, 0);
|
|
posix_spawn_file_actions_addopen(&fa, STDOUT_FILENO, "/dev/null", O_WRONLY, 0);
|
|
posix_spawn_file_actions_addopen(&fa, STDERR_FILENO, "/dev/null", O_WRONLY, 0);
|
|
}
|
|
|
|
pid_t pid = 0;
|
|
int rc = posix_spawnp(&pid, opener, haveFa ? &fa : nullptr, nullptr, argv, environ);
|
|
if (haveFa) posix_spawn_file_actions_destroy(&fa);
|
|
if (rc != 0) {
|
|
DEBUG_LOGF("Failed to launch %s: %s\n", opener, std::strerror(rc));
|
|
return false;
|
|
}
|
|
|
|
// Never wait on the caller (the UI thread): generic xdg-open sessions (sway/i3/WSLg/...) run the
|
|
// browser / file manager as xdg-open's foreground child, so the old waitpid() froze the window until
|
|
// the user closed that app. A detached waiter reaps it instead. `opener` is always a string literal.
|
|
// No Logger in the waiter: it can wake during static teardown (tests), where Logger is gone.
|
|
try {
|
|
std::thread([pid, opener] {
|
|
int status = 0;
|
|
pid_t waited = 0;
|
|
do {
|
|
waited = waitpid(pid, &status, 0);
|
|
} while (waited < 0 && errno == EINTR);
|
|
if (waited >= 0 && (!WIFEXITED(status) || WEXITSTATUS(status) != 0))
|
|
std::fprintf(stderr, "Launcher %s exited with status %d\n", opener,
|
|
WIFEXITED(status) ? WEXITSTATUS(status) : -1);
|
|
}).detach();
|
|
} catch (const std::system_error&) {
|
|
int status = 0;
|
|
(void)waitpid(pid, &status, WNOHANG); // out of threads: worst case one zombie until exit
|
|
}
|
|
return true; // "launched"; the opener's exit status is no longer awaited (no caller reads it)
|
|
}
|
|
#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__)
|
|
return launchOpener("open", url);
|
|
#else
|
|
return launchOpener("xdg-open", url);
|
|
#endif
|
|
}
|
|
|
|
bool Platform::ensureDirectory(const std::string& dir, std::string* outError)
|
|
{
|
|
if (dir.empty()) {
|
|
if (outError) *outError = "Cannot create directory: empty path.";
|
|
return false;
|
|
}
|
|
std::error_code ec;
|
|
if (std::filesystem::is_directory(dir, ec)) return true;
|
|
ec.clear();
|
|
std::filesystem::create_directories(dir, ec);
|
|
if (ec) {
|
|
if (outError) {
|
|
*outError = "Cannot create " + dir + ": " + ec.message() +
|
|
". Check permissions / free space.";
|
|
}
|
|
DEBUG_LOGF("[ERROR] ensureDirectory failed for %s: %s\n", dir.c_str(), ec.message().c_str());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Platform::openFolder(const std::string& path, bool createIfMissing)
|
|
{
|
|
if (path.empty()) return false;
|
|
|
|
// Create directory if it doesn't exist
|
|
if (createIfMissing) {
|
|
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
|
|
// Windows: Use explorer.exe to open folder
|
|
HINSTANCE result = ShellExecuteA(nullptr, "explore", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
|
return (reinterpret_cast<intptr_t>(result) > 32);
|
|
#elif defined(__APPLE__)
|
|
return launchOpener("open", path);
|
|
#else
|
|
return launchOpener("xdg-open", path);
|
|
#endif
|
|
}
|
|
|
|
uint64_t Platform::getFileSize(const std::string& path)
|
|
{
|
|
struct stat st;
|
|
if (stat(path.c_str(), &st) == 0) {
|
|
return static_cast<uint64_t>(st.st_size);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
std::string Platform::formatFileSize(uint64_t bytes)
|
|
{
|
|
char buf[64];
|
|
|
|
if (bytes >= 1024ULL * 1024ULL * 1024ULL) {
|
|
snprintf(buf, sizeof(buf), "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
|
} else if (bytes >= 1024ULL * 1024ULL) {
|
|
snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
|
|
} else if (bytes >= 1024ULL) {
|
|
snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "%llu bytes", static_cast<unsigned long long>(bytes));
|
|
}
|
|
|
|
return std::string(buf);
|
|
}
|
|
|
|
std::string Platform::getHomeDir()
|
|
{
|
|
#ifdef _WIN32
|
|
char path[MAX_PATH];
|
|
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_PROFILE, nullptr, 0, path))) {
|
|
return std::string(path);
|
|
}
|
|
// Fallback
|
|
const char* home = getenv("USERPROFILE");
|
|
return home ? home : "C:\\";
|
|
#else
|
|
const char* home = getenv("HOME");
|
|
if (home) return home;
|
|
|
|
// Fallback to passwd entry
|
|
struct passwd* pw = getpwuid(getuid());
|
|
if (pw && pw->pw_dir) return pw->pw_dir;
|
|
|
|
return "/tmp";
|
|
#endif
|
|
}
|
|
|
|
std::string Platform::getDragonXDataDir()
|
|
{
|
|
#ifdef _WIN32
|
|
char path[MAX_PATH];
|
|
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
|
|
return std::string(path) + "\\Hush\\DRAGONX\\";
|
|
}
|
|
return getHomeDir() + "\\AppData\\Roaming\\Hush\\DRAGONX\\";
|
|
#elif defined(__APPLE__)
|
|
return getHomeDir() + "/Library/Application Support/Hush/DRAGONX/";
|
|
#else
|
|
return getHomeDir() + "/.hush/DRAGONX/";
|
|
#endif
|
|
}
|
|
|
|
std::string Platform::getDataDir()
|
|
{
|
|
return getDragonXDataDir();
|
|
}
|
|
|
|
std::string Platform::getLiteWalletDataDir()
|
|
{
|
|
// Mirror the SilentDragonXLite backend's get_zcash_data_path() for the "main" chain:
|
|
// Windows/macOS: dirs::data_dir()/silentdragonxlite (data_dir = %APPDATA%, ~/Library/App Support)
|
|
// Linux: ~/.silentdragonxlite
|
|
#ifdef _WIN32
|
|
char path[MAX_PATH];
|
|
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
|
|
return std::string(path) + "\\silentdragonxlite\\";
|
|
}
|
|
return getHomeDir() + "\\AppData\\Roaming\\silentdragonxlite\\";
|
|
#elif defined(__APPLE__)
|
|
return getHomeDir() + "/Library/Application Support/silentdragonxlite/";
|
|
#else
|
|
return getHomeDir() + "/.silentdragonxlite/";
|
|
#endif
|
|
}
|
|
|
|
std::string Platform::getConfigDir()
|
|
{
|
|
#ifdef _WIN32
|
|
char path[MAX_PATH];
|
|
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
|
|
return std::string(path) + "\\" DRAGONX_APP_NAME "\\";
|
|
}
|
|
return getHomeDir() + "\\AppData\\Roaming\\" DRAGONX_APP_NAME "\\";
|
|
#elif defined(__APPLE__)
|
|
// macOS-native config location (matches what settings.json / addressbook.json use).
|
|
return getHomeDir() + "/Library/Application Support/" DRAGONX_APP_NAME "/";
|
|
#else
|
|
return getHomeDir() + "/.config/" DRAGONX_APP_NAME "/";
|
|
#endif
|
|
}
|
|
|
|
bool Platform::deleteFile(const std::string& path)
|
|
{
|
|
#ifdef _WIN32
|
|
return DeleteFileA(path.c_str()) != 0 || GetLastError() == ERROR_FILE_NOT_FOUND;
|
|
#else
|
|
return (remove(path.c_str()) == 0 || errno == ENOENT);
|
|
#endif
|
|
}
|
|
|
|
bool Platform::secureWipeAndRemove(const std::string& path)
|
|
{
|
|
// Best-effort; SSD wear-levelling and CoW/journaling filesystems may retain copies.
|
|
if (path.empty()) return false;
|
|
static constexpr size_t kChunk = 64 * 1024;
|
|
const std::string buf(kChunk, static_cast<char>(0xAB));
|
|
#ifdef _WIN32
|
|
HANDLE h = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, nullptr);
|
|
if (h != INVALID_HANDLE_VALUE) {
|
|
LARGE_INTEGER sz;
|
|
if (GetFileSizeEx(h, &sz) && sz.QuadPart > 0) {
|
|
long long left = sz.QuadPart;
|
|
while (left > 0) {
|
|
DWORD n = static_cast<DWORD>(left < (long long)kChunk ? left : (long long)kChunk);
|
|
DWORD written = 0;
|
|
if (!WriteFile(h, buf.data(), n, &written, nullptr) || written == 0) break;
|
|
left -= written;
|
|
}
|
|
FlushFileBuffers(h);
|
|
}
|
|
CloseHandle(h);
|
|
}
|
|
DeleteFileA(path.c_str());
|
|
return GetFileAttributesA(path.c_str()) == INVALID_FILE_ATTRIBUTES;
|
|
#else
|
|
int fd = ::open(path.c_str(), O_WRONLY | O_NOFOLLOW | O_CLOEXEC);
|
|
if (fd >= 0) {
|
|
struct stat st;
|
|
if (::fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0) {
|
|
off_t off = 0;
|
|
const off_t total = st.st_size;
|
|
while (off < total) {
|
|
size_t n = static_cast<size_t>(total - off < (off_t)kChunk ? total - off : (off_t)kChunk);
|
|
ssize_t w = ::pwrite(fd, buf.data(), n, off);
|
|
if (w <= 0) break;
|
|
off += w;
|
|
}
|
|
#ifdef __APPLE__
|
|
if (::fcntl(fd, F_FULLFSYNC) != 0) ::fsync(fd);
|
|
#else
|
|
::fsync(fd);
|
|
#endif
|
|
}
|
|
::close(fd);
|
|
}
|
|
::remove(path.c_str());
|
|
struct stat st2;
|
|
return ::lstat(path.c_str(), &st2) != 0 && errno == ENOENT;
|
|
#endif
|
|
}
|
|
|
|
bool Platform::writeFileAtomically(const std::string& path,
|
|
const std::string& content,
|
|
bool restrictPermissions)
|
|
{
|
|
namespace fs = std::filesystem;
|
|
std::error_code ec;
|
|
fs::path target(path);
|
|
fs::path dir = target.parent_path();
|
|
if (!dir.empty()) {
|
|
fs::create_directories(dir, ec); // ignore ec; the open below will fail if truly unwritable
|
|
}
|
|
const std::string tmp = path + ".tmp";
|
|
|
|
#ifdef _WIN32
|
|
{
|
|
// One handle for write + flush. CreateFileA with no SECURITY_ATTRIBUTES is NOT inheritable: the old
|
|
// std::ofstream (CRT) handle was, so a child spawned with bInheritHandles=TRUE at that moment (xmrig,
|
|
// runHiddenCapture) kept "<path>.tmp" open without FILE_SHARE_DELETE and every later MoveFileEx
|
|
// failed while it ran. Also no second share-mode-0 open, which silently skipped the flush whenever
|
|
// AV / the indexer had the new file open.
|
|
HANDLE h = CreateFileA(tmp.c_str(), GENERIC_WRITE, 0, nullptr,
|
|
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (h == INVALID_HANDLE_VALUE) return false;
|
|
bool ok = true;
|
|
size_t written = 0;
|
|
while (written < content.size()) {
|
|
const size_t left = content.size() - written;
|
|
const DWORD chunk = static_cast<DWORD>(left > (1u << 30) ? (1u << 30) : left);
|
|
DWORD n = 0;
|
|
if (!WriteFile(h, content.data() + written, chunk, &n, nullptr) || n == 0) { ok = false; break; }
|
|
written += n;
|
|
}
|
|
// Flush the temp file's contents to stable storage before the rename.
|
|
if (ok && !FlushFileBuffers(h)) ok = false;
|
|
CloseHandle(h);
|
|
if (!ok) { DeleteFileA(tmp.c_str()); return false; }
|
|
}
|
|
(void)restrictPermissions; // owner-only ACLs rely on the per-user profile dir on Windows
|
|
// MoveFileEx with REPLACE_EXISTING is atomic on NTFS; WRITE_THROUGH makes it durable. AV / the search
|
|
// indexer can hold the destination briefly (ACCESS_DENIED / SHARING_VIOLATION / LOCK_VIOLATION): retry
|
|
// a few times (20+40+80+160 ms worst case, only on a write that used to just fail) instead of dropping it.
|
|
BOOL moved = FALSE;
|
|
DWORD delayMs = 20;
|
|
for (int attempt = 0; attempt < 5; ++attempt) {
|
|
moved = MoveFileExA(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
|
|
if (moved) break;
|
|
const DWORD err = GetLastError();
|
|
if (err != ERROR_ACCESS_DENIED && err != ERROR_SHARING_VIOLATION && err != ERROR_LOCK_VIOLATION) break;
|
|
if (attempt < 4) { Sleep(delayMs); delayMs *= 2; }
|
|
}
|
|
if (!moved) {
|
|
DeleteFileA(tmp.c_str());
|
|
return false;
|
|
}
|
|
return true;
|
|
#else
|
|
const mode_t mode = restrictPermissions ? 0600 : 0644;
|
|
// O_CLOEXEC: the background settings writer can race a dragonxd/xmrig fork+exec; don't leak this fd.
|
|
int fd = ::open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode);
|
|
if (fd < 0) return false;
|
|
if (restrictPermissions) {
|
|
::fchmod(fd, 0600); // enforce even if the temp file pre-existed with looser perms
|
|
}
|
|
bool ok = true;
|
|
size_t written = 0;
|
|
const char* data = content.data();
|
|
while (written < content.size()) {
|
|
ssize_t n = ::write(fd, data + written, content.size() - written);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
ok = false;
|
|
break;
|
|
}
|
|
written += static_cast<size_t>(n);
|
|
}
|
|
if (ok && ::fsync(fd) != 0) ok = false;
|
|
::close(fd);
|
|
if (!ok) {
|
|
::remove(tmp.c_str());
|
|
return false;
|
|
}
|
|
if (::rename(tmp.c_str(), path.c_str()) != 0) {
|
|
::remove(tmp.c_str());
|
|
return false;
|
|
}
|
|
// fsync the directory so the rename (the metadata that makes the new file
|
|
// visible) survives a crash, not just the file contents.
|
|
int dfd = ::open(dir.empty() ? "." : dir.c_str(), O_RDONLY | O_CLOEXEC);
|
|
if (dfd >= 0) {
|
|
::fsync(dfd);
|
|
::close(dfd);
|
|
}
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
std::string Platform::getExecutableDirectory()
|
|
{
|
|
#ifdef _WIN32
|
|
char path[MAX_PATH];
|
|
DWORD len = GetModuleFileNameA(nullptr, path, MAX_PATH);
|
|
if (len > 0 && len < MAX_PATH) {
|
|
std::string fullPath(path);
|
|
size_t pos = fullPath.find_last_of("\\/");
|
|
if (pos != std::string::npos) {
|
|
return fullPath.substr(0, pos);
|
|
}
|
|
}
|
|
return ".";
|
|
#elif defined(__APPLE__)
|
|
char path[1024];
|
|
uint32_t size = sizeof(path);
|
|
if (_NSGetExecutablePath(path, &size) == 0) {
|
|
char* resolved = realpath(path, nullptr);
|
|
if (resolved) {
|
|
std::string fullPath(resolved);
|
|
free(resolved);
|
|
size_t pos = fullPath.find_last_of('/');
|
|
if (pos != std::string::npos) {
|
|
return fullPath.substr(0, pos);
|
|
}
|
|
}
|
|
}
|
|
return ".";
|
|
#else
|
|
// Linux: read /proc/self/exe
|
|
char path[1024];
|
|
ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
|
if (len > 0) {
|
|
path[len] = '\0';
|
|
std::string fullPath(path);
|
|
size_t pos = fullPath.find_last_of('/');
|
|
if (pos != std::string::npos) {
|
|
return fullPath.substr(0, pos);
|
|
}
|
|
}
|
|
return ".";
|
|
#endif
|
|
}
|
|
|
|
std::string getExecutableDirectory()
|
|
{
|
|
return Platform::getExecutableDirectory();
|
|
}
|
|
|
|
std::string Platform::getObsidianDragonDir()
|
|
{
|
|
#ifdef _WIN32
|
|
const char* appdata = std::getenv("APPDATA");
|
|
if (appdata) {
|
|
return (std::filesystem::path(appdata) / DRAGONX_APP_NAME).string();
|
|
}
|
|
return (std::filesystem::path(getHomeDir()) / "AppData" / "Roaming" / DRAGONX_APP_NAME).string();
|
|
#elif defined(__APPLE__)
|
|
return (std::filesystem::path(getHomeDir()) / "Library" / "Application Support" / DRAGONX_APP_NAME).string();
|
|
#else
|
|
const char* xdg_config = std::getenv("XDG_CONFIG_HOME");
|
|
if (xdg_config) {
|
|
return (std::filesystem::path(xdg_config) / DRAGONX_APP_NAME).string();
|
|
}
|
|
return (std::filesystem::path(getHomeDir()) / ".config" / DRAGONX_APP_NAME).string();
|
|
#endif
|
|
}
|
|
|
|
void Platform::ensureObsidianDragonSetup()
|
|
{
|
|
namespace fs = std::filesystem;
|
|
|
|
fs::path base = getObsidianDragonDir();
|
|
fs::path themes_dir = base / "themes";
|
|
fs::path example_dir = base / "example" / "example_theme";
|
|
fs::path example_img = example_dir / "img";
|
|
|
|
// Create directory structure
|
|
std::error_code ec;
|
|
fs::create_directories(themes_dir, ec);
|
|
fs::create_directories(example_img, ec);
|
|
if (ec) {
|
|
DEBUG_LOGF("Warning: Could not create ObsidianDragon directories: %s\n", ec.message().c_str());
|
|
return;
|
|
}
|
|
DEBUG_LOGF("ObsidianDragon: Config directory: %s\n", base.string().c_str());
|
|
|
|
// Write example theme.toml — a full copy of ui.toml so users can see
|
|
// and modify the complete layout+theme structure. UISchema can load
|
|
// themes from TOML format.
|
|
// Regenerate whenever the binary is newer than the example file, so
|
|
// layout changes in ui.toml are always reflected.
|
|
fs::path example_toml = example_dir / "theme.toml";
|
|
bool needsWrite = !fs::exists(example_toml);
|
|
if (!needsWrite) {
|
|
// Regenerate if the running binary is newer than the example file
|
|
fs::path exePath = fs::path(getExecutableDirectory()) /
|
|
#ifdef _WIN32
|
|
DRAGONX_APP_NAME ".exe";
|
|
#else
|
|
DRAGONX_APP_NAME;
|
|
#endif
|
|
std::error_code tec;
|
|
auto exeTime = fs::last_write_time(exePath, tec);
|
|
auto fileTime = fs::last_write_time(example_toml, tec);
|
|
if (!tec && exeTime > fileTime)
|
|
needsWrite = true;
|
|
}
|
|
if (needsWrite) {
|
|
std::ofstream f(example_toml);
|
|
if (f.is_open()) {
|
|
// Use the build-time embedded copy first; fall back to the
|
|
// bundled file on disk.
|
|
std::string content;
|
|
content.assign(reinterpret_cast<const char*>(g_ui_toml_data), g_ui_toml_size);
|
|
|
|
if (!content.empty()) {
|
|
// Patch the theme metadata so the example is clearly
|
|
// distinguishable from the bundled default.
|
|
// In TOML: name = "DragonX" → name = "Example Theme"
|
|
auto replaceFirst = [&](const std::string& key,
|
|
const std::string& oldVal,
|
|
const std::string& newVal) {
|
|
// Find 'key = "oldVal"' pattern and replace oldVal
|
|
std::string needle = key + " = ";
|
|
auto pos = content.find(needle);
|
|
if (pos == std::string::npos) return;
|
|
auto vpos = content.find(oldVal, pos);
|
|
if (vpos != std::string::npos && vpos - pos < 80)
|
|
content.replace(vpos, oldVal.size(), newVal);
|
|
};
|
|
replaceFirst("name", "\"DragonX\"", "\"Example Theme\"");
|
|
replaceFirst("author", "\"The Hush Developers\"", "\"Your Name\"");
|
|
|
|
// Prepend comment block (TOML uses # for comments natively)
|
|
std::string header =
|
|
"# === DragonX Wallet Theme File ===\n"
|
|
"# Copy this entire folder into the 'themes' folder to use it.\n"
|
|
"# The folder name becomes the theme ID.\n"
|
|
"#\n"
|
|
"# === Images ===\n"
|
|
"# Place image files in the 'img' subfolder.\n"
|
|
"# Specify custom filenames below, or use the defaults:\n"
|
|
"# backgrounds/gradient/dark_gradient.png - background gradient overlay\n"
|
|
"# logos/logo_ObsidianDragon_dark.png - sidebar logo (128x128 recommended)\n"
|
|
"#\n"
|
|
"# === Colors ===\n"
|
|
"# Colors use CSS-style formats: #RRGGBB, #RRGGBBAA, or rgba(r,g,b,a)\n"
|
|
"# Palette variables can be referenced elsewhere with var(--name)\n\n";
|
|
content = header + content;
|
|
} else {
|
|
DEBUG_LOGF("ObsidianDragon: Warning — ui.toml not available, skipping example theme\n");
|
|
}
|
|
|
|
f << content;
|
|
f.close();
|
|
DEBUG_LOGF("ObsidianDragon: Created example theme: %s\n", example_toml.string().c_str());
|
|
}
|
|
}
|
|
|
|
// Copy bundled images into example/example_theme/img/ for reference
|
|
fs::path exe_dir = getExecutableDirectory();
|
|
fs::path res_img = exe_dir / "res" / "img";
|
|
|
|
auto copyIfMissing = [&](const char* srcRelPath, const char* dstFilename) {
|
|
fs::path src = res_img / srcRelPath;
|
|
fs::path dst = example_img / dstFilename;
|
|
if (fs::exists(src) && !fs::exists(dst)) {
|
|
std::error_code cpec;
|
|
fs::copy_file(src, dst, cpec);
|
|
if (!cpec) {
|
|
DEBUG_LOGF("ObsidianDragon: Copied %s to example theme\n", dstFilename);
|
|
}
|
|
}
|
|
};
|
|
copyIfMissing("backgrounds/gradient/dark_gradient.png", "dark_gradient.png");
|
|
copyIfMissing("logos/logo_ObsidianDragon_dark.png", "logo_ObsidianDragon_dark.png");
|
|
copyIfMissing("logos/logo_ObsidianDragon_light.png", "logo_ObsidianDragon_light.png");
|
|
}
|
|
|
|
double Platform::getTotalSystemRAM_MB()
|
|
{
|
|
static double cached = 0.0;
|
|
if (cached > 0.0) return cached;
|
|
|
|
#ifdef _WIN32
|
|
MEMORYSTATUSEX memInfo;
|
|
memInfo.dwLength = sizeof(memInfo);
|
|
if (GlobalMemoryStatusEx(&memInfo)) {
|
|
cached = (double)memInfo.ullTotalPhys / (1024.0 * 1024.0);
|
|
}
|
|
#elif defined(__APPLE__)
|
|
int64_t memSize = 0;
|
|
size_t len = sizeof(memSize);
|
|
if (sysctlbyname("hw.memsize", &memSize, &len, nullptr, 0) == 0) {
|
|
cached = (double)memSize / (1024.0 * 1024.0);
|
|
}
|
|
#else
|
|
// Linux: use sysconf
|
|
long pages = sysconf(_SC_PHYS_PAGES);
|
|
long pageSize = sysconf(_SC_PAGE_SIZE);
|
|
if (pages > 0 && pageSize > 0) {
|
|
cached = (double)pages * (double)pageSize / (1024.0 * 1024.0);
|
|
}
|
|
#endif
|
|
return cached;
|
|
}
|
|
|
|
double Platform::getUsedSystemRAM_MB()
|
|
{
|
|
#ifdef _WIN32
|
|
MEMORYSTATUSEX memInfo;
|
|
memInfo.dwLength = sizeof(memInfo);
|
|
if (GlobalMemoryStatusEx(&memInfo)) {
|
|
double total = (double)memInfo.ullTotalPhys / (1024.0 * 1024.0);
|
|
double avail = (double)memInfo.ullAvailPhys / (1024.0 * 1024.0);
|
|
return total - avail;
|
|
}
|
|
return 0.0;
|
|
#elif defined(__APPLE__)
|
|
// macOS: vm_statistics64 for free/active/inactive/wired pages
|
|
vm_size_t pageSize = 0;
|
|
host_page_size(mach_host_self(), &pageSize);
|
|
vm_statistics64_data_t vmStats;
|
|
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
|
|
if (host_statistics64(mach_host_self(), HOST_VM_INFO64,
|
|
(host_info64_t)&vmStats, &count) == KERN_SUCCESS) {
|
|
// "used" = total - free - inactive (matches Activity Monitor "Memory Used")
|
|
double total = getTotalSystemRAM_MB();
|
|
double freeMB = (double)(vmStats.free_count + vmStats.inactive_count) * (double)pageSize / (1024.0 * 1024.0);
|
|
return total - freeMB;
|
|
}
|
|
return 0.0;
|
|
#else
|
|
// Linux: parse /proc/meminfo for MemAvailable
|
|
FILE* f = fopen("/proc/meminfo", "r");
|
|
if (!f) return 0.0;
|
|
double totalMB = 0.0, availMB = 0.0;
|
|
char line[256];
|
|
int found = 0;
|
|
while (fgets(line, sizeof(line), f) && found < 2) {
|
|
long val = 0;
|
|
if (strncmp(line, "MemTotal:", 9) == 0) {
|
|
if (sscanf(line + 9, "%ld", &val) == 1) { totalMB = (double)val / 1024.0; found++; }
|
|
} else if (strncmp(line, "MemAvailable:", 13) == 0) {
|
|
if (sscanf(line + 13, "%ld", &val) == 1) { availMB = (double)val / 1024.0; found++; }
|
|
}
|
|
}
|
|
fclose(f);
|
|
if (found == 2) return totalMB - availMB;
|
|
return 0.0;
|
|
#endif
|
|
}
|
|
|
|
double Platform::getSelfMemoryUsageMB()
|
|
{
|
|
#ifdef _WIN32
|
|
PROCESS_MEMORY_COUNTERS pmc;
|
|
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
|
|
return (double)pmc.WorkingSetSize / (1024.0 * 1024.0);
|
|
}
|
|
return 0.0;
|
|
#elif defined(__APPLE__)
|
|
struct mach_task_basic_info info;
|
|
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
|
|
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
|
|
(task_info_t)&info, &count) == KERN_SUCCESS) {
|
|
return (double)info.resident_size / (1024.0 * 1024.0);
|
|
}
|
|
return 0.0;
|
|
#else
|
|
// Linux: read /proc/self/status for VmRSS
|
|
FILE* f = fopen("/proc/self/status", "r");
|
|
if (!f) return 0.0;
|
|
char line[256];
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (strncmp(line, "VmRSS:", 6) == 0) {
|
|
long val = 0;
|
|
if (sscanf(line + 6, "%ld", &val) == 1) {
|
|
fclose(f);
|
|
return (double)val / 1024.0; // kB -> MB
|
|
}
|
|
}
|
|
}
|
|
fclose(f);
|
|
return 0.0;
|
|
#endif
|
|
}
|
|
|
|
double Platform::getDaemonMemoryUsageMB()
|
|
{
|
|
#ifdef _WIN32
|
|
// Windows: use CreateToolhelp32Snapshot to find dragonxd.exe processes
|
|
// and query their working set size.
|
|
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if (snap == INVALID_HANDLE_VALUE) return 0.0;
|
|
|
|
PROCESSENTRY32 entry;
|
|
entry.dwSize = sizeof(entry);
|
|
|
|
double totalMB = 0.0;
|
|
if (Process32First(snap, &entry)) {
|
|
do {
|
|
if (_stricmp(entry.szExeFile, "dragonxd.exe") == 0) {
|
|
// GetProcessMemoryInfo requires PROCESS_QUERY_INFORMATION + PROCESS_VM_READ.
|
|
// Try full access first; fall back to limited for elevated processes.
|
|
HANDLE hProc = OpenProcess(
|
|
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
|
|
FALSE, entry.th32ProcessID);
|
|
if (!hProc) {
|
|
hProc = OpenProcess(
|
|
PROCESS_QUERY_LIMITED_INFORMATION,
|
|
FALSE, entry.th32ProcessID);
|
|
}
|
|
if (hProc) {
|
|
PROCESS_MEMORY_COUNTERS pmc;
|
|
ZeroMemory(&pmc, sizeof(pmc));
|
|
pmc.cb = sizeof(pmc);
|
|
if (GetProcessMemoryInfo(hProc, &pmc, sizeof(pmc)))
|
|
totalMB += (double)pmc.WorkingSetSize / (1024.0 * 1024.0);
|
|
CloseHandle(hProc);
|
|
}
|
|
}
|
|
} while (Process32Next(snap, &entry));
|
|
}
|
|
CloseHandle(snap);
|
|
return totalMB;
|
|
#elif defined(__APPLE__)
|
|
// macOS: use pgrep to find dragonxd PIDs, then read RSS via proc_pidinfo
|
|
FILE* fp = popen("pgrep -x dragonxd", "r");
|
|
if (!fp) return 0.0;
|
|
double totalMB = 0.0;
|
|
char line[64];
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
pid_t pid = (pid_t)atoi(line);
|
|
if (pid <= 0) continue;
|
|
struct mach_task_basic_info info;
|
|
mach_msg_type_number_t cnt = MACH_TASK_BASIC_INFO_COUNT;
|
|
// We can't directly task_for_pid without entitlements, so fall back to ps
|
|
char cmd[128];
|
|
snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d", pid);
|
|
FILE* ps = popen(cmd, "r");
|
|
if (ps) {
|
|
char rssLine[64];
|
|
if (fgets(rssLine, sizeof(rssLine), ps)) {
|
|
long rssKB = atol(rssLine);
|
|
if (rssKB > 0) totalMB += (double)rssKB / 1024.0;
|
|
}
|
|
pclose(ps);
|
|
}
|
|
}
|
|
pclose(fp);
|
|
return totalMB;
|
|
#else
|
|
// Linux: iterate /proc/<pid>/comm looking for "dragonxd"
|
|
DIR* procDir = opendir("/proc");
|
|
if (!procDir) return 0.0;
|
|
double totalMB = 0.0;
|
|
pid_t selfPid = getpid();
|
|
struct dirent* entry;
|
|
while ((entry = readdir(procDir)) != nullptr) {
|
|
// Only numeric directories (PIDs)
|
|
if (entry->d_name[0] < '0' || entry->d_name[0] > '9') continue;
|
|
pid_t pid = (pid_t)atoi(entry->d_name);
|
|
if (pid <= 0 || pid == selfPid) continue;
|
|
|
|
// Read comm (process name)
|
|
char commPath[64];
|
|
snprintf(commPath, sizeof(commPath), "/proc/%d/comm", pid);
|
|
FILE* cf = fopen(commPath, "r");
|
|
if (!cf) continue;
|
|
char comm[256] = {};
|
|
if (fgets(comm, sizeof(comm), cf)) {
|
|
// Strip trailing newline
|
|
size_t len = strlen(comm);
|
|
if (len > 0 && comm[len - 1] == '\n') comm[len - 1] = '\0';
|
|
}
|
|
fclose(cf);
|
|
|
|
if (strcmp(comm, "dragonxd") != 0) continue;
|
|
|
|
// Read VmRSS from /proc/<pid>/status
|
|
char statusPath[64];
|
|
snprintf(statusPath, sizeof(statusPath), "/proc/%d/status", pid);
|
|
FILE* sf = fopen(statusPath, "r");
|
|
if (!sf) continue;
|
|
char sline[256];
|
|
while (fgets(sline, sizeof(sline), sf)) {
|
|
if (strncmp(sline, "VmRSS:", 6) == 0) {
|
|
long val = 0;
|
|
if (sscanf(sline + 6, "%ld", &val) == 1)
|
|
totalMB += (double)val / 1024.0; // kB -> MB
|
|
break;
|
|
}
|
|
}
|
|
fclose(sf);
|
|
}
|
|
closedir(procDir);
|
|
return totalMB;
|
|
#endif
|
|
}
|
|
|
|
// ============================================================================
|
|
// System-wide idle time detection
|
|
// ============================================================================
|
|
|
|
int Platform::getSystemIdleSeconds()
|
|
{
|
|
#ifdef _WIN32
|
|
LASTINPUTINFO lii;
|
|
lii.cbSize = sizeof(lii);
|
|
if (GetLastInputInfo(&lii)) {
|
|
DWORD elapsed = GetTickCount() - lii.dwTime;
|
|
return (int)(elapsed / 1000);
|
|
}
|
|
return 0;
|
|
#elif defined(__APPLE__)
|
|
// macOS: use IOKit HIDSystem idle time
|
|
// Fallback to 0 (feature not supported) to keep it simple
|
|
return 0;
|
|
#else
|
|
// Linux: dynamically load libXss to query X11 screensaver idle time
|
|
// This avoids a hard link dependency on X11/Xss libraries.
|
|
typedef struct {
|
|
int type;
|
|
unsigned long serial;
|
|
int/*Bool*/ send_event;
|
|
void* /*Display**/ display;
|
|
unsigned long/*Drawable*/ window;
|
|
int state;
|
|
int kind;
|
|
unsigned long til_or_since;
|
|
unsigned long idle;
|
|
unsigned long eventMask;
|
|
} XScreenSaverInfo;
|
|
|
|
// Function pointer types
|
|
typedef void* (*XOpenDisplay_t)(const char*);
|
|
typedef unsigned long (*XDefaultRootWindow_t)(void*);
|
|
typedef XScreenSaverInfo* (*XScreenSaverAllocInfo_t)(void);
|
|
typedef int (*XScreenSaverQueryInfo_t)(void*, unsigned long, XScreenSaverInfo*);
|
|
typedef int (*XCloseDisplay_t)(void*);
|
|
typedef void (*XFree_t)(void*);
|
|
|
|
static bool s_tried = false;
|
|
static void* s_x11 = nullptr;
|
|
static void* s_xss = nullptr;
|
|
static XOpenDisplay_t s_XOpenDisplay = nullptr;
|
|
static XDefaultRootWindow_t s_XDefaultRootWindow = nullptr;
|
|
static XScreenSaverAllocInfo_t s_XScreenSaverAllocInfo = nullptr;
|
|
static XScreenSaverQueryInfo_t s_XScreenSaverQueryInfo = nullptr;
|
|
static XCloseDisplay_t s_XCloseDisplay = nullptr;
|
|
(void)s_XCloseDisplay; // retained for potential cleanup
|
|
static XFree_t s_XFree = nullptr;
|
|
static void* s_display = nullptr;
|
|
|
|
if (!s_tried) {
|
|
s_tried = true;
|
|
s_x11 = dlopen("libX11.so.6", RTLD_LAZY);
|
|
s_xss = dlopen("libXss.so.1", RTLD_LAZY);
|
|
if (s_x11 && s_xss) {
|
|
s_XOpenDisplay = (XOpenDisplay_t)dlsym(s_x11, "XOpenDisplay");
|
|
s_XDefaultRootWindow = (XDefaultRootWindow_t)dlsym(s_x11, "XDefaultRootWindow");
|
|
s_XCloseDisplay = (XCloseDisplay_t)dlsym(s_x11, "XCloseDisplay"); // NOLINT
|
|
s_XFree = (XFree_t)dlsym(s_x11, "XFree");
|
|
s_XScreenSaverAllocInfo = (XScreenSaverAllocInfo_t)dlsym(s_xss, "XScreenSaverAllocInfo");
|
|
s_XScreenSaverQueryInfo = (XScreenSaverQueryInfo_t)dlsym(s_xss, "XScreenSaverQueryInfo");
|
|
|
|
if (s_XOpenDisplay && s_XDefaultRootWindow && s_XScreenSaverAllocInfo && s_XScreenSaverQueryInfo) {
|
|
s_display = s_XOpenDisplay(nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!s_display || !s_XScreenSaverAllocInfo || !s_XScreenSaverQueryInfo || !s_XDefaultRootWindow) {
|
|
return 0;
|
|
}
|
|
|
|
XScreenSaverInfo* info = s_XScreenSaverAllocInfo();
|
|
if (!info) return 0;
|
|
|
|
int idleSec = 0;
|
|
unsigned long root = s_XDefaultRootWindow(s_display);
|
|
if (s_XScreenSaverQueryInfo(s_display, root, info)) {
|
|
idleSec = (int)(info->idle / 1000); // idle is in milliseconds
|
|
}
|
|
if (s_XFree) s_XFree(info);
|
|
return idleSec;
|
|
#endif
|
|
}
|
|
|
|
// ============================================================================
|
|
// GPU utilization detection
|
|
// ============================================================================
|
|
|
|
std::string Platform::runHiddenCapture(const std::string& cmdLine, bool mergeStderr, int* exitCode)
|
|
{
|
|
if (exitCode) *exitCode = -1;
|
|
#ifdef _WIN32
|
|
SECURITY_ATTRIBUTES sa;
|
|
ZeroMemory(&sa, sizeof(sa));
|
|
sa.nLength = sizeof(sa);
|
|
sa.bInheritHandle = TRUE;
|
|
HANDLE hRead = NULL, hWrite = NULL;
|
|
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return {};
|
|
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); // parent's read end stays private
|
|
|
|
HANDLE hNul = INVALID_HANDLE_VALUE;
|
|
if (!mergeStderr) {
|
|
hNul = CreateFileA("NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
|
|
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
|
}
|
|
|
|
STARTUPINFOA si;
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
|
si.wShowWindow = SW_HIDE;
|
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
si.hStdOutput = hWrite;
|
|
si.hStdError = mergeStderr ? hWrite : hNul;
|
|
|
|
PROCESS_INFORMATION pi;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
std::string cl = cmdLine; // CreateProcessA may modify lpCommandLine → needs a mutable buffer
|
|
std::string out;
|
|
if (CreateProcessA(NULL, cl.empty() ? NULL : &cl[0], NULL, NULL, TRUE,
|
|
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
|
CloseHandle(hWrite); hWrite = NULL; // close our copy so ReadFile hits EOF when the child exits
|
|
if (hNul != INVALID_HANDLE_VALUE) { CloseHandle(hNul); hNul = INVALID_HANDLE_VALUE; }
|
|
char buf[4096];
|
|
DWORD n = 0;
|
|
while (ReadFile(hRead, buf, sizeof(buf), &n, NULL) && n > 0) out.append(buf, n);
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
if (exitCode) {
|
|
DWORD code = 0;
|
|
if (GetExitCodeProcess(pi.hProcess, &code)) *exitCode = static_cast<int>(code);
|
|
}
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
}
|
|
if (hWrite != NULL) CloseHandle(hWrite);
|
|
if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul);
|
|
CloseHandle(hRead);
|
|
return out;
|
|
#else
|
|
const std::string full = cmdLine + (mergeStderr ? " 2>&1" : " 2>/dev/null");
|
|
std::string out;
|
|
FILE* f = popen(full.c_str(), "r");
|
|
if (!f) return out;
|
|
char buf[512];
|
|
size_t n;
|
|
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) out.append(buf, n);
|
|
const int st = pclose(f);
|
|
if (exitCode) *exitCode = st; // raw status (matches prior pclose-based rc checks)
|
|
return out;
|
|
#endif
|
|
}
|
|
|
|
int Platform::getGpuUtilization()
|
|
{
|
|
#ifdef _WIN32
|
|
// Windows: read GPU utilization via SetupAPI / D3DKMT
|
|
// Not all GPUs expose this; return -1 if unavailable.
|
|
// Use a popen fallback: nvidia-smi for NVIDIA, or return -1.
|
|
static bool s_tried_nvidia = false;
|
|
static bool s_has_nvidia = false;
|
|
if (!s_tried_nvidia) {
|
|
s_tried_nvidia = true;
|
|
// Windowless (runHiddenCapture) so GPU-aware idle detection never flashes a cmd.exe console.
|
|
const std::string w = runHiddenCapture("where nvidia-smi");
|
|
s_has_nvidia = (w.find_first_not_of(" \t\r\n") != std::string::npos);
|
|
}
|
|
if (s_has_nvidia) {
|
|
const std::string o = runHiddenCapture(
|
|
"nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits");
|
|
if (!o.empty()) {
|
|
int util = atoi(o.c_str());
|
|
if (util < 0 || util > 100) util = -1;
|
|
return util;
|
|
}
|
|
}
|
|
return -1;
|
|
#elif defined(__APPLE__)
|
|
return -1;
|
|
#else
|
|
// Linux: try multiple GPU sysfs paths
|
|
|
|
// AMD: /sys/class/drm/card*/device/gpu_busy_percent
|
|
{
|
|
// Try card0 through card3
|
|
char path[128];
|
|
for (int i = 0; i < 4; i++) {
|
|
snprintf(path, sizeof(path), "/sys/class/drm/card%d/device/gpu_busy_percent", i);
|
|
std::ifstream ifs(path);
|
|
if (ifs.is_open()) {
|
|
int val = -1;
|
|
ifs >> val;
|
|
if (val >= 0 && val <= 100)
|
|
return val;
|
|
}
|
|
}
|
|
}
|
|
|
|
// NVIDIA: nvidia-smi (binary may exist even without sysfs)
|
|
{
|
|
static bool s_tried = false;
|
|
static bool s_has_nvidia_smi = false;
|
|
if (!s_tried) {
|
|
s_tried = true;
|
|
FILE* f = popen("which nvidia-smi 2>/dev/null", "r");
|
|
if (f) {
|
|
char buf[256];
|
|
s_has_nvidia_smi = (fgets(buf, sizeof(buf), f) != nullptr);
|
|
pclose(f);
|
|
}
|
|
}
|
|
if (s_has_nvidia_smi) {
|
|
FILE* f = popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "r");
|
|
if (f) {
|
|
char buf[64];
|
|
int util = -1;
|
|
if (fgets(buf, sizeof(buf), f)) {
|
|
util = atoi(buf);
|
|
if (util < 0 || util > 100) util = -1;
|
|
}
|
|
pclose(f);
|
|
return util;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Intel: compare current vs max freq as a rough proxy
|
|
{
|
|
std::ifstream curF("/sys/class/drm/card0/gt_cur_freq_mhz");
|
|
std::ifstream maxF("/sys/class/drm/card0/gt_max_freq_mhz");
|
|
if (curF.is_open() && maxF.is_open()) {
|
|
int cur = 0, mx = 0;
|
|
curF >> cur;
|
|
maxF >> mx;
|
|
if (mx > 0)
|
|
return std::min(100, (cur * 100) / mx);
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|