Skip to content

Commit

Permalink
special behaviour on first ctrl-c, fixes borgbackup#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 2, 2019
1 parent e299ad8 commit 8d5661b
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 7 deletions.
19 changes: 14 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 @@ -1093,6 +1094,17 @@ 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):
if forced or sig_int or \
self.checkpoint_interval and time.monotonic() - self.last_checkpoint > self.checkpoint_interval:
if sig_int:
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:
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 @@ -1111,17 +1123,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
18 changes: 16 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:
# the user says "get out of here!"
return

try:
recurse_excluded_dir = False
if matcher.match(path):
Expand Down Expand Up @@ -4429,7 +4439,11 @@ def main(): # pragma: no cover
print(tb, file=sys.stderr)
sys.exit(e.exit_code)
try:
exit_code = archiver.run(args)
sig_int.install()
try:
exit_code = archiver.run(args)
finally:
sig_int.uninstall()
except Error as e:
msg = e.get_message()
msgid = type(e).__qualname__
Expand Down
32 changes: 32 additions & 0 deletions src/borg/helpers/process.py
Expand Up @@ -86,6 +86,38 @@ def handler(sig_no, frame):
return handler


class SigIntManager:
def __init__(self):
self.state = False
self.ctx = signal_handler('SIGINT', self.handler)

def set(self, new_state):
# set the boolean value of this object
self.state = new_state

def __bool__(self):
return self.state

def handler(self, sig_no, stack):
# handle the first ctrl-c / SIGINT.
self.uninstall()
self.set(True)

def install(self):
self.ctx.__enter__()

def uninstall(self):
# restore the original ctrl-c handler, so the next ctrl-c / SIGINT does the normal thing:
if self.ctx:
self.ctx.__exit__(None, None, None)
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 8d5661b

Please sign in to comment.