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

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

9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Notes:
- ³¹ rclone and restic are command-line tools; their GUIs are separate third-party projects (for example RcloneView, Backrest).
- ³² Duplicati runs as a background service with a local web UI plus a tray helper, not a native desktop app.
- ³³ restic search and selective restore are driven from the CLI (or a mounted snapshot), not an in-app browser.
- ³⁴ Driven writes daily rolling logs (pruned at 14 days / 25 MB) that interleave backend tracing with the webview's own console output, and the in-app diagnostics export bundles them.
- ³⁴ Driven writes daily rolling logs (pruned at 14 days / 25 MB, widened to 250 MB while the opt-in Debug logging toggle is on) that interleave backend tracing with the webview's own console output, and the in-app diagnostics export bundles them.
- ³⁵ rclone logs to a file only when you pass `--log-file` (rotation via `--log-file-max-size` and friends), and has no bundle export; its bug template asks you to attach a log you produced by hand.
- ³⁶ Duplicati's "Create bug report" export is a genuine one-click bundle (system info plus an obfuscated copy of the local database), and its web UI has a live log view; file logging is opt-in via `--log-file`, defaults to warnings only, and does not rotate.
- ³⁷ restic has no log-file option at all - output goes to stdout, and the only file logging is an unrotated `DEBUG_LOG` env var that its contributing guide asks you to redact yourself.
Expand Down Expand Up @@ -189,7 +189,12 @@ These move: check each project's current docs before relying on a cell.
- Rolling local log files covering both the backend and the webview console,
collected into a one-click diagnostics bundle alongside a redacted summary of
in-flight upload recovery state and a trailing window of process-memory
samples.
samples. An opt-in Debug logging toggle (Settings > Privacy & Data) raises
logs to per-file / IPC-trace / engine-state detail and widens the bundle
with an unredacted engine-state snapshot for the hardest bugs - it warns you
up front that this logs file names, full paths, and timing data, can slow
backups down, and turns itself off automatically after 24 hours; every other
part of the bundle stays redacted regardless.
- In-app auto-update with signed update manifests and a stable / dev channel
selector.
- Anonymous, opt-out telemetry (coarse counts only; never file names, paths, or
Expand Down
98 changes: 98 additions & 0 deletions src-tauri/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ pub struct AppState {
/// issue #308 (2026-08-17 follow-up): live bottleneck-classification
/// sampling runtime (the Activity dashboard's Bottleneck stat tile).
bottleneck: BottleneckRuntime,
/// issue #309: the debug-logging-mode 24h auto-off watchdog's task handle
/// and shutdown signal, so the app-quit drain joins it with no orphan
/// (mirrors [`UpdaterRuntime`]/[`IostatRuntime`]). No shared "hub" field
/// like those two - the watchdog only reads/writes the persisted settings
/// KV directly, nothing else on `AppState` needs to observe it.
debug_mode: DebugModeRuntime,
/// The ONE in-flight streaming exclusion preview
/// ([`crate::commands::exclusion_stream`]). The exclusion editor re-previews
/// on every rule edit, so without a single-slot registry a user tweaking
Expand Down Expand Up @@ -440,6 +446,22 @@ pub struct BottleneckRuntime {
shutdown: std::sync::Mutex<Option<watch::Sender<bool>>>,
}

/// issue #309: the debug-logging-mode 24h auto-off watchdog's runtime state
/// held on [`AppState`] - just the task's lifecycle slots (mirrors
/// [`UpdaterRuntime`]/[`TelemetryRuntime`]'s task+shutdown pair). No "hub"
/// field like [`BottleneckRuntime`]/[`IostatRuntime`]: the watchdog reads and
/// writes the persisted `global.debug_logging_*` settings directly via its
/// `StateRepo` handle, so there is nothing else on `AppState` for another
/// caller to read.
#[derive(Default)]
pub struct DebugModeRuntime {
/// The spawned watchdog task, behind `Option` so the shutdown drain can
/// TAKE + await it by value; `None` once drained / never spawned.
task: std::sync::Mutex<Option<JoinHandle<()>>>,
/// The shutdown signal the watchdog `select!`s on.
shutdown: std::sync::Mutex<Option<watch::Sender<bool>>>,
}

/// M9b (SPEC s16): the anonymous-telemetry runtime state held on [`AppState`].
///
/// `task` + `shutdown` track the single app-wide periodic-ping task so the quit
Expand Down Expand Up @@ -702,6 +724,7 @@ impl AppState {
updater: UpdaterRuntime::default(),
iostat: IostatRuntime::default(),
bottleneck: BottleneckRuntime::default(),
debug_mode: DebugModeRuntime::default(),
exclusion_previews: Arc::default(),
preview_tree_cache: Arc::default(),
telemetry: TelemetryRuntime::default(),
Expand Down Expand Up @@ -1104,6 +1127,44 @@ impl AppState {
.take()
}

// --- issue #309: debug-logging-mode watchdog runtime ----------------------

/// Register the spawned watchdog task + its shutdown sender so the
/// app-quit drain can stop + join it with no orphan (mirrors
/// [`Self::set_bottleneck_task`]).
pub fn set_debug_mode_task(&self, task: JoinHandle<()>, shutdown: watch::Sender<bool>) {
*self
.debug_mode
.task
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(task);
*self
.debug_mode
.shutdown
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(shutdown);
}

/// Signal the watchdog to stop and TAKE its handle so the quit drain can
/// await it. Mirrors [`Self::shutdown_bottleneck_task`].
#[must_use]
pub fn shutdown_debug_mode_task(&self) -> Option<JoinHandle<()>> {
if let Some(tx) = self
.debug_mode
.shutdown
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
{
let _ = tx.send(true);
}
self.debug_mode
.task
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
}

// --- M9b telemetry runtime (SPEC s16) ----------------------------------

/// M9b: register the spawned periodic-ping task + its shutdown sender so the
Expand Down Expand Up @@ -2119,6 +2180,43 @@ pub(crate) mod tests {
let _ = std::fs::remove_dir_all(dir);
}

#[tokio::test]
async fn debug_mode_runtime_task_and_shutdown_round_trip() {
// Issue #309: the debug-logging-mode watchdog's runtime bookkeeping.
// No hub getter to cover (unlike bottleneck/iostat) - just the
// set/shutdown task pair, mirrors
// `bottleneck_runtime_hub_task_and_shutdown_round_trip`.
let (state, dir) = temp_state().await;
let app_state = AppState::new(
state,
HashMap::new(),
RemoteMode::Fake,
default_fake_registry(),
);

// No task registered yet: shutdown is a safe no-op.
assert!(app_state.shutdown_debug_mode_task().is_none());

// Register a task that exits promptly on the shutdown signal (the
// real watchdog's own shape), then confirm shutdown signals + hands
// back the handle so the quit drain can join it.
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
let task = tokio::spawn(async move {
let _ = shutdown_rx.changed().await;
});
app_state.set_debug_mode_task(task, shutdown_tx);

let handle = app_state
.shutdown_debug_mode_task()
.expect("the just-registered task round-trips");
handle.await.unwrap();

// Taken: a second shutdown call is again a safe no-op.
assert!(app_state.shutdown_debug_mode_task().is_none());

let _ = std::fs::remove_dir_all(dir);
}

#[tokio::test]
async fn vss_helper_manager_installs_and_shutdown_is_noop() {
// Issue #25: AppState owns the least-privilege VSS helper broker manager -
Expand Down
22 changes: 22 additions & 0 deletions src-tauri/src/commands/dtos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,23 @@ pub struct GlobalSettings {
/// the unchanged V1 behaviour; `false` exempts EXACTLY the reachability
/// pause-reason family from the gate (a captive portal still pauses).
pub pause_when_offline: bool,
/// Issue #309: debug logging mode. When `true` the live `tracing` filter
/// is raised to a verbose directive (per-file activity, IPC traces, state
/// transitions, reconcile/queue decisions) and the diagnostic bundle
/// gains a debug section; both cost noticeable performance and log
/// verbose paths/timings, so this defaults `false` and auto-clears itself
/// (see [`Self::debug_logging_expires_at_ms`]). `serde(default)` so a
/// `global` blob persisted before this field still deserialises as off.
#[serde(default)]
pub debug_logging_enabled: bool,
/// Issue #309: the epoch-ms deadline [`Self::debug_logging_enabled`]
/// auto-turns-off at (set to `now + 24h` whenever the toggle is switched
/// on). Persisted (not just an in-memory timer) so a restart mid-window
/// still honours the original deadline rather than granting a fresh 24h.
/// `None` when debug logging is off. `serde(default)` for the same
/// pre-#309 backward-compat reason as the toggle itself.
#[serde(default)]
pub debug_logging_expires_at_ms: Option<i64>,
}

/// V2 schedule-window settings (DESIGN s17). Mirrors
Expand Down Expand Up @@ -1288,6 +1305,11 @@ pub struct GlobalSettingsPatch {
pub proxy_url: Option<Option<String>>,
/// See [`GlobalSettings::pause_when_offline`]. Absent = unchanged.
pub pause_when_offline: Option<bool>,
/// See [`GlobalSettings::debug_logging_enabled`]. Absent = unchanged;
/// present = set it (the backend computes/clears
/// `debug_logging_expires_at_ms` itself - that field is not directly
/// patchable from the webview).
pub debug_logging_enabled: Option<bool>,
}

/// Partial SPEC s22 `telemetry` settings.
Expand Down
Loading