// // Fulcrum - A fast & nimble SPV Server for Bitcoin Cash // Copyright (C) 2019-2026 Calin A. Culianu // // 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 // . // #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 # include # include # include #elif defined(Q_OS_LINUX) # include # include # include # include # include # include # include #elif defined(Q_OS_WINDOWS) # define WIN32_LEAN_AND_MEAN 1 # include # include # include // for _write(), _read(), _pipe(), _close() # include // for O_BINARY, O_TEXT # include // for errno #endif #if defined(Q_OS_UNIX) # include // for write(), read(), pipe(), close() # if __has_include() && __has_include() // POSIX includes for setrlimit/getrlimit # include // for setrlimit related stuff # include // for setrlimit related stuff # define HAS_SETRLIMIT # endif #endif #include #include #include #include // for std::byte, offsetof() #include // for strerror #include #include #include #include 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(now - t0).count(); } qint64 getTimeNS() noexcept { const auto now = std::chrono::high_resolution_clock::now(); return std::chrono::duration_cast(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 & lambda, int timeout_ms) { try { LambdaOnObject(obj, lambda, timeout_ms); return true; } catch (const Exception &) {} return false; } #if defined(Q_OS_DARWIN) unsigned getNVirtualProcessors() { static std::atomic 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 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 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(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(size_t(length) + align); uintptr_t ptrval = reinterpret_cast(buffer.get()); if (const auto rem = ptrval % align; rem) ptrval += align - rem; // ensure alignment PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX info = reinterpret_cast(reinterpret_cast(ptrval)); res = GetLogicalProcessorInformationEx(RelationProcessorCore, info, &length); if (!res) return getNVirtualProcessors(); // fallback unsigned nPhysProc = 0; DWORD offset = 0; const std::byte *buf = reinterpret_cast(info); while (offset < length) { const std::byte *punaligned = buf + offset + offsetof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, Size); decltype(std::declval().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(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 IntType get() const { return static_cast(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(), 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())); } 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 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(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 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(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 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 NL{"\r\n"}; #elif defined(Q_OS_UNIX) auto writeFD = ::write; auto readFD = ::read; auto closeFD = ::close; inline constexpr std::array 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 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> Sem::acquire() noexcept { std::optional> 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> Sem::release() noexcept { std::optional> 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> Sem::acquire() noexcept { std::mutex dummy; // hack, but works std::unique_lock l(dummy); p.cond.wait(l); return std::nullopt; } std::optional> 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 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 :"; 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{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 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 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(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