예제 #1
0
def enable_ansi_support():
    """
    Try to enable support for ANSI escape sequences (required on Windows).

    :returns: :data:`True` if ANSI is supported, :data:`False` otherwise.

    This functions checks for the following supported configurations, in the
    given order:

    1. On Windows, if :func:`have_windows_native_ansi_support()` confirms
       native support for ANSI escape sequences :mod:`ctypes` will be used to
       enable this support.

    2. On Windows, if the environment variable ``$ANSICON`` is set nothing is
       done because it is assumed that support for ANSI escape sequences has
       already been enabled via `ansicon <https://github.com/adoxa/ansicon>`_.

    3. On Windows, an attempt is made to import and initialize the Python
       package :pypi:`colorama` instead (of course for this to work
       :pypi:`colorama` has to be installed).

    4. On other platforms this function calls :func:`connected_to_terminal()`
       to determine whether ANSI escape sequences are supported (that is to
       say all platforms that are not Windows are assumed to support ANSI
       escape sequences natively, without weird contortions like above).

       This makes it possible to call :func:`enable_ansi_support()`
       unconditionally without checking the current platform.

    The :func:`~humanfriendly.decorators.cached` decorator is used to ensure
    that this function is only executed once, but its return value remains
    available on later calls.
    """
    if have_windows_native_ansi_support():
        import ctypes
        ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7)
        ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-12), 7)
        return True
    elif on_windows():
        if 'ANSICON' in os.environ:
            return True
        try:
            import colorama
            colorama.init()
            return True
        except ImportError:
            return False
    else:
        return connected_to_terminal()
예제 #2
0
def is_syslog_supported():
    """
    Determine whether system logging is supported.

    :returns:

        :data:`True` if system logging is supported and can be enabled,
        :data:`False` if system logging is not supported or there are good
        reasons for not enabling it.

    The decision making process here is as follows:

    Override
     If the environment variable ``$COLOREDLOGS_SYSLOG`` is set it is evaluated
     using :func:`~humanfriendly.coerce_boolean()` and the resulting value
     overrides the platform detection discussed below, this allows users to
     override the decision making process if they disagree / know better.

    Linux / UNIX
     On systems that are not Windows or MacOS (see below) we assume UNIX which
     means either syslog is available or sending a bunch of UDP packets to
     nowhere won't hurt anyone...

    Microsoft Windows
     Over the years I've had multiple reports of :pypi:`coloredlogs` spewing
     extremely verbose errno 10057 warning messages to the console (once for
     each log message I suppose) so I now assume it a default that
     "syslog-style system logging" is not generally available on Windows.

    Apple MacOS
     There's cPython issue `#38780`_ which seems to result in a fatal exception
     when the Python interpreter shuts down. This is (way) worse than not
     having system logging enabled. The error message mentioned in `#38780`_
     has actually been following me around for years now, see for example:

     - https://github.com/xolox/python-rotate-backups/issues/9 mentions Docker
       images implying Linux, so not strictly the same as `#38780`_.

     - https://github.com/xolox/python-npm-accel/issues/4 is definitely related
       to `#38780`_ and is what eventually prompted me to add the
       :func:`is_syslog_supported()` logic.

    .. _#38780: https://bugs.python.org/issue38780
    """
    override = os.environ.get("COLOREDLOGS_SYSLOG")
    if override is not None:
        return coerce_boolean(override)
    else:
        return not (on_windows() or on_macos())
예제 #3
0
def have_windows_native_ansi_support():
    """
    Check if we're running on a Windows 10 release with native support for ANSI escape sequences.

    :returns: :data:`True` if so, :data:`False` otherwise.

    The :func:`~humanfriendly.decorators.cached` decorator is used as a minor
    performance optimization. Semantically this should have zero impact because
    the answer doesn't change in the lifetime of a computer process.
    """
    if on_windows():
        try:
            # I can't be 100% sure this will never break and I'm not in a
            # position to test it thoroughly either, so I decided that paying
            # the price of one additional try / except statement is worth the
            # additional peace of mind :-).
            components = tuple(int(c) for c in platform.version().split('.'))
            return components >= (10, 0, 14393)
        except Exception:
            pass
    return False
예제 #4
0
def use_color():
    use_colors = True
    if use_colors or (use_colors is None):
        # Respect the user's choice not to have colors.
        if use_colors is None and 'NO_COLOR' in os.environ:
            # For details on this see https://no-color.org/.
            use_colors = False
        # Try to enable Windows native ANSI support or Colorama?
        if (use_colors or use_colors is None) and on_windows():
            # This can fail, in which case ANSI escape sequences would end
            # up being printed to the terminal in raw form. This is very
            # user hostile, so to avoid this happening we disable color
            # support on failure.
            use_colors = enable_ansi_support()
        # When auto detection is enabled, and so far we encountered no
        # reason to disable color support, then we will enable color
        # support if 'stream' is connected to a terminal.
        if use_colors is None:
            use_colors = terminal_supports_colors()

    return use_colors
예제 #5
0
def terminal_supports_colors(stream=None):
    """
    Check if a stream is connected to a terminal that supports ANSI escape sequences.

    :param stream: The stream to check (a file-like object,
                   defaults to :data:`sys.stdout`).
    :returns: :data:`True` if the terminal supports ANSI escape sequences,
              :data:`False` otherwise.

    This function was originally inspired by the implementation of
    `django.core.management.color.supports_color()
    <https://github.com/django/django/blob/master/django/core/management/color.py>`_
    but has since evolved significantly.
    """
    if on_windows():
        # On Windows support for ANSI escape sequences is not a given.
        have_ansicon = 'ANSICON' in os.environ
        have_colorama = 'colorama' in sys.modules
        have_native_support = have_windows_native_ansi_support()
        if not (have_ansicon or have_colorama or have_native_support):
            return False
    return connected_to_terminal(stream)
예제 #6
0
def main():
    """Command line interface for the ``rotate-backups`` program."""
    coloredlogs.install()
    # Command line option defaults.
    rotation_scheme = {}
    kw = dict(include_list=[], exclude_list=[])
    parallel = False
    use_sudo = False
    use_syslog = (not on_windows())
    # Internal state.
    selected_locations = []
    # Parse the command line arguments.
    try:
        options, arguments = getopt.getopt(
            sys.argv[1:], 'M:H:d:w:m:y:t:I:x:jpri:c:C:uS:fnvqh', [
                'minutely=',
                'hourly=',
                'daily=',
                'weekly=',
                'monthly=',
                'yearly=',
                'timestamp-pattern=',
                'include=',
                'exclude=',
                'parallel',
                'prefer-recent',
                'relaxed',
                'ionice=',
                'config=',
                'removal-command=',
                'use-sudo',
                'syslog=',
                'force',
                'dry-run',
                'verbose',
                'quiet',
                'help',
            ])
        for option, value in options:
            if option in ('-M', '--minutely'):
                rotation_scheme['minutely'] = coerce_retention_period(value)
            elif option in ('-H', '--hourly'):
                rotation_scheme['hourly'] = coerce_retention_period(value)
            elif option in ('-d', '--daily'):
                rotation_scheme['daily'] = coerce_retention_period(value)
            elif option in ('-w', '--weekly'):
                rotation_scheme['weekly'] = coerce_retention_period(value)
            elif option in ('-m', '--monthly'):
                rotation_scheme['monthly'] = coerce_retention_period(value)
            elif option in ('-y', '--yearly'):
                rotation_scheme['yearly'] = coerce_retention_period(value)
            elif option in ('-t', '--timestamp-pattern'):
                kw['timestamp_pattern'] = value
            elif option in ('-I', '--include'):
                kw['include_list'].append(value)
            elif option in ('-x', '--exclude'):
                kw['exclude_list'].append(value)
            elif option in ('-j', '--parallel'):
                parallel = True
            elif option in ('-p', '--prefer-recent'):
                kw['prefer_recent'] = True
            elif option in ('-r', '--relaxed'):
                kw['strict'] = False
            elif option in ('-i', '--ionice'):
                value = validate_ionice_class(value.lower().strip())
                kw['io_scheduling_class'] = value
            elif option in ('-c', '--config'):
                kw['config_file'] = parse_path(value)
            elif option in ('-C', '--removal-command'):
                removal_command = shlex.split(value)
                logger.info("Using custom removal command: %s",
                            removal_command)
                kw['removal_command'] = removal_command
            elif option in ('-u', '--use-sudo'):
                use_sudo = True
            elif option in ('-S', '--syslog'):
                use_syslog = coerce_boolean(value)
            elif option in ('-f', '--force'):
                kw['force'] = True
            elif option in ('-n', '--dry-run'):
                logger.info("Performing a dry run (because of %s option) ..",
                            option)
                kw['dry_run'] = True
            elif option in ('-v', '--verbose'):
                coloredlogs.increase_verbosity()
            elif option in ('-q', '--quiet'):
                coloredlogs.decrease_verbosity()
            elif option in ('-h', '--help'):
                usage(__doc__)
                return
            else:
                assert False, "Unhandled option! (programming error)"
        if use_syslog:
            enable_system_logging()
        if rotation_scheme:
            logger.verbose("Rotation scheme defined on command line: %s",
                           rotation_scheme)
        if arguments:
            # Rotation of the locations given on the command line.
            location_source = 'command line arguments'
            selected_locations.extend(
                coerce_location(value, sudo=use_sudo) for value in arguments)
        else:
            # Rotation of all configured locations.
            location_source = 'configuration file'
            selected_locations.extend(
                location
                for location, rotation_scheme, options in load_config_file(
                    configuration_file=kw.get('config_file'), expand=True))
        # Inform the user which location(s) will be rotated.
        if selected_locations:
            logger.verbose("Selected %s based on %s:",
                           pluralize(len(selected_locations), "location"),
                           location_source)
            for number, location in enumerate(selected_locations, start=1):
                logger.verbose(" %i. %s", number, location)
        else:
            # Show the usage message when no directories are given nor configured.
            logger.verbose("No location(s) to rotate selected.")
            usage(__doc__)
            return
    except Exception as e:
        logger.error("%s", e)
        sys.exit(1)
    # Rotate the backups in the selected directories.
    program = RotateBackups(rotation_scheme, **kw)
    if parallel:
        program.rotate_concurrent(*selected_locations)
    else:
        for location in selected_locations:
            program.rotate_backups(location)