mirror of
https://github.com/borgbackup/borg.git
synced 2025-02-21 21:57:36 +00:00
introduce popen_with_error_handling to handle common user errors
(without tracebacks)
This commit is contained in:
parent
52fab07b3b
commit
293324810b
3 changed files with 64 additions and 3 deletions
|
@ -63,6 +63,7 @@
|
||||||
from .helpers import basic_json_data, json_print
|
from .helpers import basic_json_data, json_print
|
||||||
from .helpers import replace_placeholders
|
from .helpers import replace_placeholders
|
||||||
from .helpers import ChunkIteratorFileWrapper
|
from .helpers import ChunkIteratorFileWrapper
|
||||||
|
from .helpers import popen_with_error_handling
|
||||||
from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
|
from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
|
||||||
from .patterns import PatternMatcher
|
from .patterns import PatternMatcher
|
||||||
from .item import Item
|
from .item import Item
|
||||||
|
@ -747,9 +748,9 @@ def do_export_tar(self, args, repository, manifest, key, archive):
|
||||||
# There is no deadlock potential here (the subprocess docs warn about this), because
|
# There is no deadlock potential here (the subprocess docs warn about this), because
|
||||||
# communication with the process is a one-way road, i.e. the process can never block
|
# communication with the process is a one-way road, i.e. the process can never block
|
||||||
# for us to do something while we block on the process for something different.
|
# for us to do something while we block on the process for something different.
|
||||||
filtercmd = shlex.split(filter)
|
filterproc = popen_with_error_handling(filter, stdin=subprocess.PIPE, stdout=filterout, log_prefix='--tar-filter: ')
|
||||||
logger.debug('--tar-filter command line: %s', filtercmd)
|
if not filterproc:
|
||||||
filterproc = subprocess.Popen(filtercmd, stdin=subprocess.PIPE, stdout=filterout)
|
return EXIT_ERROR
|
||||||
# Always close the pipe, otherwise the filter process would not notice when we are done.
|
# Always close the pipe, otherwise the filter process would not notice when we are done.
|
||||||
tarstream = filterproc.stdin
|
tarstream = filterproc.stdin
|
||||||
tarstream_close = True
|
tarstream_close = True
|
||||||
|
|
|
@ -11,9 +11,11 @@
|
||||||
import platform
|
import platform
|
||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import stat
|
import stat
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
import threading
|
import threading
|
||||||
|
@ -1962,3 +1964,34 @@ def secure_erase(path):
|
||||||
fd.flush()
|
fd.flush()
|
||||||
os.fsync(fd.fileno())
|
os.fsync(fd.fileno())
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
|
def popen_with_error_handling(cmd_line: str, log_prefix='', **kwargs):
|
||||||
|
"""
|
||||||
|
Handle typical errors raised by subprocess.Popen. Return None if an error occurred,
|
||||||
|
otherwise return the Popen object.
|
||||||
|
|
||||||
|
*cmd_line* is split using shlex (e.g. 'gzip -9' => ['gzip', '-9']).
|
||||||
|
|
||||||
|
Log messages will be prefixed with *log_prefix*; if set, it should end with a space
|
||||||
|
(e.g. log_prefix='--some-option: ').
|
||||||
|
|
||||||
|
Does not change the exit code.
|
||||||
|
"""
|
||||||
|
assert not kwargs.get('shell'), 'Sorry pal, shell mode is a no-no'
|
||||||
|
try:
|
||||||
|
command = shlex.split(cmd_line)
|
||||||
|
if not command:
|
||||||
|
raise ValueError('an empty command line is not permitted')
|
||||||
|
except ValueError as ve:
|
||||||
|
logger.error('%s%s', log_prefix, ve)
|
||||||
|
return
|
||||||
|
logger.debug('%scommand line: %s', log_prefix, command)
|
||||||
|
try:
|
||||||
|
return subprocess.Popen(command, **kwargs)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error('%sexecutable not found: %s', log_prefix, command[0])
|
||||||
|
return
|
||||||
|
except PermissionError:
|
||||||
|
logger.error('%spermission denied: %s', log_prefix, command[0])
|
||||||
|
return
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from time import mktime, strptime, sleep
|
from time import mktime, strptime, sleep
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
from ..helpers import swidth_slice
|
from ..helpers import swidth_slice
|
||||||
from ..helpers import chunkit
|
from ..helpers import chunkit
|
||||||
from ..helpers import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS
|
from ..helpers import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS
|
||||||
|
from ..helpers import popen_with_error_handling
|
||||||
|
|
||||||
from . import BaseTestCase, FakeInputs
|
from . import BaseTestCase, FakeInputs
|
||||||
|
|
||||||
|
@ -816,3 +818,28 @@ def test_safe_timestamps():
|
||||||
datetime.utcfromtimestamp(beyond_y10k)
|
datetime.utcfromtimestamp(beyond_y10k)
|
||||||
assert datetime.utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2262, 1, 1)
|
assert datetime.utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2262, 1, 1)
|
||||||
assert datetime.utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2262, 1, 1)
|
assert datetime.utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2262, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPopenWithErrorHandling:
|
||||||
|
@pytest.mark.skipif(not shutil.which('test'), reason='"test" binary is needed')
|
||||||
|
def test_simple(self):
|
||||||
|
proc = popen_with_error_handling('test 1')
|
||||||
|
assert proc.wait() == 0
|
||||||
|
|
||||||
|
@pytest.mark.skipif(shutil.which('borg-foobar-test-notexist'), reason='"borg-foobar-test-notexist" binary exists (somehow?)')
|
||||||
|
def test_not_found(self):
|
||||||
|
proc = popen_with_error_handling('borg-foobar-test-notexist 1234')
|
||||||
|
assert proc is None
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('cmd', (
|
||||||
|
'mismatched "quote',
|
||||||
|
'foo --bar="baz',
|
||||||
|
''
|
||||||
|
))
|
||||||
|
def test_bad_syntax(self, cmd):
|
||||||
|
proc = popen_with_error_handling(cmd)
|
||||||
|
assert proc is None
|
||||||
|
|
||||||
|
def test_shell(self):
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
popen_with_error_handling('', shell=True)
|
||||||
|
|
Loading…
Reference in a new issue