mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
140 lines
5.7 KiB
Rust
140 lines
5.7 KiB
Rust
//! Tauri shell side of file-based logging.
|
|
//!
|
|
//! Resolves the OpenHuman data directory the same way the core does
|
|
//! (`~/.openhuman` or `OPENHUMAN_WORKSPACE` override) and hands it to
|
|
//! [`openhuman_core::core::logging::init_for_embedded`], which installs a
|
|
//! daily-rotated file appender so packaged GUI builds — where stderr is
|
|
//! invisible — still produce a log users can share for support.
|
|
//!
|
|
//! Both the shell's `log::*` calls (via the `tracing_log::LogTracer` bridge)
|
|
//! and the embedded core's `tracing::*` events funnel into the same file.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use openhuman_core::core::logging::{self, log_directory};
|
|
|
|
/// Initialize logging for the Tauri shell + embedded core. Idempotent and
|
|
/// safe to call from any startup position; the underlying `Once` guard means
|
|
/// the first caller's data dir wins.
|
|
///
|
|
/// Verbosity defaults to `info` (or `debug` when `OPENHUMAN_VERBOSE=1`); the
|
|
/// `RUST_LOG` env var continues to override both.
|
|
pub fn init() {
|
|
let data_dir = resolve_data_dir();
|
|
let verbose = std::env::var("OPENHUMAN_VERBOSE")
|
|
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
.unwrap_or(false);
|
|
logging::init_for_embedded(&data_dir, verbose);
|
|
}
|
|
|
|
/// Resolve the directory used to host `<data_dir>/logs/`. Mirrors the core's
|
|
/// own resolution so log files sit next to `active_user.toml`, the per-user
|
|
/// `users/` tree, and the CEF caches a support engineer would also need.
|
|
///
|
|
/// If `default_root_openhuman_dir` fails (very unusual — it requires
|
|
/// `dirs::home_dir` to return `None`), falls back to `<temp>/openhuman`
|
|
/// rather than a relative `.openhuman` whose final location depends on the
|
|
/// shell's CWD at launch time.
|
|
pub(crate) fn resolve_data_dir() -> PathBuf {
|
|
if let Ok(workspace) = std::env::var("OPENHUMAN_WORKSPACE") {
|
|
if !workspace.is_empty() {
|
|
return PathBuf::from(workspace);
|
|
}
|
|
}
|
|
openhuman_core::openhuman::config::default_root_openhuman_dir().unwrap_or_else(|err| {
|
|
eprintln!(
|
|
"[file_logging] default_root_openhuman_dir failed ({err}); falling back to temp dir"
|
|
);
|
|
std::env::temp_dir().join("openhuman")
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Lock around env-var mutation. Cargo runs unit tests in parallel
|
|
/// threads in the same process, so concurrent `set_var` / `remove_var`
|
|
/// can race; the lock keeps the env stable for each test's duration.
|
|
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
|
|
#[test]
|
|
fn resolve_data_dir_honors_workspace_override() {
|
|
let _guard = ENV_LOCK.lock().unwrap();
|
|
let prior = std::env::var("OPENHUMAN_WORKSPACE").ok();
|
|
std::env::set_var("OPENHUMAN_WORKSPACE", "/tmp/openhuman-test-override");
|
|
let dir = resolve_data_dir();
|
|
assert_eq!(dir, PathBuf::from("/tmp/openhuman-test-override"));
|
|
match prior {
|
|
Some(v) => std::env::set_var("OPENHUMAN_WORKSPACE", v),
|
|
None => std::env::remove_var("OPENHUMAN_WORKSPACE"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_data_dir_ignores_empty_workspace() {
|
|
let _guard = ENV_LOCK.lock().unwrap();
|
|
let prior = std::env::var("OPENHUMAN_WORKSPACE").ok();
|
|
std::env::set_var("OPENHUMAN_WORKSPACE", "");
|
|
// Empty string must NOT short-circuit — fall through to the
|
|
// default resolver so the user's real `~/.openhuman` is used.
|
|
let dir = resolve_data_dir();
|
|
assert_ne!(dir, PathBuf::from(""));
|
|
assert!(dir.is_absolute(), "expected absolute fallback, got {dir:?}");
|
|
match prior {
|
|
Some(v) => std::env::set_var("OPENHUMAN_WORKSPACE", v),
|
|
None => std::env::remove_var("OPENHUMAN_WORKSPACE"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn logs_folder_path_returns_none_pre_init() {
|
|
// `init()` is `Once`-guarded across the whole process, so in unit
|
|
// tests where the embedded subscriber hasn't been installed,
|
|
// `logs_folder_path` should return `None` rather than a stale path.
|
|
// (When run alongside a test that *did* call `init`, the function
|
|
// is allowed to return Some — assert the type signature only.)
|
|
let result = logs_folder_path();
|
|
let _: Option<String> = result;
|
|
}
|
|
|
|
#[test]
|
|
fn reveal_logs_folder_errors_when_uninitialized() {
|
|
// If logging hasn't been initialized, the command must surface a
|
|
// typed error so the UI can show it instead of silently launching
|
|
// an `open` against an empty path.
|
|
if openhuman_core::core::logging::log_directory().is_none() {
|
|
let err = reveal_logs_folder().expect_err("must error pre-init");
|
|
assert!(err.contains("not initialized"), "unexpected error: {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tauri command — return the absolute path to the active log directory, or
|
|
/// `None` if logging hasn't been initialized in embedded mode (shouldn't
|
|
/// happen at runtime; guard for tests).
|
|
#[tauri::command]
|
|
pub fn logs_folder_path() -> Option<String> {
|
|
log_directory().map(|p| p.display().to_string())
|
|
}
|
|
|
|
/// Tauri command — open the platform file manager at the log directory so a
|
|
/// user can grab today's log file and send it to support.
|
|
#[tauri::command]
|
|
pub fn reveal_logs_folder() -> Result<(), String> {
|
|
let dir = log_directory().ok_or_else(|| "log directory not initialized".to_string())?;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
let result = std::process::Command::new("open").arg(dir).spawn();
|
|
|
|
#[cfg(target_os = "windows")]
|
|
let result = std::process::Command::new("explorer").arg(dir).spawn();
|
|
|
|
#[cfg(target_os = "linux")]
|
|
let result = std::process::Command::new("xdg-open").arg(dir).spawn();
|
|
|
|
result
|
|
.map(|_| ())
|
|
.map_err(|e| format!("failed to open log directory {}: {e}", dir.display()))
|
|
}
|