Browse Source

Merge pull request #4635 from ThomasWaldmann/ctrlc-checkpoint

first ctrl-c: checkpoint and abort, fixes #4606
TW 5 years ago
parent
commit
aa7df50a2d
3 changed files with 75 additions and 7 deletions
  1. 16 5
      src/borg/archive.py
  2. 13 2
      src/borg/archiver.py
  3. 46 0
      src/borg/helpers/process.py

+ 16 - 5
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
@@ -1096,6 +1097,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):
@@ -1114,17 +1128,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:

+ 13 - 2
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__

+ 46 - 0
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,