mirror of
https://github.com/cculianu/Fulcrum.git
synced 2026-08-13 12:33:27 +02:00
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.
888 lines
33 KiB
C++
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
|