Repository: borgbackup/borg

Issue:
early uncaught exceptions are reported by python as exit code 1 which borg defines as "warning".
If an exception happens before the try: blocks in archiver.py: main() python will terminate the process with exit code 1. But this exit code is defined as "warning" by borg, thus a hard error is misdetected as just an warning by software automating borg backups.

This mostly means all exceptions that are raised while importing code.

In our concrete case it was caused by a missing dependency on liblz4 (in our custom packaging), other kind of broken installations will have similar effects.


Discussion:
idea:
- import all critical stuff early in archiver.py
- try/except these imports
- in except block: output traceback, sys.exit(2)

Resolving patch:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -1,83 +1,94 @@
-import argparse
-import collections
-import configparser
-import faulthandler
-import functools
-import hashlib
-import inspect
-import itertools
-import json
-import logging
-import os
-import re
-import shlex
-import shutil
-import signal
-import stat
-import subprocess
+# borg cli interface / toplevel archiver code
+
 import sys
-import tarfile
-import textwrap
-import time
 import traceback
-from binascii import unhexlify
-from contextlib import contextmanager
-from datetime import datetime, timedelta
-from itertools import zip_longest
-
-from .logger import create_logger, setup_logging
-
-logger = create_logger()
-
-import msgpack
-
-import borg
-from . import __version__
-from . import helpers
-from . import shellpattern
-from .algorithms.checksums import crc32
-from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special
-from .archive import BackupError, BackupOSError, backup_io
-from .cache import Cache, assert_secure, SecurityManager
-from .constants import *  # NOQA
-from .compress import CompressionSpec
-from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required, RepoKey, PassphraseKey
-from .crypto.keymanager import KeyManager
-from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
-from .helpers import Error, NoManifestError, set_ec
-from .helpers import positive_int_validator, location_validator, archivename_validator, ChunkerParams, Location
-from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS, FilesCacheMode
-from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter
-from .helpers import format_timedelta, format_file_size, parse_file_size, format_archive
-from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict
-from .helpers import interval, prune_within, prune_split
-from .helpers import timestamp
-from .helpers import get_cache_dir
-from .helpers import Manifest
-from .helpers import hardlinkable
-from .helpers import StableDict
-from .helpers import check_python, check_extension_modules
-from .helpers import dir_is_tagged, is_slow_msgpack, is_supported_msgpack, yes, sysinfo
-from .helpers import log_multi
-from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm
-from .helpers import ErrorIgnoringTextIOWrapper
-from .helpers import ProgressIndicatorPercent
-from .helpers import basic_json_data, json_print
-from .helpers import replace_placeholders
-from .helpers import ChunkIteratorFileWrapper
-from .helpers import popen_with_error_handling, prepare_subprocess_env
-from .helpers import dash_open
-from .helpers import umount
-from .nanorst import rst_to_terminal
-from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
-from .patterns import PatternMatcher
-from .item import Item
-from .platform import get_flags, get_process_id, SyncFile
-from .remote import RepositoryServer, RemoteRepository, cache_if_remote
-from .repository import Repository, LIST_SCAN_LIMIT, TAG_PUT, TAG_DELETE, TAG_COMMIT
-from .selftest import selftest
-from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader
 
+try:
+    import argparse
+    import collections
+    import configparser
+    import faulthandler
+    import functools
+    import hashlib
+    import inspect
+    import itertools
+    import json
+    import logging
+    import os
+    import re
+    import shlex
+    import shutil
+    import signal
+    import stat
+    import subprocess
+    import tarfile
+    import textwrap
+    import time
+    from binascii import unhexlify
+    from contextlib import contextmanager
+    from datetime import datetime, timedelta
+    from itertools import zip_longest
+
+    from .logger import create_logger, setup_logging
+
+    logger = create_logger()
+
+    import msgpack
+
+    import borg
+    from . import __version__
+    from . import helpers
+    from . import shellpattern
+    from .algorithms.checksums import crc32
+    from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics, is_special
+    from .archive import BackupError, BackupOSError, backup_io
+    from .cache import Cache, assert_secure, SecurityManager
+    from .constants import *  # NOQA
+    from .compress import CompressionSpec
+    from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required, RepoKey, PassphraseKey
+    from .crypto.keymanager import KeyManager
+    from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
+    from .helpers import Error, NoManifestError, set_ec
+    from .helpers import positive_int_validator, location_validator, archivename_validator, ChunkerParams, Location
+    from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS, FilesCacheMode
+    from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter
+    from .helpers import format_timedelta, format_file_size, parse_file_size, format_archive
+    from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict
+    from .helpers import interval, prune_within, prune_split
+    from .helpers import timestamp
+    from .helpers import get_cache_dir
+    from .helpers import Manifest
+    from .helpers import hardlinkable
+    from .helpers import StableDict
+    from .helpers import check_python, check_extension_modules
+    from .helpers import dir_is_tagged, is_slow_msgpack, is_supported_msgpack, yes, sysinfo
+    from .helpers import log_multi
+    from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm
+    from .helpers import ErrorIgnoringTextIOWrapper
+    from .helpers import ProgressIndicatorPercent
+    from .helpers import basic_json_data, json_print
+    from .helpers import replace_placeholders
+    from .helpers import ChunkIteratorFileWrapper
+    from .helpers import popen_with_error_handling, prepare_subprocess_env
+    from .helpers import dash_open
+    from .helpers import umount
+    from .nanorst import rst_to_terminal
+    from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
+    from .patterns import PatternMatcher
+    from .item import Item
+    from .platform import get_flags, get_process_id, SyncFile
+    from .remote import RepositoryServer, RemoteRepository, cache_if_remote
+    from .repository import Repository, LIST_SCAN_LIMIT, TAG_PUT, TAG_DELETE, TAG_COMMIT
+    from .selftest import selftest
+    from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader
+except BaseException:
+    # an unhandled exception in the try-block would cause the borg cli command to exit with rc 1 due to python's
+    # default behavior, see issue #4424.
+    # as borg defines rc 1 as WARNING, this would be a mismatch, because a crash should be an ERROR (rc 2).
+    traceback.print_exc()
+    sys.exit(2)  # == EXIT_ERROR
+
+assert EXIT_ERROR == 2, "EXIT_ERROR is not 2, as expected - fix assert AND exception handler right above this line."
 
 STATS_HEADER = "                       Original size      Compressed size    Deduplicated size"
 


Test patch: