Fulcrum/src/Util.cpp
Calin Culianu eba8431a0b
API nit: Use Util::AsyncOnObject everywhere instead of QTimer::singleShot
This is just to make the API easier to follow. Also cleaned up
AsyncOnObject to not default to caarse timer unless the interval is
>2000 msec.

Also we use QMetaObject::invokeMethod() directly by default if the
AsyncOnObjectCall() has when_ms <= 0 (default).

This should have no real observable change on app behavior other than
being a code cleanup & nit.
2026-08-03 23:45:42 -05:00

888 lines
33 KiB
C++

//
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
// Copyright (C) 2019-2026 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt). If not, see
// <https://www.gnu.org/licenses/>.
//
#include "App.h"
#include "CityHash.h"
#include "Json/Json.h"
#include "Logger.h"
#include "Util.h"
#include "bitcoin/hash.h"
// below headers are for getN*Processors, etc.
#if defined(Q_OS_DARWIN)
# include <sys/types.h>
# include <sys/sysctl.h>
# include <mach/mach.h>
# include <mach/mach_time.h>
#elif defined(Q_OS_LINUX)
# include <array>
# include <fstream>
# include <locale>
# include <sstream>
# include <strings.h>
# include <time.h>
# include <unistd.h>
#elif defined(Q_OS_WINDOWS)
# define WIN32_LEAN_AND_MEAN 1
# include <windows.h>
# include <psapi.h>
# include <io.h> // for _write(), _read(), _pipe(), _close()
# include <fcntl.h> // for O_BINARY, O_TEXT
# include <errno.h> // for errno
#endif
#if defined(Q_OS_UNIX)
# include <unistd.h> // for write(), read(), pipe(), close()
# if __has_include(<sys/time.h>) && __has_include(<sys/resource.h>) // POSIX includes for setrlimit/getrlimit
# include <sys/time.h> // for setrlimit related stuff
# include <sys/resource.h> // for setrlimit related stuff
# define HAS_SETRLIMIT
# endif
#endif
#include <QRegularExpression>
#include <QHostAddress>
#include <cctype>
#include <cstddef> // for std::byte, offsetof()
#include <cstring> // for strerror
#include <iostream>
#include <mutex>
#include <thread>
#include <utility>
namespace Util {
QString basename(const QString &s) {
const QRegularExpression re("[\\/]");
auto toks = s.split(re);
return toks.last();
}
#if defined(Q_OS_LINUX)
static int64_t getAbsTimeNS() noexcept
{
struct timespec ts;
// Note: CLOCK_MONOTONIC does *not* include the time spent suspended. If we want that, then we can Use
// CLOCK_BOOTTIME here for that.
if (clock_gettime(CLOCK_MONOTONIC, &ts)) {
ts = {0, 0};
// We can't do a Warning() or Error() here because that would cause infinite recursion.
// This is an unlikely and also pretty fatal situation, though, so we must warn.
// Also we will use these noexcept functions here to preserve our noexcept-ness
using namespace AsyncSignalSafe;
writeStdErr(SBuf("Fatal: clock_gettime for CLOCK_MONOTONIC returned error status: ", std::strerror(errno)));
}
return int64_t(ts.tv_sec * 1000000000LL) + int64_t(ts.tv_nsec);
}
static int64_t absT0 = getAbsTimeNS();
qint64 getTimeNS() noexcept {
const auto now = getAbsTimeNS();
return now - absT0;
}
qint64 getTime() noexcept {
return getTimeNS()/1000000LL;
}
bool isClockSteady() noexcept { return true; }
#elif defined(Q_OS_WINDOWS)
// Windows lacks a decent high resolution clock source on some C++ implementations (such as MinGW). So we
// query the OS's QPC mechanism, which, on Windows 7+ is very fast to query and guaranteed to be accurate, and also
// monotocic ("steady").
static int64_t getAbsTimeNS() noexcept
{
static __int64 freq = 0;
__int64 ct, factor;
if (!freq) {
QueryPerformanceFrequency((LARGE_INTEGER *)&freq);
}
QueryPerformanceCounter((LARGE_INTEGER *)&ct); // reads the current time (in system units)
factor = 1000000000LL/freq;
if (factor <= 0) factor = 1;
return int64_t(ct * factor);
}
static qint64 absT0 = qint64(getAbsTimeNS()); // initializes static data inside getAbsTimeNS() once at startup in main thread.
qint64 getTimeNS() noexcept {
const auto now = getAbsTimeNS();
return now - absT0;
}
qint64 getTime() noexcept {
return getTimeNS()/1000000LL;
}
bool isClockSteady() noexcept { return true; }
#else
// MacOS or generic platform (on MacOS with clang this happens to be very accurate)
static const auto t0 = std::chrono::high_resolution_clock::now();
qint64 getTime() noexcept {
const auto now = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(now - t0).count();
}
qint64 getTimeNS() noexcept {
const auto now = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::nanoseconds>(now - t0).count();
}
bool isClockSteady() noexcept {
return std::chrono::high_resolution_clock::is_steady;
}
#endif
qint64 getTimeMicros() noexcept {
return getTimeNS()/1000LL;
}
double getTimeSecs() noexcept {
return double(getTime()) / 1e3;
}
bool VoidFuncOnObjectNoThrow(const QObject *obj, const std::function<void()> & lambda, int timeout_ms)
{
try {
LambdaOnObject<void>(obj, lambda, timeout_ms);
return true;
} catch (const Exception &) {}
return false;
}
#if defined(Q_OS_DARWIN)
unsigned getNVirtualProcessors()
{
static std::atomic<unsigned> nVProcs = 0;
if (!nVProcs) {
int a = 0;
size_t b = sizeof(a);
if (0 == sysctlbyname("hw.ncpu",&a, &b, nullptr, 0)) {
nVProcs = unsigned(a); // this returns virtual CPUs which isn't always what we want..
}
}
return nVProcs.load() ? nVProcs.load() : 1;
}
unsigned getNPhysicalProcessors()
{
static std::atomic<unsigned> nProcs = 0;
if (!nProcs) {
int a = 0;
size_t b = sizeof(a);
if (0 == sysctlbyname("hw.physicalcpu",&a,&b,nullptr,0)) {
nProcs = unsigned(a);
}
}
return nProcs.load() ? nProcs.load() : 1;
}
#elif defined(Q_OS_LINUX)
unsigned getNVirtualProcessors() { return std::thread::hardware_concurrency(); }
unsigned getNPhysicalProcessors() {
static std::atomic<unsigned> nProcs = 0;
if (!nProcs) {
nProcs = unsigned(sysconf(_SC_NPROCESSORS_ONLN));
}
return nProcs.load() ? nProcs.load() : 1;
}
#elif defined(Q_OS_WINDOWS)
unsigned getNVirtualProcessors()
{
static std::atomic_uint nProcs = 0;
if (auto val = nProcs.load()) return val;
SYSTEM_INFO system_info = {};
GetSystemInfo(&system_info);
const auto nVirtProc = static_cast<unsigned>(system_info.dwNumberOfProcessors);
return nProcs = std::max(nVirtProc, 1u);
}
unsigned getNPhysicalProcessors()
{
static std::atomic_uint nProcs = 0;
if (auto val = nProcs.load()) return val;
// from: https://stackoverflow.com/questions/150355/programmatically-find-the-number-of-cores-on-a-machine
DWORD length = 0;
auto res = GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &length);
if (res || GetLastError() != ERROR_INSUFFICIENT_BUFFER)
return getNVirtualProcessors(); // fallback
const std::size_t align = alignof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
auto buffer = std::make_unique_for_overwrite<std::byte[]>(size_t(length) + align);
uintptr_t ptrval = reinterpret_cast<uintptr_t>(buffer.get());
if (const auto rem = ptrval % align; rem) ptrval += align - rem; // ensure alignment
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX info =
reinterpret_cast<PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>(reinterpret_cast<std::byte *>(ptrval));
res = GetLogicalProcessorInformationEx(RelationProcessorCore, info, &length);
if (!res)
return getNVirtualProcessors(); // fallback
unsigned nPhysProc = 0;
DWORD offset = 0;
const std::byte *buf = reinterpret_cast<std::byte *>(info);
while (offset < length) {
const std::byte *punaligned = buf + offset + offsetof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, Size);
decltype(std::declval<SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>().Size) len{};
std::memcpy(&len, punaligned, sizeof(len));
if (!len) break; // prevent infinite loops
offset += len;
++nPhysProc;
}
return nProcs = std::max(nPhysProc, 1u);
}
#else
unsigned getNVirtualProcessors() { return std::thread::hardware_concurrency(); }
unsigned getNPhysicalProcessors() { return std::thread::hardware_concurrency(); }
#endif
QByteArray ParseHexFast(const QByteArray &hex, bool checkDigits)
{
const int size = hex.size();
QByteArray ret(size / 2, Qt::Initialization::Uninitialized);
if (size % 2) [[unlikely]] {
// bad / not hex because not even number of chars.
ret.clear();
return ret;
}
const char *d = hex.constData(), * const dend = d + size;
uint8_t c1, c2;
for (char *out = ret.data(); d < dend; d += 2, ++out) {
constexpr uint8_t offset_A = 'A' - 0xa,
offset_a = 'a' - 0xa,
offset_0 = '0';
// slightly unrolled loop, does 2 chars at a time
c1 = uint8_t(d[0]);
c2 = uint8_t(d[1]);
// c1
if (c1 <= '9') // this is the most likely for any random digit, so we check this first
c1 -= offset_0;
else if (c1 >= 'a') // next, we anticipate lcase, so we do this check first
c1 -= offset_a;
else // c1 >= 'A'
c1 -= offset_A;
// c2
if (c2 <= '9') // this is the most likely for any random digit, so we check this first
c2 -= offset_0;
else if (c2 >= 'a') // next, we anticipate lcase, so we do this check first
c2 -= offset_a;
else // c2 >= 'A'
c2 -= offset_A;
// The below is slowish... we can just accept bad hex data as 'corrupt' ...
// checkDigit = false allows us to skip this check, making this function >5x faster!
if (checkDigits && (c1 > 0xf || c2 > 0xf)) [[unlikely]] { // ensure data was actually in range
ret.clear();
break;
}
*out = char((c1 << 4) | c2);
}
return ret;
}
QByteArray ToHexFast(const QByteArray &ba)
{
QByteArray ret(ba.size()*2, Qt::Initialization::Uninitialized);
if (!ToHexFastInPlace(ba, ret.data(), size_t(ret.size())))
ret.clear();
return ret;
}
bool ToHexFastInPlace(const QByteArray &ba, char *out, size_t bufsz)
{
static const char hexmap[513] =
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
"303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"
"606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f"
"909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
"c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef"
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff";
const int size = ba.size();
if (bufsz < size_t(size*2))
return false;
const uint8_t *cur = reinterpret_cast<const uint8_t *>(ba.constData()), * const end = cur + size;
for (const char *nibbles; cur < end; ++cur, out += 2) {
nibbles = &hexmap[*cur * 2];
out[0] = nibbles[0];
out[1] = nibbles[1];
}
return true;
}
bool IsValidHex(const QByteArray &s)
{
return s.size() % 2 == 0 && std::all_of(s.begin(), s.end(), [](char const c) { return std::isxdigit(c); });
}
namespace {
/// Stores a hash seed that we will use for our hash tables.
/// There really should only be one of these globally.
class HashSeed {
uint64_t seed;
public:
/// seeds 'seed' from QRandomGenerator
HashSeed() {
auto gen = QRandomGenerator::global();
if (!gen) {
Warning() << "App-global random number generator is null! Seeding hash seed with current time. FIXME!";
seed = uint64_t(getTimeNS());
} else {
seed = uint64_t(gen->generate64());
}
}
template <std::integral IntType>
IntType get() const { return static_cast<IntType>(seed); }
};
/// app-global hash seed -- initialized before we enter main()
const HashSeed hashSeed;
} // namespace (anonymous)
uint32_t hashData32(const ByteView &bv) noexcept
{
// bitcoin::MurmurHash3 is not marked noexcept but it will never throw -- it does not allocate and
// just uses basic arithmetic ops on the data in-place.
return bitcoin::MurmurHash3(hashSeed.get<uint32_t>(), bv.ucharData(), bv.size());
}
uint64_t hashData64(const ByteView &bv) noexcept
{
// CityHash::CityHash64WithSeed is not marked noexcept but it will never throw -- it does not allocate and
// just uses basic arithmetic ops on the data in-place.
return uint64_t(CityHash::CityHash64WithSeed(bv.charData(), bv.size(), hashSeed.get<CityHash::uint64>()));
}
MemUsage getProcessMemoryUsage()
{
#if defined(Q_OS_WINDOWS)
PROCESS_MEMORY_COUNTERS_EX pmc;
GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc));
return { std::size_t{pmc.WorkingSetSize}, std::size_t{pmc.PrivateUsage} };
#elif defined(Q_OS_LINUX)
MemUsage ret;
std::ifstream file("/proc/self/status", std::ios_base::in);
if (!file) return ret;
file.imbue(std::locale::classic());
std::array<char, 256> buf;
buf[0] = 0;
// sizes are in kB
while (file.getline(buf.data(), buf.size()) && (ret.phys == 0 || ret.virt == 0)) {
if (strncasecmp(buf.data(), "VmSize:", 7) == 0) {
std::istringstream is(buf.data() + 7);
is.imbue(std::locale::classic());
is >> std::skipws >> ret.virt;
ret.virt *= std::size_t(1024);
} else if (strncasecmp(buf.data(), "VmRSS:", 6) == 0) {
std::istringstream is(buf.data() + 6);
is.imbue(std::locale::classic());
is >> std::skipws >> ret.phys;
ret.phys *= std::size_t(1024);
}
}
return ret;
#elif defined(Q_OS_DARWIN)
struct task_basic_info t_info;
mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;
if (KERN_SUCCESS != task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count)) {
return {};
}
return { std::size_t{t_info.resident_size}, std::size_t{t_info.virtual_size} };
#else
return {};
#endif
}
uint64_t getAvailablePhysicalRAM()
{
uint64_t ret = 2048u * 1024u * 1024u; // just return 2GB, even if it's wrong, for unknown platforms
#if defined(Q_OS_WINDOWS)
MEMORYSTATUSEX statex;
statex.dwLength = sizeof(statex);
GlobalMemoryStatusEx(&statex);
ret = static_cast<uint64_t>(statex.ullAvailPhys);
#elif defined(Q_OS_DARWIN)
// can't easily query memory on darwin, just take 1/2 of physical memory
char buf[8];
size_t bufsz = 8;
static_assert(sizeof(uint64_t) == 8);
if ( 0 == ::sysctlbyname("hw.memsize", buf, &bufsz, nullptr, 0) ) {
switch (bufsz) {
case 4: { uint32_t tmp; std::memcpy(&tmp, buf, 4); ret = tmp; ret /= uint64_t(2); break; }
case 8: { std::memcpy(&ret, buf, 8); ret /= uint64_t(2); break; }
default: qWarning() << "Failed to query physical RAM, kernel returned unexpected bufsize: " << bufsz;
}
}
#elif defined(Q_OS_LINUX)
std::ifstream file("/proc/meminfo", std::ios_base::in);
if (!file) return ret;
file.imbue(std::locale::classic());
std::array<char, 256> buf;
buf[0] = 0;
// sizes are in KiB
while (file.getline(buf.data(), buf.size())) {
if (strncasecmp(buf.data(), "MemAvailable:", 13) == 0) {
std::istringstream is(buf.data() + 13);
is.imbue(std::locale::classic());
uint64_t tmp = 0;
is >> std::skipws >> tmp;
tmp *= uint64_t(1024);
if (tmp > 0) ret = tmp;
break;
}
}
#endif
return ret;
}
uint64_t getTotalPhysicalRAM()
{
uint64_t ret = 2048u * 1024u * 1024u; // just return 2GB, even if it's wrong, for unknown platforms
#if defined(Q_OS_WINDOWS)
MEMORYSTATUSEX statex;
statex.dwLength = sizeof(statex);
GlobalMemoryStatusEx(&statex);
ret = static_cast<uint64_t>(statex.ullTotalPhys);
#elif defined(Q_OS_DARWIN)
char buf[8];
size_t bufsz = 8;
static_assert(sizeof(uint64_t) == 8);
if ( 0 == ::sysctlbyname("hw.memsize", buf, &bufsz, nullptr, 0) ) {
switch (bufsz) {
case 4: { uint32_t tmp; std::memcpy(&tmp, buf, 4); ret = tmp; break; }
case 8: { std::memcpy(&ret, buf, 8); break; }
default: qWarning() << "Failed to query physical RAM, kernel returned unexpected bufsize: " << bufsz;
}
}
#elif defined(Q_OS_LINUX)
std::ifstream file("/proc/meminfo", std::ios_base::in);
if (!file) return ret;
file.imbue(std::locale::classic());
std::array<char, 256> buf;
buf[0] = 0;
// sizes are in KiB
while (file.getline(buf.data(), buf.size())) {
if (strncasecmp(buf.data(), "MemTotal:", 9) == 0) {
std::istringstream is(buf.data() + 9);
is.imbue(std::locale::classic());
uint64_t tmp = 0;
is >> std::skipws >> tmp;
tmp *= uint64_t(1024);
if (tmp > 0) ret = tmp;
break;
}
}
#endif
return ret;
}
namespace AsyncSignalSafe {
namespace {
#if defined(Q_OS_WIN)
auto writeFD = ::_write; // Windows API docs say to use this function, since write() is deprecated
auto readFD = ::_read; // Windows API docs say to use this function, since read() is deprecated
auto closeFD = ::_close; // Windows API docs say to use this function, since close() is deprecated
inline constexpr std::array<char, 3> NL{"\r\n"};
#elif defined(Q_OS_UNIX)
auto writeFD = ::write;
auto readFD = ::read;
auto closeFD = ::close;
inline constexpr std::array<char, 2> NL{"\n"};
#else
// no-op on unknown platform (this platform would use the cond variable and doesn't need read/close/pipe)
auto writeFD = [](int, const void *, size_t n) { return int(n); };
inline constexpr std::array<char, 1> NL{0};
#endif
}
void writeStdErr(const std::string_view &sv, bool wrnl) noexcept {
constexpr int stderr_fd = 2; /* this is the case on all platforms */
writeFD(stderr_fd, sv.data(), sv.length());
if (wrnl && NL.size() > 1)
writeFD(stderr_fd, NL.data(), NL.size()-1);
}
#if defined(Q_OS_WIN) || defined(Q_OS_UNIX)
Sem::Pipe::Pipe() {
const int res =
# ifdef Q_OS_WIN
::_pipe(fds, 32 /* bufsize */, O_BINARY);
# else
::pipe(fds);
# endif
if (res != 0)
throw InternalError(QString("Failed to create a Cond::Pipe: (%1) %2").arg(errno).arg(std::strerror(errno)));
}
Sem::Pipe::~Pipe() { closeFD(fds[0]), closeFD(fds[1]); }
std::optional<SBuf<>> Sem::acquire() noexcept {
std::optional<SBuf<>> ret;
char c;
if (const int res = readFD(p.fds[0], &c, 1); res != 1)
ret.emplace("Sem::acquire: readFD returned ", res);
return ret;
}
std::optional<SBuf<>> Sem::release() noexcept {
std::optional<SBuf<>> ret;
const char c = 0;
if (const int res = writeFD(p.fds[1], &c, 1); res != 1)
ret.emplace("Sem::release: writeFD returned ", res);
return ret;
}
#else
// fallback to emulated -- use std C++ condition variable which is not technically
// guaranteed async signal safe, but for all pratical purposes it's safe enough as a fallback.
std::optional<SBuf<>> Sem::acquire() noexcept {
std::mutex dummy; // hack, but works
std::unique_lock l(dummy);
p.cond.wait(l);
return std::nullopt;
}
std::optional<SBuf<>> Sem::release() noexcept {
p.cond.notify_one();
return std::nullopt;
}
#endif // defined(Q_OS_WIN) || defined(Q_OS_UNIX)
} // end namespace AsyncSignalSafe
MaxOpenFilesResult raiseMaxOpenFilesToHardLimit()
{
#ifdef HAS_SETRLIMIT
MaxOpenFilesResult ret;
struct rlimit rl;
auto get = [&rl, &ret] {
if (getrlimit(RLIMIT_NOFILE, &rl)) {
ret.status = ret.Error;
ret.errMsg = QString("getrlimit: ") + std::strerror(errno);
return false;
}
return true;
};
// first get the current limits
if (!get())
return ret;
// paranoia
if (long(rl.rlim_cur) < 0 || long(rl.rlim_max) < 0) {
ret.status = ret.Error;
ret.errMsg = "getrlimit reports limits are negative";
}
// more paranoia
if (rl.rlim_cur > rl.rlim_max) {
ret.status = ret.Error;
ret.errMsg = "soft limit > hard limit (this shouldn't happen)";
}
// save value
ret.oldLimit = long(rl.rlim_cur);
if (rl.rlim_cur != rl.rlim_max) { // if not at hard limit, raise it
// set to max
rl.rlim_cur = rl.rlim_max;
if (setrlimit(RLIMIT_NOFILE, &rl)) {
ret.status = ret.Error;
ret.errMsg = QString("setrlimit: ") + std::strerror(errno);
return ret;
}
}
// get the new limits again
if (!get())
return ret;
// save value, indicate success
ret.newLimit = long(rl.rlim_cur);
ret.status = ret.Ok;
return ret;
#else
// On Windows this call is not even needed -- our use of Qt uses the Win32 API directly which has a limit
// of 16.7 million for the handle tables.
return {MaxOpenFilesResult::NotRelevant};
#endif
}
QPair<QString, quint16> ParseHostPortPair(const QString &s, bool allowImplicitLoopback)
{
constexpr auto parsePort = [](const QString & portStr) -> quint16 {
bool ok;
quint16 port = portStr.toUShort(&ok);
if (!ok || port == 0)
throw BadArgs(QString("Bad port: %1").arg(portStr));
return port;
};
auto toks = s.split(":");
constexpr const char *msg1 = "Malformed host:port spec. Please specify a string of the form <host>:<port>";
if (const auto len = toks.length(); len < 2) {
if (allowImplicitLoopback && len == 1)
// this option allows bare port number with the implicit ipv4 127.0.0.1 -- try that (may throw if bad port number)
return QPair<QString, quint16>{QHostAddress(QHostAddress::LocalHost).toString(), parsePort(toks.front())};
throw BadArgs(msg1);
}
QString portStr = toks.last();
toks.removeLast(); // pop off port
QString hostStr = toks.join(':'); // rejoin on ':' in case it was IPv6 which is full of colons
if (hostStr.isEmpty())
throw BadArgs(msg1);
if (toks.length() > 1 && hostStr.length() > 2 && hostStr.front() == QChar('[') && hostStr.back() == QChar(']'))
hostStr = hostStr.mid(1, hostStr.length()-2); // pop off leading and trailing [] for ipv6, if present
return {hostStr, parsePort(portStr)};
}
std::pair<double, QString> ScaleBytes(uint64_t bytes, std::string_view baseByteUnitLabel)
{
double dataSize = bytes;
if (dataSize > 1e3) { baseByteUnitLabel = "KB"; dataSize /= 1e3; }
if (dataSize > 1e3) { baseByteUnitLabel = "MB"; dataSize /= 1e3; }
if (dataSize > 1e3) { baseByteUnitLabel = "GB"; dataSize /= 1e3; }
if (dataSize > 1e3) { baseByteUnitLabel = "TB"; dataSize /= 1e3; }
if (dataSize > 1e3) { baseByteUnitLabel = "PB"; dataSize /= 1e3; }
if (dataSize > 1e3) { baseByteUnitLabel = "EB"; dataSize /= 1e3; }
return {dataSize, QString::fromUtf8(baseByteUnitLabel.data(), baseByteUnitLabel.size())};
}
QString RenderHostPortPair(const QHostAddress &addr, quint16 port)
{
QString ret = addr.toString();
if (!ret.isEmpty()) {
if (addr.protocol() == QAbstractSocket::IPv6Protocol && ret.front() != QChar('[') && ret.back() != QChar(']')) {
ret.insert(0, QChar('['));
ret.append(QChar(']'));
}
ret.append(QStringLiteral(":%1").arg(port));
}
return ret;
}
namespace ThreadName {
namespace {
QString & GetMutable() {
static thread_local QString threadName;
return threadName;
}
} // namespace
const QString & Get() { return GetMutable(); }
void Set(const QString &name) { GetMutable() = name; }
} // namespace ThreadName
ThreadInterrupt::operator bool() const { return flag.load(std::memory_order_acquire); }
void ThreadInterrupt::reset() { flag.store(false, std::memory_order_release); }
void ThreadInterrupt::operator()()
{
{
std::unique_lock l(mut);
flag.store(true, std::memory_order_release);
}
cond.notify_all();
}
bool ThreadInterrupt::wait(std::optional<std::chrono::milliseconds> rel_time) const
{
const auto predicate = [this] { return this->operator bool(); };
std::unique_lock lock(mut);
if (predicate()) {
return true;
} else if (rel_time) {
return cond.wait_for(lock, *rel_time, predicate);
} else {
cond.wait(lock, predicate);
return predicate(); // should always be true here
}
}
size_t GetWindowsObjectCount()
{
#if defined(Q_OS_WINDOWS)
return GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) + GetGuiResources(GetCurrentProcess(), GR_USEROBJECTS);
#else
return 0;
#endif
}
} // end namespace Util
Log::Log() {}
Log::Log(Color c)
{
setColor(c);
}
Log::Log(const char *fmt...)
: s()
{
va_list ap;
va_start(ap,fmt);
str = QString::vasprintf(fmt,ap);
va_end(ap);
s.setString(&str, QIODevice::WriteOnly|QIODevice::Append);
}
Log::~Log()
{
if (doprt) {
App *ourApp = app();
if (ourApp && !ourApp->options) [[unlikely]]
ourApp = nullptr; // spurious Qt message -- ourApp not yet fully constructed.
using LTS = Options::LogTimestampMode;
const LTS ltsMode = !ourApp ? Options::defaultLogTimeStampMode : ourApp->options->logTimestampMode;
s.flush(); // does nothing probably..
// [timestamp]
// Note: we always want to log the timestamp, even in syslog mode.
// This is because if logging from a thread, log lines may be out-of-order.
// The timestamp is the only record of the actual order in which things
// occurred. Currently the timestamp is to 4 decimal places (hundreds of micros) in Uptime mode only.
// We do offer LogTimestampMode::None for users really wishing to suppress timestamp logging.
QString tsStr;
switch (ltsMode) {
case LTS::None:
break;
case LTS::Uptime: {
const auto unow = Util::getTimeNS()/1000LL;
tsStr = QString::asprintf("[%lld.%04d] ", unow/1000000LL, int((unow/100LL)%10000));
}
break;
case LTS::UTC:
case LTS::Local: {
const auto now = ltsMode == LTS::UTC ? QDateTime::currentDateTimeUtc() : QDateTime::currentDateTime();
tsStr = now.toString(u"[yyyy-MM-dd hh:mm:ss.zzz] ");
}
break;
}
// /[timestamp]
QString thrdStr;
if (QThread *th = QThread::currentThread(); th && ourApp && th != ourApp->thread()) {
QString thrdName = Util::ThreadName::Get(); /* We must use an internal name. THIS IS UNSAFE --> th->objectName(); */
if (thrdName.trimmed().isEmpty()) thrdName = QString::asprintf("%p", reinterpret_cast<void *>(QThread::currentThreadId()));
thrdStr = QStringLiteral("<%1> ").arg(thrdName);
}
Logger *logger = ourApp ? ourApp->logger() : nullptr;
QString theString = tsStr + thrdStr + (logger && logger->isaTTY() ? colorize(str, color) : str);
if (logger) {
emit logger->log(level, theString);
} else {
// logger not active yet; just print to console for now..
static std::mutex mut;
{
const auto bytes = theString.toUtf8();
std::unique_lock g(mut);
std::fwrite(bytes.constData(), 1, bytes.size(), stderr);
std::fwrite("\n", 1, 1, stderr);
std::fflush(stderr);
}
// Fatal should signal a quit even here
if (level == Logger::Level::Fatal && qApp) {
Util::AsyncOnObject(qApp, []{ qApp->quit(); });
}
}
}
}
/* static */
QString Log::colorString(Color c) {
const char *suffix = "[0m"; // normal
switch(c) {
case Black: suffix = "[30m"; break;
case Red: suffix = "[31m"; break;
case Green: suffix = "[32m"; break;
case Yellow: suffix = "[33m"; break;
case Blue: suffix = "[34m"; break;
case Magenta: suffix = "[35m"; break;
case Cyan: suffix = "[36m"; break;
case White: suffix = "[37m"; break;
case BrightBlack: suffix = "[30;1m"; break;
case BrightRed: suffix = "[31;1m"; break;
case BrightGreen: suffix = "[32;1m"; break;
case BrightYellow: suffix = "[33;1m"; break;
case BrightBlue: suffix = "[34;1m"; break;
case BrightMagenta: suffix = "[35;1m"; break;
case BrightCyan: suffix = "[36;1m"; break;
case BrightWhite: suffix = "[37;1m"; break;
default:
// will just use normal
break;
}
static const char prefix[2] = { 033, 0 }; // esc 033 in octal
return QString::asprintf("%s%s", prefix, suffix);
}
QString Log::colorize(const QString &str, Color c) {
QString colorStr = useColor && c != Normal ? colorString(c) : "";
QString normalStr = useColor && c != Normal ? colorString(Normal) : "";
return colorStr + str + normalStr;
}
template <> Log & Log::operator<<(const Color &c) { setColor(c); return *this; }
Debug::~Debug()
{
level = Logger::Level::Debug;
doprt = isEnabled();
if (!doprt) return;
if (!colorOverridden) color = Cyan;
str = QStringLiteral("(Debug) ") + str;
}
bool Debug::forceEnable = false;
bool Debug::isEnabled() {
auto ourApp = app();
return forceEnable || !ourApp || !ourApp->options || ourApp->options->verboseDebug;
}
Trace::~Trace()
{
level = Logger::Level::Debug;
doprt = isEnabled();
if (!doprt) return;
if (!colorOverridden) color = Green;
str = QStringLiteral("(Trace) ") + str;
}
bool Trace::forceEnable = false;
bool Trace::isEnabled() {
auto ourApp = app();
return forceEnable
|| (ourApp && ourApp->options && ourApp->options->verboseTrace && ourApp->options->verboseDebug); // both trace and debug must be on
}
Error::~Error()
{
level = Logger::Level::Critical;
if (!colorOverridden) color = BrightRed;
}
Warning::~Warning()
{
level = Logger::Level::Warning;
if (!colorOverridden) color = Yellow;
}
Alert::~Alert()
{
level = Logger::Level::Alert;
if (!colorOverridden) color = BrightMagenta;
}
Fatal::~Fatal()
{
level = Logger::Level::Fatal;
str = QString("FATAL: ") + str;
if (!colorOverridden) color = BrightRed;
}
#ifdef ENABLE_TESTS
#endif