Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions crates/uffs-daemon/src/log_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@

use std::path::PathBuf;

/// Default log file location: `<data-local-dir>/uffs/uffsd.log`.
/// Default log file location: `<native-log-dir>/uffsd.log`.
///
/// Falls back to `./uffsd.log` if the platform data directory
/// cannot be determined.
/// The directory is the shared per-platform native log location resolved
/// by [`uffs_security::log_dir::log_dir`] (macOS `~/Library/Logs/uffs`,
/// Windows `%LOCALAPPDATA%\uffs\logs`, Linux `$XDG_STATE_HOME/uffs/logs`),
/// overridable via `UFFS_LOG_DIR`. When that resolution falls back to a
/// relative `logs` (no home dir), the parent-dir normalisation in
/// [`init_tracing`] still produces a usable `logs/uffsd.log`.
#[must_use]
pub(crate) fn default_log_file() -> PathBuf {
dirs_next::data_local_dir().map_or_else(
|| PathBuf::from("uffsd.log"),
|dir| dir.join("uffs").join("uffsd.log"),
)
uffs_security::log_dir::log_dir().join("uffsd.log")
}

/// Initialise tracing for the daemon process.
Expand Down
6 changes: 5 additions & 1 deletion crates/uffs-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ uffs-client.workspace = true
# code can name `DriveLetter` natively (parse into the typed value
# rather than chasing the re-export through `uffs-client`).
uffs-mft.workspace = true
# Shared per-platform native log-directory resolution (used by the
# stdio + HTTP-gateway log-file defaults). Direct dep so the MCP
# binaries don't reinvent the path logic; replaces the former direct
# `dirs-next` usage.
uffs-security.workspace = true

# Async — `net` re-enabled because the HTTP gateway binds a TCP
# listener and the health-check probes use `tokio::net::TcpStream`.
Expand All @@ -111,7 +116,6 @@ thiserror.workspace = true
# Logging
tracing.workspace = true
tracing-subscriber.workspace = true
dirs-next.workspace = true
tracing-appender.workspace = true

# HTTP gateway (feature-gated)
Expand Down
2 changes: 1 addition & 1 deletion crates/uffs-mcp/src/bin/http_gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use core::net::SocketAddr;
use anyhow as _;
use axum as _;
use clap as _;
use dirs_next as _;
use rmcp as _;
use schemars as _;
use serde as _;
Expand All @@ -29,6 +28,7 @@ use tower_service as _;
use tracing_appender as _;
use uffs_client as _;
use uffs_mft as _;
use uffs_security as _;

/// CLI arguments for the HTTP gateway.
#[derive(Clone, Debug)]
Expand Down
9 changes: 5 additions & 4 deletions crates/uffs-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,12 @@ pub fn init_mcp_tracing(
}

/// Default log file path for MCP diagnostic sessions.
///
/// Uses the shared per-platform native log directory resolved by
/// [`uffs_security::log_dir::log_dir`] (overridable via `UFFS_LOG_DIR`);
/// an explicit `UFFS_LOG_FILE` still takes precedence at the call site.
fn default_mcp_log_file() -> std::path::PathBuf {
dirs_next::data_local_dir()
.unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
.join("uffs")
.join("uffs_mcp.log")
uffs_security::log_dir::log_dir().join("uffs_mcp.log")
}

// Phase 3 module-layout: most submodules are crate-internal. Only
Expand Down
6 changes: 1 addition & 5 deletions crates/uffs-mcp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
// Crates used by the library but not directly by this binary.
#[cfg(feature = "streamable-http")]
use axum as _;
use dirs_next as _;
use rmcp as _;
use schemars as _;
use serde as _;
Expand Down Expand Up @@ -306,10 +305,7 @@ async fn mcp_start(
cmd.stderr(std::process::Stdio::null());

if std::env::var("UFFS_LOG_FILE").is_err() {
let default_log = dirs_next::data_local_dir()
.unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
.join("uffs")
.join("mcp-gateway.log");
let default_log = uffs_security::log_dir::log_dir().join("mcp-gateway.log");
cmd.env("UFFS_LOG_FILE", &default_log);
}

Expand Down
2 changes: 1 addition & 1 deletion crates/uffs-mcp/tests/mcp_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use anyhow as _;
#[cfg(feature = "streamable-http")]
use axum as _;
use clap as _;
use dirs_next as _;
use rmcp::{ClientHandler, ServiceExt as _};
use schemars as _;
use serde as _;
Expand All @@ -40,6 +39,7 @@ use tracing_subscriber as _;
use uffs_client as _;
use uffs_mcp::handler::UffsMcpServer;
use uffs_mft as _;
use uffs_security as _;

/// Spin up an in-process MCP server + client pair over a duplex channel.
///
Expand Down
19 changes: 6 additions & 13 deletions crates/uffs-mft/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
//! Logging initialization for the `uffs-mft` binary.

use std::io;
use std::path::PathBuf;

use tracing_appender::non_blocking::NonBlocking;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
Expand All @@ -18,25 +17,19 @@ use tracing_subscriber::{EnvFilter, Layer as _};
/// If `verbose` is true and `RUST_LOG` is not set, uses `debug` level for
/// terminal. Otherwise, terminal logging is controlled by `RUST_LOG` (default:
/// `info`). File logging is controlled by `RUST_LOG_FILE` (default: `info`).
/// Log directory is controlled by `UFFS_LOG_DIR` (default: `~/bin/uffs/logs`).
/// Log directory is the shared per-platform native location resolved by
/// [`uffs_security::log_dir::log_dir`] (overridable via `UFFS_LOG_DIR`).
#[expect(
clippy::single_call_fn,
reason = "logical separation of logging initialization"
)]
pub(crate) fn init_logging(verbose: bool) -> tracing_appender::non_blocking::WorkerGuard {
use std::fs;

// Get log directory (default: ~/bin/uffs/logs)
let log_dir = std::env::var("UFFS_LOG_DIR").map_or_else(
|_| {
dirs_next::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("bin")
.join("uffs")
.join("logs")
},
PathBuf::from,
);
// Shared native log dir (macOS ~/Library/Logs/uffs, Windows
// %LOCALAPPDATA%\uffs\logs, Linux $XDG_STATE_HOME/uffs/logs),
// honoring the UFFS_LOG_DIR override.
let log_dir = uffs_security::log_dir::log_dir();

// Create log directory if it doesn't exist
drop(fs::create_dir_all(&log_dir));
Expand Down
4 changes: 4 additions & 0 deletions crates/uffs-mft/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ use clap::Parser as _;
use criterion as _;
// Pipelining / chaos-test dependencies (used cross-platform)
use crossbeam_channel as _;
// `dirs_next` is used only by the library (`cache.rs` cache-dir lookup);
// the binary's logging now routes through `uffs_security::log_dir`, so
// acknowledge the dep here to keep `unused-crate-dependencies` quiet.
use dirs_next as _;
#[cfg(test)]
use hex as _;
// Platform-gated dependencies (used on Windows only)
Expand Down
7 changes: 7 additions & 0 deletions crates/uffs-security/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
//! - [`runtime_dir`] — Daemon-private runtime tempfile lifecycle (Phase 2b
//! memory tiering): owner-only file creation, orphan-pid sweep, read-only
//! mmap behind a typed soundness wrapper
//! - [`log_dir`] — Shared per-platform native log-directory resolution for
//! every UFFS binary (macOS `~/Library/Logs/uffs`, Windows
//! `%LOCALAPPDATA%\uffs\logs`, Linux `$XDG_STATE_HOME/uffs/logs`)
//!
//! # Environment
//!
Expand All @@ -27,6 +30,8 @@
//! |---|---|---|---|
//! | `UFFS_DEV` | `bool` | `false` | Enables dev-mode keystore relaxation in [`keystore`] (no DPAPI binding; file-based key at `~/.local/share/uffs/key.bin` on Unix). INTERNAL semver class. |
//! | `USERNAME` | `string` | (Windows: current user) | Read by [`fs::set_file_permissions_owner_only`] on Windows to derive the principal for the `icacls /grant` ACL. STANDARD semver class. |
//! | `UFFS_LOG_DIR` | path | (native per-OS dir) | Read by [`log_dir`] to override the log directory for every UFFS binary. STANDARD semver class. |
//! | `XDG_STATE_HOME` | path | `~/.local/state` | Read by [`log_dir`] on Linux for the native log location (absolute paths only, per XDG spec). STANDARD semver class. |

// Platform-gated deps: used by sub-modules behind #[cfg] gates.
// Suppress unused-crate-dependencies lint for platforms where the
Expand All @@ -38,6 +43,8 @@ use security_framework as _;
pub mod crypto;
pub mod fs;
pub mod keystore;
/// Shared per-platform log-directory resolution for all UFFS binaries.
pub mod log_dir;
pub mod runtime_dir;

/// Windows named-pipe security helpers (DACL, SID resolution, pipe naming).
Expand Down
140 changes: 140 additions & 0 deletions crates/uffs-security/src/log_dir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2025-2026 SKY, LLC.

//! Shared per-platform log-directory resolution for all UFFS binaries.
//!
//! UFFS is a shipped end-user product, so logs go where each OS expects
//! them rather than into a single `~/.uffs` dotfile, the cache dir, or
//! `~/bin` (a binaries dir — writing `logs/` under the installed `uffs`
//! file there fails with `Not a directory`).
//!
//! # Resolution order
//!
//! 1. `UFFS_LOG_DIR` env var, if set to a **non-empty** value — used verbatim.
//! 2. The per-platform native location
//! ([`native_log_dir`](crate::log_dir::native_log_dir)):
//! - macOS: `~/Library/Logs/uffs` (read by Console.app)
//! - Windows: `%LOCALAPPDATA%\uffs\logs` (== `dirs data_local_dir`)
//! - Linux: `$XDG_STATE_HOME/uffs/logs`, else `~/.local/state/uffs/logs`
//! 3. `./logs` — final fallback, only when the home dir cannot be determined.
//!
//! All UFFS binaries share **one** base directory; each keeps its own
//! distinct filename within it (`uffsd.log`, `uffs_mcp.log`,
//! `mcp-gateway.log`, `uffs_mft_log_*`), so Console.app / journald show
//! every UFFS log in one place.
//!
//! # Environment
//!
//! | Env var | Type | Default | Notes |
//! |---|---|---|---|
//! | `UFFS_LOG_DIR` | path | (native per-OS dir) | Overrides the log directory for every UFFS binary. STANDARD semver class. |

use std::path::PathBuf;

/// Env var that overrides the log directory for every UFFS binary.
pub const LOG_DIR_ENV: &str = "UFFS_LOG_DIR";

/// Resolve the UFFS log directory, honoring the `UFFS_LOG_DIR` override.
///
/// See the [module docs](self) for the full resolution order. The
/// returned path is **not** created — callers are responsible for
/// `create_dir_all` (they already do, and need to handle the error in
/// their own logging-init style).
#[must_use]
pub fn log_dir() -> PathBuf {
match std::env::var_os(LOG_DIR_ENV) {
Some(value) if !value.is_empty() => PathBuf::from(value),
_ => native_log_dir(),
}
}

/// The per-platform native log directory, ignoring any env override.
///
/// Exposed for callers that want the OS-native location regardless of
/// `UFFS_LOG_DIR` (e.g. diagnostics that report "where logs *would*
/// land natively"). Most code should call [`log_dir`] instead.
#[must_use]
pub fn native_log_dir() -> PathBuf {
// Bind the per-platform value rather than early-returning from each
// cfg arm, so clippy's `needless_return` does not fire on the
// single-expression branches.
#[cfg(target_os = "macos")]
let dir = macos_log_dir();

#[cfg(target_os = "windows")]
let dir = windows_log_dir();

#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let dir = linux_log_dir();

dir
}

/// macOS: `~/Library/Logs/uffs` (the location Console.app reads).
///
/// Falls back to `./logs` if the home directory cannot be determined.
#[cfg(target_os = "macos")]
fn macos_log_dir() -> PathBuf {
dirs_next::home_dir().map_or_else(
|| PathBuf::from("logs"),
|home| home.join("Library").join("Logs").join("uffs"),
)
}

/// Windows: `%LOCALAPPDATA%\uffs\logs` (== `dirs_next::data_local_dir`).
///
/// Falls back to `./logs` if the local-app-data directory cannot be
/// determined.
#[cfg(target_os = "windows")]
fn windows_log_dir() -> PathBuf {
dirs_next::data_local_dir().map_or_else(
|| PathBuf::from("logs"),
|dir| dir.join("uffs").join("logs"),
)
}

/// Linux / other Unix: `$XDG_STATE_HOME/uffs/logs`, falling back to
/// `~/.local/state/uffs/logs`.
///
/// `dirs_next` has no `state_dir()` helper, so the XDG state home is
/// resolved by hand per the XDG Base Directory spec: honor
/// `XDG_STATE_HOME` only when it is an **absolute** path (the spec
/// requires relative values to be ignored), else `~/.local/state`.
/// Final fallback is `./logs` if the home directory is also unknown.
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn linux_log_dir() -> PathBuf {
let state_home = xdg_state_home(std::env::var_os("XDG_STATE_HOME"), dirs_next::home_dir());
state_home.join("uffs").join("logs")
}

/// Resolve the XDG state home from a raw `XDG_STATE_HOME` value and the
/// detected home dir, applying the XDG spec's absolute-path rule.
///
/// Honors `xdg_state_home_raw` only when it is an **absolute** path (the
/// spec requires relative values to be ignored), else `~/.local/state`.
/// Returns `.` (current dir) only when neither input yields a path — the
/// caller then lands on `./uffs/logs`, close enough to the documented
/// `./logs` last-resort fallback for the genuinely-headless case.
///
/// Split out as a pure function (no direct `std::env` read) so it is
/// unit-testable without mutating the process-global environment, which
/// is `unsafe` under the Rust 2024 edition.
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn xdg_state_home(
xdg_state_home_raw: Option<std::ffi::OsString>,
home: Option<PathBuf>,
) -> PathBuf {
if let Some(value) = xdg_state_home_raw {
let candidate = PathBuf::from(&value);
if candidate.is_absolute() {
return candidate;
}
}
home.map_or_else(
|| PathBuf::from("."),
|home_dir| home_dir.join(".local").join("state"),
)
}

#[cfg(test)]
mod tests;
Loading
Loading