# borg cli interface / toplevel archiver code

import sys
import traceback

# quickfix to disallow running borg with assertions switched off
try:
    assert False
except AssertionError:
    pass  # OK
else:
    print(
        "Borg requires working assertions. Please run Python without -O and/or unset PYTHONOPTIMIZE.", file=sys.stderr
    )
    sys.exit(2)  # == EXIT_ERROR

try:
    import faulthandler
    import functools
    import inspect
    import itertools
    import json
    import logging
    import os
    import shlex
    import signal
    from datetime import UTC, datetime

    from ..logger import create_logger, setup_logging

    logger = create_logger()

    from ._common import Highlander
    from .. import __version__
    from ..constants import *  # NOQA
    from ..helpers import EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
    from ..helpers import Error, CommandError, get_ec, modern_ec
    from ..helpers import add_warning, BorgWarning, BackupWarning
    from ..helpers import format_file_size
    from ..helpers import remove_surrogates, text_to_json
    from ..helpers import DatetimeWrapper, replace_placeholders
    from ..helpers.argparsing import flatten_namespace, ArgumentTypeError, ArgumentParser, SUPPRESS
    from ..helpers import is_slow_msgpack, is_supported_msgpack, sysinfo
    from ..helpers import signal_handler, raising_signal_handler, SigHup, SigTerm
    from ..helpers import ErrorIgnoringTextIOWrapper
    from ..helpers import msgpack
    from ..helpers import sig_int
    from ..helpers import get_config_dir
    from ..platformflags import is_msystem
    from ..legacy.remote import LegacyRemoteRepository
    from ..selftest import selftest
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    Deduplicated size"

PURE_PYTHON_MSGPACK_WARNING = "Using a pure-python msgpack! This will result in lower performance."


from .analyze_cmd import AnalyzeMixIn
from .benchmark_cmd import BenchmarkMixIn
from .check_cmd import CheckMixIn
from .compact_cmd import CompactMixIn
from .completion_cmd import CompletionMixIn
from .create_cmd import CreateMixIn
from .debug_cmd import DebugMixIn
from .delete_cmd import DeleteMixIn
from .diff_cmd import DiffMixIn
from .extract_cmd import ExtractMixIn
from .find_cmd import FindMixIn
from .help_cmd import HelpMixIn
from .info_cmd import InfoMixIn
from .key_cmds import KeysMixIn
from .list_cmd import ListMixIn
from .lock_cmds import LocksMixIn
from .mount_cmds import MountMixIn
from .prune_cmd import PruneMixIn
from .repo_compress_cmd import RepoCompressMixIn
from .recreate_cmd import RecreateMixIn
from .rename_cmd import RenameMixIn
from .repo_create_cmd import RepoCreateMixIn
from .repo_info_cmd import RepoInfoMixIn
from .repo_delete_cmd import RepoDeleteMixIn
from .repo_list_cmd import RepoListMixIn
from .repo_space_cmd import RepoSpaceMixIn
from .serve_cmd import ServeMixIn
from .tag_cmd import TagMixIn
from .tar_cmds import TarMixIn
from .transfer_cmd import TransferMixIn
from .undelete_cmd import UnDeleteMixIn
from .version_cmd import VersionMixIn
from .webdav_cmd import WebDAVMixIn


class Archiver(
    AnalyzeMixIn,
    BenchmarkMixIn,
    CheckMixIn,
    CompactMixIn,
    CompletionMixIn,
    CreateMixIn,
    DebugMixIn,
    DeleteMixIn,
    DiffMixIn,
    ExtractMixIn,
    FindMixIn,
    HelpMixIn,
    InfoMixIn,
    KeysMixIn,
    ListMixIn,
    LocksMixIn,
    MountMixIn,
    PruneMixIn,
    RecreateMixIn,
    RenameMixIn,
    RepoCompressMixIn,
    RepoCreateMixIn,
    RepoDeleteMixIn,
    RepoInfoMixIn,
    RepoListMixIn,
    RepoSpaceMixIn,
    ServeMixIn,
    TagMixIn,
    TarMixIn,
    TransferMixIn,
    UnDeleteMixIn,
    VersionMixIn,
    WebDAVMixIn,
):
    def __init__(self, lock_wait=None, prog=None):
        self.lock_wait = lock_wait
        self.prog = prog
        self.start_backup = None

    def print_warning(self, msg, *args, **kw):
        warning_code = kw.get("wc", EXIT_WARNING)  # note: wc=None can be used to not influence exit code
        warning_type = kw.get("wt", "percent")
        assert warning_type in ("percent", "curly")
        warning_msgid = kw.get("msgid")
        if warning_code is not None:
            add_warning(msg, *args, wc=warning_code, wt=warning_type)
        if warning_type == "percent":
            output = args and msg % args or msg
        else:  # == "curly"
            output = args and msg.format(*args) or msg
        logger.warning(output, msgid=warning_msgid) if warning_msgid else logger.warning(output)

    def print_warning_instance(self, warning):
        assert isinstance(warning, BorgWarning)
        # if it is a BackupWarning, use the wrapped BackupError exception instance:
        cls = type(warning.args[1]) if isinstance(warning, BackupWarning) else type(warning)
        msg, msgid, args, wc = cls.__doc__, cls.__qualname__, warning.args, warning.exit_code
        self.print_warning(msg, *args, wc=wc, wt="curly", msgid=msgid)

    def print_file_status(self, status, path):
        # if we get called with status == None, the final file status was already printed
        if self.output_list and status is not None and (self.output_filter is None or status in self.output_filter):
            if self.log_json:
                json_data = {"type": "file_status", "status": status}
                json_data |= text_to_json("path", path)
                print(json.dumps(json_data), file=sys.stderr)
            else:
                logging.getLogger("borg.output.list").info("%1s %s", status, remove_surrogates(path))

    def preprocess_args(self, args):
        deprecations = [
            # ('--old', '--new' or None, 'Warning: "--old" has been deprecated. Use "--new" instead.'),
        ]
        for i, arg in enumerate(args[:]):
            for old_name, new_name, warning in deprecations:
                # either --old_name or --old_name=...
                if arg == old_name or (arg.startswith(old_name) and arg[len(old_name)] == "="):
                    if new_name is not None:
                        args[i] = arg.replace(old_name, new_name)
                    print(warning, file=sys.stderr)
        return args

    class CommonOptions:
        """
        Support class to allow specifying common options at multiple levels of the command hierarchy.

        Common options (e.g. --log-level, --repo) can be placed anywhere in the command line:

            borg --info create ...          # before the subcommand
            borg create --info ...          # after the subcommand
            borg --info debug info --debug  # at both levels of a two-level command

        Each parser level registers the same options with the same dest names.
        Defaults are only provided on the top-level parser; all sub-parsers use SUPPRESS so
        that unset options don't appear in the namespace at all.

        flatten_namespace() handles precedence: it walks sub-namespaces depth-first, so the
        most-specific (innermost) value wins.  For append-action options (e.g. --debug-topic)
        it merges lists from all levels.
        """

        def __init__(self, define_common_options):
            """
            *define_common_options* should be a callable taking one argument, which
            will be an argparse.Parser.add_argument-like function.

            *define_common_options* will be called multiple times, and should call
            the passed function to define common options exactly the same way each time.
            """
            self.define_common_options = define_common_options
            # This is the sentinel object that replaces all default values in parsers
            # below the top-level parser.
            self.default_sentinel = object()

        def add_common_group(self, parser, provide_defaults=False):
            """
            Add common options to *parser*.

            *provide_defaults* must be True exactly once in a parser hierarchy (the top-level
            parser) and False on all sub-parsers.  Sub-parsers get SUPPRESS as the default so
            that an unspecified option produces no attribute, leaving the top-level default intact
            after flatten_namespace() merges the namespaces.
            """

            def add_argument(*args, **kwargs):
                if "dest" in kwargs:
                    kwargs.setdefault("action", "store")
                    assert kwargs["action"] in (
                        Highlander,
                        "help",
                        "store_const",
                        "store_true",
                        "store_false",
                        "store",
                        "append",
                    )
                    is_append = kwargs["action"] == "append"
                    if not provide_defaults:
                        # Interpolate help now, in case %(default)d (or similar) is mentioned,
                        # to avoid producing incorrect help output.
                        kwargs["help"] = kwargs["help"] % kwargs
                        if not is_append:
                            kwargs["default"] = SUPPRESS

                common_group.add_argument(*args, **kwargs)

            common_group = parser.add_argument_group("Common options")
            self.define_common_options(add_argument)

    def build_parser(self):
        from ._common import define_common_options, process_epilog

        additional_help = process_epilog(
            """
        Description of additional help topics:

        Use ``borg help <topic>`` to get help on:

          patterns            selection patterns for including/excluding paths
          match-archives      selection patterns for matching archives
          placeholders        placeholders in repository URLs, archive names, etc.
          compression         options and specifications for data compression
          environment         environment variables used by borg (short: env)
        """
        )
        parser = ArgumentParser(
            prog=self.prog,
            description="Borg - Deduplicated Backups",
            default_config_files=[os.path.join(get_config_dir(), "default.yaml")],
            default_env=True,
            env_prefix="BORG",
            epilog=additional_help,
        )
        parser.add_argument("--config", action="config")
        # paths and patterns must have an empty list as default everywhere
        parser.common_options = self.CommonOptions(define_common_options)
        parser.add_argument(
            "-V", "--version", action="version", version="%(prog)s " + __version__, help="show version number and exit"
        )
        parser.add_argument("--cockpit", dest="cockpit", action="store_true", help="Start the Borg TUI")
        parser.common_options.add_common_group(parser, provide_defaults=True)

        common_parser = ArgumentParser(prog=self.prog)
        parser.common_options.add_common_group(common_parser)

        mid_common_parser = ArgumentParser(prog=self.prog)
        parser.common_options.add_common_group(mid_common_parser)

        if parser.prog == "borgfs":
            return self.build_parser_borgfs(parser)

        subparsers = parser.add_subcommands(required=False, title="required arguments", metavar="<command>")

        self.build_parser_analyze(subparsers, common_parser, mid_common_parser)
        self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser)
        self.build_parser_check(subparsers, common_parser, mid_common_parser)
        self.build_parser_compact(subparsers, common_parser, mid_common_parser)
        self.build_parser_completion(subparsers, common_parser, mid_common_parser)
        self.build_parser_create(subparsers, common_parser, mid_common_parser)
        self.build_parser_debug(subparsers, common_parser, mid_common_parser)
        self.build_parser_delete(subparsers, common_parser, mid_common_parser)
        self.build_parser_diff(subparsers, common_parser, mid_common_parser)
        self.build_parser_extract(subparsers, common_parser, mid_common_parser)
        self.build_parser_find(subparsers, common_parser, mid_common_parser)
        self.build_parser_help(subparsers, common_parser, mid_common_parser, parser)
        self.build_parser_info(subparsers, common_parser, mid_common_parser)
        self.build_parser_keys(subparsers, common_parser, mid_common_parser)
        self.build_parser_list(subparsers, common_parser, mid_common_parser)
        self.build_parser_locks(subparsers, common_parser, mid_common_parser)
        self.build_parser_mount_umount(subparsers, common_parser, mid_common_parser)
        self.build_parser_prune(subparsers, common_parser, mid_common_parser)
        self.build_parser_repo_compress(subparsers, common_parser, mid_common_parser)
        self.build_parser_repo_create(subparsers, common_parser, mid_common_parser)
        self.build_parser_repo_delete(subparsers, common_parser, mid_common_parser)
        self.build_parser_repo_info(subparsers, common_parser, mid_common_parser)
        self.build_parser_repo_list(subparsers, common_parser, mid_common_parser)
        self.build_parser_recreate(subparsers, common_parser, mid_common_parser)
        self.build_parser_rename(subparsers, common_parser, mid_common_parser)
        self.build_parser_repo_space(subparsers, common_parser, mid_common_parser)
        self.build_parser_serve(subparsers, common_parser, mid_common_parser)
        self.build_parser_tag(subparsers, common_parser, mid_common_parser)
        self.build_parser_tar(subparsers, common_parser, mid_common_parser)
        self.build_parser_transfer(subparsers, common_parser, mid_common_parser)
        self.build_parser_undelete(subparsers, common_parser, mid_common_parser)
        self.build_parser_version(subparsers, common_parser, mid_common_parser)
        self.build_parser_webdav(subparsers, common_parser, mid_common_parser)
        return parser

    def get_args(self, argv, cmd):
        """Usually just returns argv, except when dealing with an SSH forced command for borg serve."""
        result = self.parse_args(argv[1:])
        if cmd is not None and result.func == self.do_serve:
            # borg serve case:
            # - "result" is how borg got invoked (e.g. via forced command from authorized_keys),
            # - "client_result" (from "cmd") refers to the command the client wanted to execute,
            #   which might be different in the case of a forced command or same otherwise.
            client_argv = shlex.split(cmd)
            # Drop environment variables (do *not* interpret them) before trying to parse
            # the borg command line.
            client_argv = list(itertools.dropwhile(lambda arg: "=" in arg, client_argv))
            client_result = self.parse_args(client_argv[1:])
            if client_result.func == result.func:
                # make sure we only process like normal if the client is executing
                # the same command as specified in the forced command, otherwise
                # just skip this block and return the forced command (== result).
                # client is allowed to specify the allowlisted options,
                # everything else comes from the forced "borg serve" command (or the defaults).
                # stuff from denylist must never be used from the client.
                denylist = {"restrict_to_paths", "restrict_to_repositories", "umask", "permissions"}
                # "backend" is given by the client to the REST server to construct a posixfs backend
                # that shall be used as a repository (borg serve --rest --backend FILE:<path>).
                # Like the legacy repository path (transmitted via the RPC protocol),
                # the client chooses *which* repository it wants to use; the ssh forced command pins the
                # restrictions, and do_serve_rest validates the client backend against restrict_to_paths
                # and restrict_to_repositories.
                # The --rest mode flag itself is intentionally NOT allowlisted, so the forced command
                # keeps pinning the mode (legacy/rpc vs. non-legacy/rest).
                allowlist = {"debug_topics", "lock_wait", "log_level", "backend"}
                not_present = object()
                for attr_name in allowlist:
                    assert attr_name not in denylist, "allowlist has denylisted attribute name %s" % attr_name
                    value = getattr(client_result, attr_name, not_present)
                    if value is not not_present:
                        # note: it is not possible to specify a allowlisted option via a forced command,
                        # it always gets overridden by the value specified (or defaulted to) by the client command.
                        setattr(result, attr_name, value)

        return result

    def parse_args(self, args=None):
        if args:
            args = self.preprocess_args(args)
        parser = self.build_parser()
        args = parser.parse_args(args or ["-h"])
        args = flatten_namespace(args)

        # Ensure list defaults previously handled by set_defaults are present
        for list_attr in ("paths", "patterns", "pattern_roots"):
            if getattr(args, list_attr, None) is None:
                setattr(args, list_attr, [])

        func = self.get_func(args, parser)
        if func == self.do_create and args.paths and args.paths_from_stdin:
            parser.error("Must not pass PATH with --paths-from-stdin.")
        if args.progress and getattr(args, "output_list", False) and not args.log_json:
            parser.error("Options --progress and --list do not play nicely together.")
        if func == self.do_create and not args.paths:
            if args.content_from_command or args.paths_from_command:
                parser.error("No command given.")
            elif not args.paths_from_stdin and not args.pattern_roots:
                parser.error("Need at least one PATH argument.")
        # we can only have a complete knowledge of placeholder replacements we should do **after** arg parsing,
        # e.g. due to options like --timestamp that override the current time.
        # thus we have to initialize replace_placeholders here and process all args that need placeholder replacement.
        if getattr(args, "timestamp", None):
            replace_placeholders.override("now", DatetimeWrapper(args.timestamp))
            replace_placeholders.override("utcnow", DatetimeWrapper(args.timestamp.astimezone(UTC)))
            args.location = args.location.with_timestamp(args.timestamp)
        for name in "name", "other_name", "newname", "comment":
            value = getattr(args, name, None)
            if value is not None:
                setattr(args, name, replace_placeholders(value))
        for name in ("match_archives",):  # lists
            value = getattr(args, name, None)
            if value:
                setattr(args, name, [replace_placeholders(elem) for elem in value])

        args.func = func

        return args

    def get_func(self, args, parser):
        if not getattr(args, "subcommand", None):
            return functools.partial(self.do_maincommand_help, parser)

        method_name = "do_" + args.subcommand.replace(" ", "_").replace("-", "_")
        func = getattr(self, method_name, None)
        if func is not None:
            if method_name == "do_help":
                return functools.partial(func, parser)
            return func

        # fallback to general help for e.g., "borg key"
        return functools.partial(self.do_maincommand_help, parser)

    def prerun_checks(self, logger, is_serve):
        if (
            not is_serve
            and is_msystem
            and ("MSYS2_ARG_CONV_EXCL" not in os.environ or "MSYS2_ENV_CONV_EXCL" not in os.environ)
        ):
            logger.warning(
                "MSYS2 path translation is active. This can cause POSIX paths to be mangled into "
                "Windows paths in archives. Consider setting MSYS2_ARG_CONV_EXCL='*' and "
                "MSYS2_ENV_CONV_EXCL='*'. See https://www.msys2.org/docs/filesystem-paths/ for details."
            )
        selftest(logger)

    def _setup_implied_logging(self, args):
        """Turn on INFO-level logging for arguments that imply they will produce output."""
        # map of option name to name of logger for that option
        option_logger = {
            "show_version": "borg.output.show-version",
            "show_rc": "borg.output.show-rc",
            "stats": "borg.output.stats",
            "progress": "borg.output.progress",
        }
        for option, logger_name in option_logger.items():
            option_set = args.get(option, False)
            logging.getLogger(logger_name).setLevel("INFO" if option_set else "WARN")

        # special-case --list / --list-kept / --list-pruned as they all work on same logger
        options = [args.get(name, False) for name in ("output_list", "list_kept", "list_pruned")]
        logging.getLogger("borg.output.list").setLevel("INFO" if any(options) else "WARN")

    def _setup_topic_debugging(self, args):
        """Turn on DEBUG level logging for specified --debug-topics."""
        for topic in args.debug_topics:
            if "." not in topic:
                topic = "borg.debug." + topic
            logger.debug("Enabling debug topic %s", topic)
            logging.getLogger(topic).setLevel("DEBUG")

    def run(self, args):
        os.umask(args.umask)  # early, before opening files
        self.lock_wait = args.lock_wait
        func = args.func
        # do not use loggers before this!
        is_serve = func == self.do_serve
        self.log_json = args.log_json and not is_serve
        func_name = getattr(func, "__name__", "none")
        setup_logging(level=args.log_level, is_serve=is_serve, log_json=self.log_json, func=func_name)
        args.progress |= is_serve
        self._setup_implied_logging(vars(args))
        self._setup_topic_debugging(args)
        # extract --dry-run reads, decrypts and decompresses every object, so its store stats are accurate.
        # create --dry-run counts the files and sums up their sizes from file system metadata.
        stats_supported_with_dry_run = func_name in ("do_extract", "do_create")
        if getattr(args, "stats", False) and getattr(args, "dry_run", False) and not stats_supported_with_dry_run:
            logger.warning("Ignoring --stats. It is not supported when using --dry-run.")
            args.stats = False
        if args.show_version:
            logging.getLogger("borg.output.show-version").info("borgbackup version %s" % __version__)
        self.prerun_checks(logger, is_serve)
        if not is_supported_msgpack():
            logger.error("You do not have a supported version of the msgpack python package installed. Terminating.")
            logger.error("This should never happen as specific, supported versions are required by our pyproject.toml.")
            logger.error("Do not contact borgbackup support about this.")
            raise Error("unsupported msgpack version")
        if is_slow_msgpack():
            logger.warning(PURE_PYTHON_MSGPACK_WARNING)
        debug_profile = os.environ.get("BORG_DEBUG_PROFILE")
        if debug_profile:
            # Import only when needed - avoids a further increase in startup time
            import cProfile
            import marshal

            logger.debug("Writing execution profile to %s", debug_profile)
            # Open the file early, before running the main program, to avoid
            # a very late crash in case the specified path is invalid.
            with open(debug_profile, "wb") as fd:
                profiler = cProfile.Profile()
                variables = dict(locals())
                profiler.enable()
                try:
                    return get_ec(func(args))
                finally:
                    profiler.disable()
                    profiler.snapshot_stats()
                    if debug_profile.endswith(".pyprof"):
                        marshal.dump(profiler.stats, fd)
                    else:
                        # We use msgpack here instead of the marshal module used by cProfile itself,
                        # because the latter is insecure. Since these files may be shared over the
                        # internet we don't want a format that is impossible to interpret outside
                        # an insecure implementation.
                        # See "borg debug convert-profile" for turning such a msgpack file
                        # into a marshal file that can be read by e.g. pyprof2calltree.
                        # For local use it's unnecessary hassle, though, that's why .pyprof makes
                        # it compatible (see above).
                        msgpack.pack(profiler.stats, fd, use_bin_type=True)
        else:
            rc = func(args)
            assert rc is None
            return get_ec(rc)


def sig_info_handler(sig_no, stack):  # pragma: no cover
    """Search the stack for information about the currently processed file and print it."""
    with signal_handler(sig_no, signal.SIG_IGN):
        for frame in inspect.getouterframes(stack):
            func, loc = frame[3], frame[0].f_locals
            if func in ("process_file", "_rec_walk"):  # create op
                path = loc["path"]
                try:
                    pos = loc["fd"].tell()
                    total = loc["st"].st_size
                except Exception:
                    pos, total = 0, 0
                logger.info(f"{path} {format_file_size(pos)}/{format_file_size(total)}")
                break
            if func in ("extract_item",):  # extract op
                path = loc["item"].path
                try:
                    pos = loc["fd"].tell()
                except Exception:
                    pos = 0
                logger.info(f"{path} {format_file_size(pos)}/???")
                break


def sig_trace_handler(sig_no, stack):  # pragma: no cover
    print("\nReceived SIGUSR2 at %s, dumping trace..." % datetime.now().replace(microsecond=0), file=sys.stderr)
    faulthandler.dump_traceback()


def format_tb(exc):
    qualname = type(exc).__qualname__
    remote = isinstance(exc, LegacyRemoteRepository.RPCError)
    if remote:
        prefix = "Borg server: "
        trace_back = "\n".join(prefix + line for line in exc.exception_full.splitlines())
        sys_info = "\n".join(prefix + line for line in exc.sysinfo.splitlines())
    else:
        trace_back = traceback.format_exc()
        sys_info = sysinfo()
    result = f"""
Error:

{qualname}: {exc}

If reporting bugs, please include the following:

{trace_back}
{sys_info}
"""
    return result


def main():  # pragma: no cover
    # Make sure stdout and stderr have errors='replace' to avoid unicode
    # issues when print()-ing unicode file names
    sys.stdout = ErrorIgnoringTextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, "replace", line_buffering=True)
    sys.stderr = ErrorIgnoringTextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, "replace", line_buffering=True)

    # If we receive SIGINT (ctrl-c), SIGTERM (kill) or SIGHUP (kill -HUP),
    # catch them and raise a proper exception that can be handled for an
    # orderly exit.
    # SIGHUP is important especially for systemd systems, where logind
    # sends it when a session exits, in addition to any traditional use.
    # Output some info if we receive SIGUSR1 or SIGINFO (ctrl-t).

    # Register fault handler for SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL.
    faulthandler.enable()
    with (
        signal_handler("SIGINT", raising_signal_handler(KeyboardInterrupt)),
        signal_handler("SIGHUP", raising_signal_handler(SigHup)),
        signal_handler("SIGTERM", raising_signal_handler(SigTerm)),
        signal_handler("SIGUSR1", sig_info_handler),
        signal_handler("SIGUSR2", sig_trace_handler),
        signal_handler("SIGINFO", sig_info_handler),
    ):
        archiver = Archiver()
        msg = msgid = tb = None
        tb_log_level = logging.ERROR
        try:
            args = archiver.get_args(sys.argv, os.environ.get("SSH_ORIGINAL_COMMAND"))
        except Error as e:
            # we might not have logging setup yet, so get out quickly
            msg = e.get_message()
            print(msg, file=sys.stderr)
            if e.traceback:
                tb = format_tb(e)
                print(tb, file=sys.stderr)
            sys.exit(e.exit_code)
        except ArgumentTypeError as e:
            # we might not have logging setup yet, so get out quickly
            print(str(e), file=sys.stderr)
            sys.exit(CommandError.exit_mcode if modern_ec else EXIT_ERROR)
        except Exception:
            msg = "Local Exception"
            tb = f"{traceback.format_exc()}\n{sysinfo()}"
            # we might not have logging setup yet, so get out quickly
            print(msg, file=sys.stderr)
            print(tb, file=sys.stderr)
            sys.exit(EXIT_ERROR)

        if args.cockpit:
            # Cockpit TUI operation
            try:
                from ..cockpit.app import BorgCockpitApp
            except ImportError as err:
                print(f"ImportError: {err}", file=sys.stderr)
                print("The Borg Cockpit feature has some additional requirements.", file=sys.stderr)
                print("Please install them using: pip install 'borgbackup[cockpit]'", file=sys.stderr)
                sys.exit(EXIT_ERROR)

            app = BorgCockpitApp()
            app.borg_args = [arg for arg in sys.argv[1:] if arg != "--cockpit"]
            app.run()
            sys.exit(EXIT_SUCCESS)  # borg subprocess RC was already shown on the TUI

        # normal borg CLI operation
        try:
            with sig_int:
                exit_code = archiver.run(args)
        except Error as e:
            msg = e.get_message()
            msgid = type(e).__qualname__
            tb_log_level = logging.ERROR if e.traceback else logging.DEBUG
            tb = format_tb(e)
            exit_code = e.exit_code
        except LegacyRemoteRepository.RPCError as e:
            important = e.traceback
            msg = e.exception_full if important else e.get_message()
            msgid = e.exception_class
            tb_log_level = logging.ERROR if important else logging.DEBUG
            tb = format_tb(e)
            exit_code = EXIT_ERROR
        except Exception as e:
            msg = "Local Exception"
            msgid = "Exception"
            tb_log_level = logging.ERROR
            tb = format_tb(e)
            exit_code = EXIT_ERROR
        except KeyboardInterrupt as e:
            msg = "Keyboard interrupt"
            tb_log_level = logging.DEBUG
            tb = format_tb(e)
            exit_code = EXIT_SIGNAL_BASE + 2
        except SigTerm as e:
            msg = "Received SIGTERM"
            msgid = "Signal.SIGTERM"
            tb_log_level = logging.DEBUG
            tb = format_tb(e)
            exit_code = EXIT_SIGNAL_BASE + 15
        except SigHup as e:
            msg = "Received SIGHUP."
            msgid = "Signal.SIGHUP"
            tb_log_level = logging.DEBUG
            tb = format_tb(e)
            exit_code = EXIT_SIGNAL_BASE + 1
        if msg:
            logger.error(msg, msgid=msgid)
        if tb:
            logger.log(tb_log_level, tb)
        if args.show_rc:
            from ..helpers import do_show_rc

            do_show_rc(exit_code)
        sys.exit(exit_code)


if __name__ == "__main__":
    main()
