#include "source_log.hpp" #include "obn/lan_tls_env.hpp" #include "obn/log.hpp" #include "obn/os_compat.hpp" #include #include #include #include #include #include #include #include #include #if defined(_WIN32) # ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN # endif # ifndef NOMINMAX # define NOMINMAX # endif # include #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(std::malloc(sizeof(wchar_t))); if (empty) empty[0] = L'\0'; return empty; } auto* w = static_cast( std::malloc(static_cast(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(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 level{LL_INFO}; std::atomic echo_stderr{true}; std::atomic next_refresh_ms{0}; }; CachedSettings& cached() { static CachedSettings c; return c; } long long steady_ms() { return std::chrono::duration_cast( 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(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 "\plugins\BambuSource.dll", so // we strip the trailing two path components to recover 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(&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(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(std::tolower(static_cast(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( 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(sizeof(small))) { big.resize(static_cast(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(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(lvl), msg); } obn::log::Level forward_threshold() { if (g_setup_depth > 0) return obn::log::LVL_OFF; return static_cast(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