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
4 changes: 4 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ members = [
"crates/uffs-text", # 📝 Unicode text processing, i18n foundation
"crates/uffs-time", # ⏱️ NTFS FILETIME arithmetic (pure, zero deps)
"crates/uffs-version", # 🏷️ Shared --version strings + build-metadata stamp (leaf)
"crates/uffs-statusfmt", # 🎨 Shared operator-status styling (color, glyphs, fields) (leaf)
"crates/uffs-broker-protocol", # 📟 Cross-platform broker wire-protocol types (F5)
"crates/uffs-winsvc", # 🪟 Native Windows service control + broker-pipe probe (leaf)
"crates/uffs-mft", # 📦 MFT reading → Polars DataFrame
Expand Down Expand Up @@ -132,6 +133,7 @@ uffs-security = { path = "crates/uffs-security", version = "0.6.23" }
uffs-text = { path = "crates/uffs-text", version = "0.6.23" }
uffs-time = { path = "crates/uffs-time", version = "0.6.23" }
uffs-version = { path = "crates/uffs-version", version = "0.6.23" }
uffs-statusfmt = { path = "crates/uffs-statusfmt", version = "0.6.23" }
uffs-mft = { path = "crates/uffs-mft", version = "0.6.23" }
uffs-format = { path = "crates/uffs-format", version = "0.6.23" }
uffs-core = { path = "crates/uffs-core", version = "0.6.23" }
Expand Down
4 changes: 2 additions & 2 deletions crates/uffs-client/src/protocol/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
use serde::{Deserialize, Serialize};

pub use super::response_status::{
DaemonStatus, DriveInfo, DriveMemoryInfo, DrivesResponse, ShardTier, StatsResponse,
StatusResponse,
DaemonPaths, DaemonStatus, DriveInfo, DriveMemoryInfo, DrivesResponse, LiveUpdateInfo,
ShardTier, StatsResponse, StatusResponse,
};
pub use super::response_tiering::{
DEFAULT_PRELOAD_PIN_MINUTES, DriveTierStatus, ForgetParams, ForgetResponse, HibernateParams,
Expand Down
54 changes: 54 additions & 0 deletions crates/uffs-client/src/protocol/response_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,60 @@ pub struct StatusResponse {
/// Per-drive memory breakdown (drive letter → heap bytes).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub drive_memory: Vec<DriveMemoryInfo>,
/// Short git commit the running daemon was built from (`UFFS_GIT_SHA`,
/// `-dirty` for a modified tree). Lets an operator confirm a rebuilt binary
/// actually took effect — the semver alone can't distinguish two builds of
/// the same version. `""`/`"unknown"` on daemons that predate this field.
#[serde(default)]
pub git_sha: String,
/// Whether the daemon process is elevated (Administrator). When `false` the
/// daemon reads the live MFT through the Access Broker's duplicated handle;
/// see [`Self::reading_via_broker`].
#[serde(default)]
pub elevated: bool,
/// Whether the daemon is reading the live MFT via **adopted Access Broker
/// handles** (the zero-UAC path) rather than its own elevated handles.
/// Always `false` off Windows / for offline-MFT sources.
#[serde(default)]
pub reading_via_broker: bool,
/// Live-update (USN journal) state — how many per-shard journal loops are
/// running. Absent on daemons that predate this field or platforms without
/// live update.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub live_update: Option<LiveUpdateInfo>,
/// Filesystem locations the daemon is using (data/cache dir, socket/pipe,
/// log dir), so the operator can find the index, connect, and read logs.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub paths: Option<DaemonPaths>,
}

/// Live-update (USN journal) liveness, surfaced in [`StatusResponse`].
///
/// Deliberately just the loop count: it is set **once when the loops spawn**,
/// so reporting it costs nothing on the live-indexing hot path (unlike a
/// per-patch "last applied" timestamp, which would add a store to every USN
/// apply). "N loops running" answers the operative question — *is live update
/// actually active?* — which is what an operator needs.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct LiveUpdateInfo {
/// Number of per-shard journal loops currently running (0 = live update
/// not active, e.g. offline MFT sources or a non-Windows host).
pub active_loops: usize,
}

/// Filesystem locations the daemon is operating from, surfaced in
/// [`StatusResponse`] so an operator can locate the index, socket, and logs.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DaemonPaths {
/// Index data / cache directory.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub data_dir: String,
/// IPC socket (Unix) or named pipe (Windows) clients connect on.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub socket: String,
/// Directory the daemon writes its logs to.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub log_dir: String,
}

/// Per-drive memory breakdown for status reporting.
Expand Down
11 changes: 11 additions & 0 deletions crates/uffs-daemon/src/index/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ impl IndexManager {
(Some(mem.rss_bytes), mem.mimalloc_committed_bytes)
});

// Broker-adoption mode: reading via adopted broker handles (zero-UAC)
// vs the daemon's own elevated handles. `is_elevated()` is `false` off
// Windows; the handle count is `0` there, so both read as "direct".
let elevated = uffs_mft::is_elevated();
let reading_via_broker = uffs_mft::registered_broker_handle_count() > 0;

StatusResponse {
status: status_snapshot,
uptime_secs: self.start_time.elapsed().as_secs(),
Expand All @@ -141,6 +147,11 @@ impl IndexManager {
index_heap_bytes: Some(total_index_heap),
mimalloc_committed_bytes,
drive_memory,
git_sha: option_env!("UFFS_GIT_SHA").unwrap_or("unknown").to_owned(),
elevated,
reading_via_broker,
live_update: crate::live_update::snapshot(),
paths: Some(crate::live_update::daemon_paths()),
}
}

Expand Down
6 changes: 6 additions & 0 deletions crates/uffs-daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ mod index;
mod ipc;
/// Lifecycle manager — PID file, idle timer, shutdown coordination.
mod lifecycle;
/// `--status` observability signals (live-update loop count, daemon paths).
mod live_update;
/// JSON-RPC protocol types.
mod protocol;
/// Phase 2b memory-tiering: runtime-tempfile orphan cleanup at boot.
Expand Down Expand Up @@ -589,6 +591,7 @@ async fn spawn_journal_loops_for_warm_shards(
save_threshold_age_secs = config.save_threshold_age.as_secs(),
"Spawning per-shard journal loops",
);
let loop_count = letters.len();
for letter in letters {
let source: Arc<dyn JournalSource> = make_journal_source(letter);
let handle = spawn_journal_loop(
Expand All @@ -600,6 +603,9 @@ async fn spawn_journal_loops_for_warm_shards(
);
idx.attach_journal_handle(letter, handle);
}
// Record live-update liveness once, here — so `--status` can report "N
// loops running" without any per-patch bookkeeping on the apply hot path.
live_update::set_active_loops(loop_count);

applier_handle
}
Expand Down
48 changes: 48 additions & 0 deletions crates/uffs-daemon/src/live_update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2025-2026 SKY, LLC.

//! `--status` observability signals kept off the live-indexing hot path.
//!
//! Live-update liveness is reported as the **number of running per-shard
//! journal loops**, recorded once when they spawn (`set_active_loops` — a
//! single relaxed store). It is deliberately *not* a per-patch "last applied"
//! timestamp: that would add a store to every USN apply on the hot path, and
//! "N loops running" already answers the operative question — is live update
//! actually active? The daemon's filesystem locations are gathered on demand
//! from the shared path helpers. Both are read only when a `status` RPC lands.

use core::sync::atomic::{AtomicUsize, Ordering};

use uffs_client::protocol::response::{DaemonPaths, LiveUpdateInfo};

/// Number of per-shard journal loops currently running. Set once after the
/// loops spawn; `0` means live update is not active (offline MFT / non-Windows,
/// or before the loops start).
static ACTIVE_LOOPS: AtomicUsize = AtomicUsize::new(0);

/// Record how many live journal loops are running — called once, right after
/// they spawn. A single relaxed store, so it adds nothing to the per-patch
/// apply path.
pub(crate) fn set_active_loops(count: usize) {
ACTIVE_LOOPS.store(count, Ordering::Relaxed);
}

/// Snapshot live-update liveness for a `status` RPC. `None` when no loops are
/// running, so the field is omitted for offline-MFT / non-Windows daemons.
pub(crate) fn snapshot() -> Option<LiveUpdateInfo> {
match ACTIVE_LOOPS.load(Ordering::Relaxed) {
0 => None,
active_loops => Some(LiveUpdateInfo { active_loops }),
}
}

/// The daemon's filesystem locations for a `status` RPC, gathered on demand
/// from the shared path helpers (index/cache dir, client socket/pipe, log dir).
/// Always available, so the caller wraps it in `Some`.
pub(crate) fn daemon_paths() -> DaemonPaths {
DaemonPaths {
data_dir: uffs_mft::cache::cache_dir().display().to_string(),
socket: uffs_client::daemon_ctl::socket_path().display().to_string(),
log_dir: std::env::var("UFFS_LOG_DIR").unwrap_or_default(),
}
}
1 change: 1 addition & 0 deletions crates/uffs-mft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ pub use platform::current_euid;
// Unix: geteuid() == 0). Exported unconditionally so uffs-cli and
// uffs-daemon can gate mutating daemon commands on all targets.
pub use platform::is_elevated;
pub use platform::registered_broker_handle_count;
// Re-export platform types
// Core types (DriveType, MftBitmap, MftExtent) are pure data — available on all platforms
// Windows-specific types and functions (VolumeHandle, detect_ntfs_drives, etc.) only on
Expand Down
11 changes: 11 additions & 0 deletions crates/uffs-mft/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ pub use system::{
detect_boot_drive, detect_drive_type, detect_ntfs_drives, infer_drive_from_path, is_boot_drive,
volume_root_path,
};
// Cross-platform broker-adoption count so `--status` can report the mode on
// any target: the real registry count on Windows, `0` (no broker) elsewhere.
#[cfg(windows)]
pub use volume::registered_broker_handle_count;
// Crate-internal: the USN journal open (FU-2b) and `$MFT` extent read (FU-3)
// adopt the same broker volume handle the MFT read uses.
#[cfg(windows)]
Expand All @@ -102,6 +106,13 @@ pub(crate) use volume::{
#[cfg(windows)]
pub use volume::{NtfsVolumeData, VolumeHandle, register_broker_handle};

/// Non-Windows: there is no Access Broker, so no adopted handles.
#[cfg(not(windows))]
#[must_use]
pub const fn registered_broker_handle_count() -> usize {
0
}

#[cfg(test)]
#[cfg(windows)]
mod tests {
Expand Down
14 changes: 14 additions & 0 deletions crates/uffs-mft/src/platform/volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ pub fn register_broker_handle(drive: super::DriveLetter, raw_handle: u64) {
}
}

/// Number of Access Broker volume handles currently registered.
///
/// The drives the daemon reads through an adopted broker handle (the zero-UAC
/// path) rather than its own elevated handle — lets `--status` report the
/// broker-adoption mode. Off Windows there is no broker, so this is always `0`.
#[cfg(windows)]
#[must_use]
pub fn registered_broker_handle_count() -> usize {
BROKER_HANDLES
.get()
.and_then(|map| map.lock().ok())
.map_or(0, |guard| guard.len())
}

/// Read (without removing) the registered broker handle for `drive`, if any.
///
/// The entry is intentionally retained for the daemon's lifetime: every
Expand Down
33 changes: 33 additions & 0 deletions crates/uffs-statusfmt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ============================================================================
# uffs-statusfmt: shared operator-status styling for UFFS
# ============================================================================
# Layer 0 leaf crate. Zero external dependencies.
#
# One visual language for every `--status` / status surface (daemon, broker,
# combined, MCP): TTY- and NO_COLOR-aware color, health glyphs, aligned
# `key: value` fields, and section headers. Keeps the human output consistent
# and scannable; the machine-readable `--json` form is modelled by each caller.
# ============================================================================

[package]
name = "uffs-statusfmt"
description = "Shared operator-status styling (color, glyphs, aligned fields) for UFFS --status surfaces"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
# Internal foundation crate — inherits the workspace `publish = false` default.
publish.workspace = true

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]

[lints]
workspace = true
Loading
Loading