From 9732fe4965316b46f60156ef4d8d61413a880a60 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 22 Jun 2019 23:19:37 +0200 Subject: [PATCH] special behaviour on first ctrl-c, fixes #4606 like: - try saving a checkpoint if borg create is ctrl-c-ed --- src/borg/archive.py | 21 +++++++++++++---- src/borg/archiver.py | 15 ++++++++++-- src/borg/helpers/process.py | 46 +++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 86e8a62e0..76d45ffd2 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -40,6 +40,7 @@ from .helpers import safe_ns from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import os_open, flags_normal from .helpers import msgpack +from .helpers import sig_int from .patterns import PathPrefixPattern, FnmatchPattern, IECommand from .item import Item, ArchiveItem, ItemDiff from .platform import acl_get, acl_set, set_flags, get_flags, swidth, hostname @@ -1095,6 +1096,19 @@ class ChunksProcessor: self.write_checkpoint() return length, number + def maybe_checkpoint(self, item, from_chunk, part_number, forced=False): + sig_int_triggered = sig_int and sig_int.action_triggered() + if forced or sig_int_triggered or \ + self.checkpoint_interval and time.monotonic() - self.last_checkpoint > self.checkpoint_interval: + if sig_int_triggered: + logger.info('checkpoint requested: starting checkpoint creation...') + from_chunk, part_number = self.write_part_file(item, from_chunk, part_number) + self.last_checkpoint = time.monotonic() + if sig_int_triggered: + sig_int.action_completed() + logger.info('checkpoint requested: finished checkpoint creation!') + return from_chunk, part_number + def process_file_chunks(self, item, cache, stats, show_progress, chunk_iter, chunk_processor=None): if not chunk_processor: def chunk_processor(data): @@ -1113,17 +1127,14 @@ class ChunksProcessor: item.chunks.append(chunk_processor(data)) if show_progress: stats.show_progress(item=item, dt=0.2) - if self.checkpoint_interval and time.monotonic() - self.last_checkpoint > self.checkpoint_interval: - from_chunk, part_number = self.write_part_file(item, from_chunk, part_number) - self.last_checkpoint = time.monotonic() + from_chunk, part_number = self.maybe_checkpoint(item, from_chunk, part_number, forced=False) else: if part_number > 1: if item.chunks[from_chunk:]: # if we already have created a part item inside this file, we want to put the final # chunks (if any) into a part item also (so all parts can be concatenated to get # the complete file): - from_chunk, part_number = self.write_part_file(item, from_chunk, part_number) - self.last_checkpoint = time.monotonic() + from_chunk, part_number = self.maybe_checkpoint(item, from_chunk, part_number, forced=True) # if we created part files, we have referenced all chunks from the part files, # but we also will reference the same chunks also from the final, complete file: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f1f406f52..583dc2fbd 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -71,6 +71,7 @@ try: from .helpers import umount from .helpers import flags_root, flags_dir, flags_special_follow, flags_special from .helpers import msgpack + from .helpers import sig_int from .nanorst import rst_to_terminal from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern from .patterns import PatternMatcher @@ -531,7 +532,12 @@ class Archiver: if args.progress: archive.stats.show_progress(final=True) archive.stats += fso.stats - archive.save(comment=args.comment, timestamp=args.timestamp, stats=archive.stats) + if sig_int: + # do not save the archive if the user ctrl-c-ed - it is valid, but incomplete. + # we already have a checkpoint archive in this case. + self.print_error("Got Ctrl-C / SIGINT.") + else: + archive.save(comment=args.comment, timestamp=args.timestamp, stats=archive.stats) args.stats |= args.json if args.stats: if args.json: @@ -587,6 +593,10 @@ class Archiver: This should only raise on critical errors. Per-item errors must be handled within this method. """ + if sig_int and sig_int.action_done(): + # the user says "get out of here!" and we have already completed the desired action. + return + try: recurse_excluded_dir = False if matcher.match(path): @@ -4431,7 +4441,8 @@ def main(): # pragma: no cover print(tb, file=sys.stderr) sys.exit(e.exit_code) try: - exit_code = archiver.run(args) + with sig_int: + exit_code = archiver.run(args) except Error as e: msg = e.get_message() msgid = type(e).__qualname__ diff --git a/src/borg/helpers/process.py b/src/borg/helpers/process.py index 44b49892e..763a8c68b 100644 --- a/src/borg/helpers/process.py +++ b/src/borg/helpers/process.py @@ -86,6 +86,52 @@ def raising_signal_handler(exc_cls): return handler +class SigIntManager: + def __init__(self): + self._sig_int_triggered = False + self._action_triggered = False + self._action_done = False + self.ctx = signal_handler('SIGINT', self.handler) + + def __bool__(self): + # this will be True (and stay True) after the first Ctrl-C/SIGINT + return self._sig_int_triggered + + def action_triggered(self): + # this is True to indicate that the action shall be done + return self._action_triggered + + def action_done(self): + # this will be True after the action has completed + return self._action_done + + def action_completed(self): + # this must be called when the action triggered is completed, + # to avoid that the action is repeatedly triggered. + self._action_triggered = False + self._action_done = True + + def handler(self, sig_no, stack): + # handle the first ctrl-c / SIGINT. + self.__exit__(None, None, None) + self._sig_int_triggered = True + self._action_triggered = True + + def __enter__(self): + self.ctx.__enter__() + + def __exit__(self, exception_type, exception_value, traceback): + # restore the original ctrl-c handler, so the next ctrl-c / SIGINT does the normal thing: + if self.ctx: + self.ctx.__exit__(exception_type, exception_value, traceback) + self.ctx = None + + +# global flag which might trigger some special behaviour on first ctrl-c / SIGINT, +# e.g. if this is interrupting "borg create", it shall try to create a checkpoint. +sig_int = SigIntManager() + + def popen_with_error_handling(cmd_line: str, log_prefix='', **kwargs): """ Handle typical errors raised by subprocess.Popen. Return None if an error occurred,