Skip to content

Conversation

@ikhyunAn
Copy link
Contributor

Progress Update on non-interactive runs (#1395)

feat(cli): add periodic logging for progress in non-interactive terminals

PR Description

Fixes

#1395

Problem

When running rustic in headless environments (systemd), the progress bar is hidden, resulting in zero output for long-running operations. This makes it impossible to distinguish between a hang and a working backup.

Solution

This PR modifies ProgressBars to detect if stderr is a TTY.

  • Interactive: Retains the existing indicatif animated progress bars.
  • Non-Interactive: Switches to a PeriodicLog mode that prints a plain text status update to stderr at a fixed interval (user-provided, otherwise default to 10s.

Changes

  • Added PeriodicLog variant to ProgressType enum.
  • Implemented IsTerminal check in progress_bytes.
  • Added logic in inc() to check the elapsed time and print a log line if the interval has passed.
  • Default Behavior: If no --progress-interval is provided, non-interactive mode defaults to 10s to avoid flooding logs (unlike the 100ms default for TTYs).

Testing

  • Verified interactive mode still renders progress bars correctly.
  • Verified piped mode (cargo run ... 2>&1 | cat) produces heartbeat logs:
[INFO] starting to backup /private/tmp/rustic-demo/data ...
[INFO] backing up...: 854.0 MiB
[INFO] backing up...: 1.7 GiB
...

Foods for thought

How set_length works in rustic_core

During backup operations: rustic_core intentionally runs the size calculation in a parallel thread to avoid blocking the actual backup process:

This means:

inc() is called before set_length() during early progress updates.
set_length() is called later once the size scan completes.
self.0.length() returns None until set_length() is called

Current Fix (Graceful handling):
Handle the case where length isn't known yet by checking if let Some(len) = self.0.length() instead of using unwrap_or(0), and display progress without total when unknown.

if let Some(len) = self.0.length() {
    eprintln!("[INFO] {}: {} / {}", prefix, ByteSize(pos), ByteSize(len));
} else {
    eprintln!("[INFO] {}: {}", prefix, ByteSize(pos));
}

What needs to be tested:
Given my machine's HW limits (tested backup on 5GB), I haven't been able to run backup on files large enough for set_length to be called. Can someone test on a very large backup task to see if set_length is run and the total size is printed correctly?

@aawsome
Copy link
Member

aawsome commented Dec 18, 2025

Hi @ikhyunAn
Thanks a lot for the proposal!

I have some general remarks:

  • The current implementation focuses on backup runs and only implements progress_bytes. I'd like to have such a solution more general, i.e. for all use-cases.
  • I don't like it too much that you extended ProgressType - as there is no additional progress type, but this in fact another kind of progress. I would suggest to rename RusticProgress to something like RusticProgressInteractive or even RusticProgressIndicatif and define a new type like RusticProgressNonInteractive which also implements rustic_core::Progress. Then RusticProgress can be an enum over these 2 variants and Progress can be implemented e.g. using something like https://crates.io/crates/trait_enum.
  • By making RusticProgressNonInteractive a separate struct, we can also get rid of using indicatif::ProgressBar here - which makes this independent from indicatif and allows easier refactoring in future. This means that the struct needs to carry all needed information needed to be able to perform the progress update.
  • the handling of set_length is fine, IMO.

Can you try to overwork the PR? Please don't hesitate to ask if you need some (more) guidance!

@ikhyunAn
Copy link
Contributor Author

ikhyunAn commented Dec 19, 2025

Hi @aawsome , thank you for the detailed feedback regarding the PR. Here's a crisp breakdown of the refactoring which now uses RusticProgress as enum:

pub enum RusticProgress {
    Hidden(HiddenProgress),
    Interactive(InteractiveProgress),
    NonInteractive(NonInteractiveProgress),
}

PR Overwork

refactor: decouple progress logic and generalize non-interactive logging

Fixes

#1395

Context

Following up on @aawsome 's general remarks, the latest commit introduces a complete refactoring of the ProgressBar implementation. The goal is to decouple the "heartbeat" logging from the indicatif library for easier future refactoring.

Key Changes

1. Refactoring

  • wrapper enum RusticProgress that dispatches to specific implementations (Hidden, Interactive, NonInteractive)
  • InteractiveProgress wraps indicatif
  • NonInteractiveProgress is a standalone struct that keeps its own state (It does not use rustic_core::ProgressBar, therefore needs its own struct struct NonInteractiveState to hold state).

Addressing the remarks on using trait_enum:

  • Not feasible since it relies on dynamic dispatch, while rustic_core::Progress has methods returning Self.
  • Considered enum_dispatch, which is static, but Progress trait itself must be annotated with enum_dispatch.

2. Generalization

The logging logic supports all progress types - bytes, counter, spinner

3. Unified Factory Pattern

lines 341-347
Instantiation logic is centralized in create_progress

4. Defaults

  • Interactive: 100ms
  • Non-interactive: 10s, unless overridden by --progress-interval

Testing

Validated against the following scenarios:

  1. Interactive Regression Test
  2. Headless Backup (Bytes)
  3. Headless Check (Counter)
  4. Headless Prune (Spinner)
  5. cargo test

Copy link
Member

@aawsome aawsome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks mostly fine for me, but I have a few nitpicks ;-)

Copy link
Member

@aawsome aawsome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks @ikhyunAn

@aawsome aawsome added this pull request to the merge queue Dec 21, 2025
Merged via the queue into rustic-rs:main with commit 30963d5 Dec 21, 2025
30 checks passed
@ikhyunAn ikhyunAn deleted the feature/1395-progress-bar-on-non-interactive-runs branch December 21, 2025 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants