From 3e2f560a615b1d74c7ee2f3535a64f275c2d69d8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 11 Oct 2025 14:00:44 +0200 Subject: [PATCH] improved tty-less progress reporting (1.4-maint) Previously when running borg in a systemd service (and similar when piping to a file and co.), these problems occurred: - The carriage return both made it so that journald interpreted the output as binary, therefore not printing the text, while also not buffering correctly, so that log output was only available every once in a while in the form [40k blob data]. This can partially be worked around by using `journalctl -a` to view the logs, which at least prints the text, though only sporadically. - The path was getting truncated to a short length, since the default get_terminal_size returns a column width of 80, which isn't relevant when printing to e.g. journald. This commit fixes this by introducing a new code path for when stream is not a tty, which always prints the full paths and ends lines with a linefeed. This is based on unfinished PR #8939 by @infinisil, thanks for your suggestion! --- src/borg/archive.py | 15 +++++++++++++-- src/borg/testsuite/archive.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index ad17dc9be..4e334d164 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -141,9 +141,12 @@ class Statistics: def csize_fmt(self): return format_file_size(self.csize, iec=self.iec) + PROGRESS_FMT = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N ' + def show_progress(self, item=None, final=False, stream=None, dt=None): now = time.monotonic() if dt is None or now - self.last_progress > dt: + stream = stream or sys.stderr self.last_progress = now if self.output_json: if not final: @@ -158,10 +161,18 @@ class Statistics: }) msg = json.dumps(data) end = '\n' + elif not stream.isatty(): + # if we don't output to a terminal, use normal linefeeds and assume line length is unlimited + if not final: + msg = self.PROGRESS_FMT.format(self) + msg += remove_surrogates(item.path) if item else '' + else: + msg = '' + end = '\n' else: columns, lines = get_terminal_size() if not final: - msg = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N '.format(self) + msg = self.PROGRESS_FMT.format(self) path = remove_surrogates(item.path) if item else '' space = columns - swidth(msg) if space < 12: @@ -172,7 +183,7 @@ class Statistics: else: msg = ' ' * columns end = '\r' - print(msg, end=end, file=stream or sys.stderr, flush=True) + print(msg, end=end, file=stream, flush=True) def is_special(mode): diff --git a/src/borg/testsuite/archive.py b/src/borg/testsuite/archive.py index 08aef74aa..7358a0454 100644 --- a/src/borg/testsuite/archive.py +++ b/src/borg/testsuite/archive.py @@ -32,27 +32,50 @@ def test_stats_basic(stats): assert stats.usize == 10 -def tests_stats_progress(stats, monkeypatch, columns=80): +def tests_stats_progress_tty(stats, monkeypatch, columns=80): + class TTYStringIO(StringIO): + def isatty(self): + return True + monkeypatch.setenv('COLUMNS', str(columns)) - out = StringIO() + out = TTYStringIO() stats.show_progress(stream=out) s = '20 B O 10 B C 10 B D 0 N ' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" - out = StringIO() + out = TTYStringIO() stats.update(10**3, 0, unique=False) stats.show_progress(item=Item(path='foo'), final=False, stream=out) s = '1.02 kB O 10 B C 10 B D 0 N foo' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" - out = StringIO() + out = TTYStringIO() stats.show_progress(item=Item(path='foo'*40), final=False, stream=out) s = '1.02 kB O 10 B C 10 B D 0 N foofoofoofoofoofoofoofo...oofoofoofoofoofoofoofoofoo' buf = ' ' * (columns - len(s)) assert out.getvalue() == s + buf + "\r" +def tests_stats_progress_file(stats, monkeypatch): + out = StringIO() + stats.show_progress(stream=out) + s = '20 B O 10 B C 10 B D 0 N ' + assert out.getvalue() == s + "\n" + + out = StringIO() + stats.update(10**3, 0, unique=False) + path = 'foo' + stats.show_progress(item=Item(path=path), final=False, stream=out) + s = f'1.02 kB O 10 B C 10 B D 0 N {path}' + assert out.getvalue() == s + "\n" + out = StringIO() + path = 'foo' * 40 + stats.show_progress(item=Item(path=path), final=False, stream=out) + s = f'1.02 kB O 10 B C 10 B D 0 N {path}' + assert out.getvalue() == s + "\n" + + def test_stats_format(stats): assert str(stats) == """\ This archive: 20 B 10 B 10 B"""