#include "source_log.hpp" #include "obn/os_compat.hpp" #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 LogLevel current_log_level() { static const LogLevel lvl = []() { return parse_log_level(std::getenv("OBN_BAMBUSOURCE_LOG_LEVEL"), LL_INFO); }(); return lvl; } #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 // Resolution order: // 1. $OBN_BAMBUSOURCE_LOG_FILE (set to "off"/"none"/empty to disable // the file mirror entirely; "stderr" routes to stderr). // 2. Windows: \obn-bambusource.log // POSIX: $XDG_STATE_HOME/bambu-studio/obn-bambusource.log // 3. POSIX: $HOME/.local/state/bambu-studio/obn-bambusource.log // Windows: %APPDATA%\BambuStudio\obn-bambusource.log (legacy) // 4. Last-resort fallback (TEMP / /tmp). FILE* mirror_log_fp() { static FILE* fp = []() -> FILE* { if (const char* env = std::getenv("OBN_BAMBUSOURCE_LOG_FILE")) { if (!*env || !std::strcmp(env, "off") || !std::strcmp(env, "none") || !std::strcmp(env, "0")) return nullptr; if (!std::strcmp(env, "stderr") || !std::strcmp(env, "-")) return stderr; if (FILE* f = std::fopen(env, "a")) { // _IOLBF + nullptr buffer is invalid on MSVC CRT and triggers // __fastfail(FAST_FAIL_INVALID_ARG); use _IONBF (no buffering) // which is allowed with a NULL buffer on every supported CRT. std::setvbuf(f, nullptr, _IONBF, 0); std::fprintf(f, "--- obn libBambuSource opened ---\n"); return f; } // Fall through to the default search if the user-supplied // path could not be opened — better than dropping logs. } static std::string p0, p1, p2, p3; const char* paths[4] = {nullptr, nullptr, nullptr, nullptr}; #if defined(_WIN32) // Preferred: alongside the host slicer's data dir, recovered // from this DLL's installed path. Works for both Studio and // Orca without any compile-time switch. std::string dd = this_dll_data_dir(); if (!dd.empty()) { p0 = dd + "\\obn-bambusource.log"; paths[0] = p0.c_str(); } if (const char* appdata = std::getenv("APPDATA")) { p1 = std::string(appdata) + "\\BambuStudio\\obn-bambusource.log"; paths[1] = p1.c_str(); } if (const char* lappdata = std::getenv("LOCALAPPDATA")) { p2 = std::string(lappdata) + "\\BambuStudio\\obn-bambusource.log"; paths[2] = p2.c_str(); } if (const char* tmp = std::getenv("TEMP")) { p3 = std::string(tmp) + "\\obn-bambusource.log"; paths[3] = p3.c_str(); } else { paths[3] = "C:\\obn-bambusource.log"; } #else if (const char* xdg = std::getenv("XDG_STATE_HOME")) { p0 = std::string(xdg) + "/bambu-studio/obn-bambusource.log"; paths[0] = p0.c_str(); } if (const char* home = std::getenv("HOME")) { p1 = std::string(home) + "/.local/state/bambu-studio/obn-bambusource.log"; paths[1] = p1.c_str(); } paths[2] = "/tmp/obn-bambusource.log"; #endif for (const char* path : paths) { if (!path) continue; if (FILE* f = std::fopen(path, "a")) { // See note above: _IOLBF + nullptr buffer crashes the MSVC CRT. std::setvbuf(f, nullptr, _IONBF, 0); std::fprintf(f, "--- obn libBambuSource opened (path=%s) ---\n", path); return f; } } return nullptr; }(); return fp; } void log_at(LogLevel lvl, Logger logger, void* ctx, const char* fmt, ...) { if (lvl < current_log_level()) return; char buf[512]; va_list ap; va_start(ap, fmt); std::vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); if (FILE* fp = mirror_log_fp()) { auto now = std::chrono::system_clock::now(); auto tt = std::chrono::system_clock::to_time_t(now); std::tm lt{}; obn::os::localtime_safe(tt, <); char ts[32]; std::strftime(ts, sizeof(ts), "%F %T", <); std::fprintf(fp, "%s [%s] %s\n", ts, level_tag(lvl), buf); } if (logger) { // Studio expects a heap-allocated buffer that it will free via // Bambu_FreeLogMsg. POSIX -> strdup; Windows -> wide-char copy // via strdup_for_logger() (caller frees with free()). logger(ctx, /*level=*/static_cast(lvl), strdup_for_logger(buf)); } } void log_fmt(Logger logger, void* ctx, const char* fmt, ...) { if (LL_INFO < current_log_level()) return; char buf[512]; va_list ap; va_start(ap, fmt); std::vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); if (FILE* fp = mirror_log_fp()) { auto now = std::chrono::system_clock::now(); auto tt = std::chrono::system_clock::to_time_t(now); std::tm lt{}; obn::os::localtime_safe(tt, <); char ts[32]; std::strftime(ts, sizeof(ts), "%F %T", <); std::fprintf(fp, "%s [INFO] %s\n", ts, buf); } if (logger) { logger(ctx, /*level=*/static_cast(LL_INFO), strdup_for_logger(buf)); } } 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