Skip to content

Commit

Permalink
special behaviour on first ctrl-c, fixes #4606
Browse files Browse the repository at this point in the history
like:
 - try saving a checkpoint if borg create is ctrl-c-ed
  • Loading branch information
ThomasWaldmann committed Aug 25, 2019
1 parent 373bd8a commit 9732fe4
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 7 deletions.
21 changes: 16 additions & 5 deletions src/borg/archive.py
Expand Up @@ -40,6 +40,7 @@
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
Expand Down Expand Up @@ -1095,6 +1096,19 @@ def write_part_file(self, item, from_chunk, number):
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):
Expand All @@ -1113,17 +1127,14 @@ def chunk_processor(data):
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:
Expand Down
15 changes: 13 additions & 2 deletions src/borg/archiver.py
Expand Up @@ -71,6 +71,7 @@
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
Expand Down Expand Up @@ -531,7 +532,12 @@ def create_inner(archive, cache, fso):
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:
Expand Down Expand Up @@ -587,6 +593,10 @@ def _process(self, *, path, parent_fd=None, name=None,
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):
Expand Down Expand Up @@ -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__
Expand Down
46 changes: 46 additions & 0 deletions src/borg/helpers/process.py
Expand Up @@ -86,6 +86,52 @@ def handler(sig_no, frame):
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,
Expand Down

0 comments on commit 9732fe4

Please sign in to comment.