mirror of
https://github.com/ClusterM/open-bamboo-networking.git
synced 2026-10-21 10:39:52 -05:00
369 lines
11 KiB
C++
369 lines
11 KiB
C++
#include "source_log.hpp"
|
|
|
|
#include "obn/lan_tls_env.hpp"
|
|
#include "obn/log.hpp"
|
|
#include "obn/os_compat.hpp"
|
|
|
|
#include <atomic>
|
|
#include <cctype>
|
|
#include <chrono>
|
|
#include <cstdarg>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <string>
|
|
|
|
#if defined(_WIN32)
|
|
# ifndef WIN32_LEAN_AND_MEAN
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# endif
|
|
# ifndef NOMINMAX
|
|
# define NOMINMAX
|
|
# endif
|
|
# include <windows.h>
|
|
#endif
|
|
|
|
namespace obn::source {
|
|
|
|
void noop_logger(void*, int, obn_tchar const*) {}
|
|
|
|
namespace {
|
|
|
|
// Allocate a heap copy of `buf` in the platform-native character width
|
|
// for the Logger callback. The caller -- gstbambusrc on Linux,
|
|
// wxMediaCtrl2 on Windows -- frees it via Bambu_FreeLogMsg, which in
|
|
// turn calls free(). On POSIX we hand back UTF-8 unchanged; on Windows
|
|
// we transcode UTF-8 -> UTF-16 once, since Studio's wide-string
|
|
// callback would otherwise see mojibake for any multi-byte input.
|
|
obn_tchar* strdup_for_logger(const char* buf)
|
|
{
|
|
#if defined(_WIN32)
|
|
int wlen = ::MultiByteToWideChar(CP_UTF8, 0, buf, -1, nullptr, 0);
|
|
if (wlen <= 0) {
|
|
// On a bogus UTF-8 sequence return an empty wide string rather
|
|
// than nullptr -- the Studio side dereferences without checking.
|
|
auto* empty = static_cast<wchar_t*>(std::malloc(sizeof(wchar_t)));
|
|
if (empty) empty[0] = L'\0';
|
|
return empty;
|
|
}
|
|
auto* w = static_cast<wchar_t*>(
|
|
std::malloc(static_cast<std::size_t>(wlen) * sizeof(wchar_t)));
|
|
if (!w) return nullptr;
|
|
::MultiByteToWideChar(CP_UTF8, 0, buf, -1, w, wlen);
|
|
return w;
|
|
#else
|
|
return ::strdup(buf);
|
|
#endif
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
LogLevel parse_log_level(const char* s, LogLevel fallback)
|
|
{
|
|
if (!s || !*s) return fallback;
|
|
auto eq = [&](const char* a) {
|
|
for (size_t i = 0;; ++i) {
|
|
char x = s[i];
|
|
char y = a[i];
|
|
if (x >= 'A' && x <= 'Z') x = static_cast<char>(x - 'A' + 'a');
|
|
if (x != y) return false;
|
|
if (!x) return true;
|
|
}
|
|
};
|
|
if (eq("trace")) return LL_TRACE;
|
|
if (eq("debug")) return LL_DEBUG;
|
|
if (eq("info")) return LL_INFO;
|
|
if (eq("warn") || eq("warning")) return LL_WARN;
|
|
if (eq("error") || eq("err")) return LL_ERROR;
|
|
if (eq("off") || eq("none") || eq("silent") || eq("0")) return LL_OFF;
|
|
return fallback;
|
|
}
|
|
|
|
const char* level_tag(LogLevel lvl)
|
|
{
|
|
switch (lvl) {
|
|
case LL_TRACE: return "TRACE";
|
|
case LL_DEBUG: return "DEBUG";
|
|
case LL_INFO: return "INFO";
|
|
case LL_WARN: return "WARN";
|
|
case LL_ERROR: return "ERROR";
|
|
case LL_OFF: return "OFF";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
thread_local std::string g_last_error;
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
// Set while this thread resolves the log settings or opens the log file:
|
|
// both read OBN_* env through env_var_get, which may log itself.
|
|
thread_local int g_setup_depth = 0;
|
|
|
|
struct SetupGuard {
|
|
SetupGuard() { ++g_setup_depth; }
|
|
~SetupGuard() { --g_setup_depth; }
|
|
};
|
|
|
|
bool truthy(const char* v)
|
|
{
|
|
return v[0] == '1' || v[0] == 'y' || v[0] == 'Y' || v[0] == 't' || v[0] == 'T';
|
|
}
|
|
|
|
// Level and stderr flag, re-read at most once a second: the main plugin
|
|
// may publish them after this library logged its first line, and reading
|
|
// the environment on every trace check is too slow on Windows.
|
|
struct CachedSettings {
|
|
std::atomic<int> level{LL_INFO};
|
|
std::atomic<bool> echo_stderr{true};
|
|
std::atomic<long long> next_refresh_ms{0};
|
|
};
|
|
|
|
CachedSettings& cached()
|
|
{
|
|
static CachedSettings c;
|
|
return c;
|
|
}
|
|
|
|
long long steady_ms()
|
|
{
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
void refresh_settings()
|
|
{
|
|
auto& c = cached();
|
|
const long long now = steady_ms();
|
|
long long due = c.next_refresh_ms.load(std::memory_order_relaxed);
|
|
if (now < due) return;
|
|
if (!c.next_refresh_ms.compare_exchange_strong(due, now + 1000)) return;
|
|
SetupGuard guard;
|
|
const char* lv = obn::lan_tls::env_var_get(obn::lan_tls::kEnvBsLogLevel);
|
|
c.level.store(lv ? parse_log_level(lv, LL_INFO) : LL_INFO);
|
|
const char* es = obn::lan_tls::env_var_get(obn::lan_tls::kEnvBsLogStderr);
|
|
c.echo_stderr.store(!es || !*es || truthy(es));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
LogLevel current_log_level()
|
|
{
|
|
refresh_settings();
|
|
return static_cast<LogLevel>(cached().level.load(std::memory_order_relaxed));
|
|
}
|
|
|
|
bool echo_stderr_enabled()
|
|
{
|
|
refresh_settings();
|
|
return cached().echo_stderr.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
namespace {
|
|
// Look up the absolute path of THIS DLL (the one that contains
|
|
// mirror_log_fp itself) using GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS.
|
|
// The plugin is installed to "<data_dir>\plugins\BambuSource.dll", so
|
|
// we strip the trailing two path components to recover <data_dir> at
|
|
// runtime — meaning the same DLL writes to "%APPDATA%\BambuStudio\"
|
|
// when Studio loads it and to "%APPDATA%\OrcaSlicer\" when Orca does.
|
|
std::string this_dll_data_dir()
|
|
{
|
|
HMODULE h = nullptr;
|
|
if (!::GetModuleHandleExW(
|
|
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
|
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
reinterpret_cast<LPCWSTR>(&this_dll_data_dir),
|
|
&h) || !h) {
|
|
return {};
|
|
}
|
|
wchar_t wpath[MAX_PATH] = {0};
|
|
DWORD n = ::GetModuleFileNameW(h, wpath, MAX_PATH);
|
|
if (n == 0 || n >= MAX_PATH) return {};
|
|
int u8 = ::WideCharToMultiByte(CP_UTF8, 0, wpath, -1,
|
|
nullptr, 0, nullptr, nullptr);
|
|
if (u8 <= 0) return {};
|
|
std::string path(static_cast<std::size_t>(u8), '\0');
|
|
if (::WideCharToMultiByte(CP_UTF8, 0, wpath, -1,
|
|
path.data(), u8, nullptr, nullptr) <= 0)
|
|
return {};
|
|
if (!path.empty() && path.back() == '\0') path.pop_back();
|
|
// Strip "\BambuSource.dll"
|
|
auto slash = path.find_last_of("\\/");
|
|
if (slash == std::string::npos) return {};
|
|
std::string dir = path.substr(0, slash);
|
|
// Strip "\plugins" if present (defensive: a developer running a
|
|
// build tree directly might have the DLL in a different layout).
|
|
auto slash2 = dir.find_last_of("\\/");
|
|
if (slash2 != std::string::npos) {
|
|
std::string leaf = dir.substr(slash2 + 1);
|
|
for (auto& c : leaf) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
if (leaf == "plugins") dir = dir.substr(0, slash2);
|
|
}
|
|
return dir;
|
|
}
|
|
} // namespace
|
|
#endif
|
|
|
|
static FILE* open_log_file(const char* path)
|
|
{
|
|
FILE* f = std::fopen(path, "a");
|
|
if (!f) return nullptr;
|
|
std::setvbuf(f, nullptr, _IONBF, 0);
|
|
std::fprintf(f, "--- obn libBambuSource opened (path=%s) ---\n", path);
|
|
return f;
|
|
}
|
|
|
|
// Resolve the default log file path when bambusource_log_to_file=1
|
|
// but no explicit bambusource_log_file is set.
|
|
static FILE* open_default_log_file()
|
|
{
|
|
#if defined(_WIN32)
|
|
std::string dd = this_dll_data_dir();
|
|
if (!dd.empty()) {
|
|
if (FILE* f = open_log_file((dd + "\\obn-bambusource.log").c_str()))
|
|
return f;
|
|
}
|
|
#else
|
|
if (const char* xdg = std::getenv("XDG_STATE_HOME")) {
|
|
std::string p = std::string(xdg) + "/bambu-studio/obn-bambusource.log";
|
|
if (FILE* f = open_log_file(p.c_str())) return f;
|
|
}
|
|
if (const char* home = std::getenv("HOME")) {
|
|
std::string p = std::string(home) + "/.local/state/bambu-studio/obn-bambusource.log";
|
|
if (FILE* f = open_log_file(p.c_str())) return f;
|
|
}
|
|
if (FILE* f = open_log_file("/tmp/obn-bambusource.log")) return f;
|
|
#endif
|
|
return nullptr;
|
|
}
|
|
|
|
// Resolution (matches main plugin pattern):
|
|
// 1. bambusource_log_file (explicit path, "off"/"none" to disable, "stderr" for stderr)
|
|
// 2. bambusource_log_to_file=1 -> auto-detect path from DLL location / $HOME
|
|
// 3. Neither set -> no file (default, same as main plugin)
|
|
FILE* mirror_log_fp()
|
|
{
|
|
static FILE* fp = []() -> FILE* {
|
|
SetupGuard guard;
|
|
const char* explicit_path =
|
|
obn::lan_tls::env_var_get(obn::lan_tls::kEnvBsLogFile);
|
|
if (explicit_path && *explicit_path) {
|
|
if (!std::strcmp(explicit_path, "off") ||
|
|
!std::strcmp(explicit_path, "none") || !std::strcmp(explicit_path, "0"))
|
|
return nullptr;
|
|
if (!std::strcmp(explicit_path, "stderr") || !std::strcmp(explicit_path, "-"))
|
|
return stderr;
|
|
if (FILE* f = open_log_file(explicit_path))
|
|
return f;
|
|
}
|
|
|
|
const char* to_file =
|
|
obn::lan_tls::env_var_get(obn::lan_tls::kEnvBsLogToFile);
|
|
if (to_file && truthy(to_file))
|
|
return open_default_log_file();
|
|
|
|
return nullptr;
|
|
}();
|
|
return fp;
|
|
}
|
|
|
|
static void emit_line(LogLevel lvl, const char* buf)
|
|
{
|
|
auto now = std::chrono::system_clock::now();
|
|
auto tt = std::chrono::system_clock::to_time_t(now);
|
|
const long long ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
now.time_since_epoch()).count() % 1000;
|
|
std::tm lt{};
|
|
obn::os::localtime_safe(tt, <);
|
|
char ts[40];
|
|
const std::size_t n = std::strftime(ts, sizeof(ts), "%F %T", <);
|
|
std::snprintf(ts + n, sizeof(ts) - n, ".%03lld", ms);
|
|
const long tid = obn::os::thread_id();
|
|
|
|
if (FILE* fp = mirror_log_fp())
|
|
std::fprintf(fp, "%s [%s] [%ld] %s\n", ts, level_tag(lvl), tid, buf);
|
|
if (echo_stderr_enabled())
|
|
std::fprintf(stderr, "[obn-bs] %s [%s] [%ld] %s\n", ts, level_tag(lvl), tid, buf);
|
|
}
|
|
|
|
static void vlog(LogLevel lvl, Logger logger, void* ctx, const char* fmt, va_list ap)
|
|
{
|
|
char small[1024];
|
|
va_list ap2;
|
|
va_copy(ap2, ap);
|
|
const int n = std::vsnprintf(small, sizeof(small), fmt, ap);
|
|
std::string big;
|
|
const char* buf = small;
|
|
if (n >= static_cast<int>(sizeof(small))) {
|
|
big.resize(static_cast<std::size_t>(n) + 1);
|
|
std::vsnprintf(big.data(), big.size(), fmt, ap2);
|
|
buf = big.c_str();
|
|
}
|
|
va_end(ap2);
|
|
|
|
emit_line(lvl, buf);
|
|
|
|
// Debug and trace stay in our own sinks: they would flood the slicer's
|
|
// log through its callback.
|
|
if (logger && lvl >= LL_INFO)
|
|
logger(ctx, /*level=*/static_cast<int>(lvl), strdup_for_logger(buf));
|
|
}
|
|
|
|
void log_at(LogLevel lvl, Logger logger, void* ctx, const char* fmt, ...)
|
|
{
|
|
if (lvl < current_log_level()) return;
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vlog(lvl, logger, ctx, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
void log_fmt(Logger logger, void* ctx, const char* fmt, ...)
|
|
{
|
|
if (LL_INFO < current_log_level()) return;
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vlog(LL_INFO, logger, ctx, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Shared code (TUTK, :6000, FTPS, TLS) logs through OBN_*; route it here
|
|
// so it follows bambusource_log_* and lands in obn-bambusource.log.
|
|
void forward_emit(obn::log::Level lvl, const char* msg)
|
|
{
|
|
emit_line(static_cast<LogLevel>(lvl), msg);
|
|
}
|
|
|
|
obn::log::Level forward_threshold()
|
|
{
|
|
if (g_setup_depth > 0) return obn::log::LVL_OFF;
|
|
return static_cast<obn::log::Level>(current_log_level());
|
|
}
|
|
|
|
[[maybe_unused]] const bool g_forward_installed = [] {
|
|
obn::log::set_forward(forward_emit, forward_threshold);
|
|
return true;
|
|
}();
|
|
|
|
} // namespace
|
|
|
|
void set_last_error(const char* msg)
|
|
{
|
|
g_last_error.assign(msg ? msg : "");
|
|
}
|
|
|
|
const char* get_last_error()
|
|
{
|
|
return g_last_error.c_str();
|
|
}
|
|
|
|
} // namespace obn::source
|