diff options
-rw-r--r-- | examples/bash_automatic_cd.sh | 2 | ||||
-rw-r--r-- | ranger/__init__.py | 37 | ||||
-rw-r--r-- | ranger/api/commands.py | 5 | ||||
-rw-r--r-- | ranger/container/fsobject.py | 4 | ||||
-rw-r--r-- | ranger/core/actions.py | 16 | ||||
-rw-r--r-- | ranger/core/fm.py | 19 | ||||
-rw-r--r-- | ranger/core/linemode.py | 4 | ||||
-rw-r--r-- | ranger/core/main.py | 46 | ||||
-rw-r--r-- | ranger/ext/logutils.py | 78 | ||||
-rw-r--r-- | ranger/ext/spawn.py | 83 | ||||
-rw-r--r-- | ranger/ext/vcs/vcs.py | 47 | ||||
-rw-r--r-- | ranger/gui/widgets/view_multipane.py | 6 |
12 files changed, 225 insertions, 122 deletions
diff --git a/examples/bash_automatic_cd.sh b/examples/bash_automatic_cd.sh index 040bf21a..bdac5757 100644 --- a/examples/bash_automatic_cd.sh +++ b/examples/bash_automatic_cd.sh @@ -6,6 +6,8 @@ # the last visited one after ranger quits. # To undo the effect of this function, you can type "cd -" to return to the # original directory. +# +# On OS X 10 or later, replace `usr/bin/ranger` with `/usr/local/bin/ranger`. function ranger-cd { tempfile="$(mktemp -t tmp.XXXXXX)" diff --git a/ranger/__init__.py b/ranger/__init__.py index 1ef62b87..c2aa804e 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -27,46 +27,11 @@ MACRO_DELIMITER = '%' DEFAULT_PAGER = 'less' CACHEDIR = os.path.expanduser("~/.cache/ranger") USAGE = '%prog [options] [path]' -VERSION = 'ranger-stable %s\n\nPython %s' % (__version__, sys.version) +VERSION = 'ranger-master %s\n\nPython %s' % (__version__, sys.version) -try: - ExceptionClass = FileNotFoundError -except NameError: - ExceptionClass = IOError -try: - LOGFILE = tempfile.gettempdir() + '/ranger_errorlog' -except ExceptionClass: - LOGFILE = '/dev/null' -del ExceptionClass # If the environment variable XDG_CONFIG_HOME is non-empty, CONFDIR is ignored # and the configuration directory will be $XDG_CONFIG_HOME/ranger instead. CONFDIR = '~/.config/ranger' -# Debugging functions. These will be activated when run with --debug. -# Example usage in the code: -# import ranger; ranger.log("hello world") - - -def log(*objects, **keywords): - """Writes objects to a logfile (for the purpose of debugging only.) - Has the same arguments as print() in python3. - """ - from ranger import arg - if LOGFILE is None or not arg.debug or arg.clean: - return - start = keywords.get('start', 'ranger:') - sep = keywords.get('sep', ' ') - end = keywords.get('end', '\n') - _file = keywords['file'] if 'file' in keywords else open(LOGFILE, 'a') - _file.write(sep.join(map(str, (start, ) + objects)) + end) - - -def log_traceback(): - from ranger import arg - if LOGFILE is None or not arg.debug or arg.clean: - return - import traceback - traceback.print_stack(file=open(LOGFILE, 'a')) - from ranger.core.main import main diff --git a/ranger/api/commands.py b/ranger/api/commands.py index 9b8ec7d5..93c50adb 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -236,6 +236,11 @@ class Command(FileManagerAware): break return flags, rest + @lazy_property + def log(self): + import logging + return logging.getLogger('ranger.commands.' + self.__class__.__name__) + # XXX: Lazy properties? Not so smart? self.line can change after all! @lazy_property def _tabinsert_left(self): diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index d73fa76c..9b312e6d 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -20,7 +20,7 @@ from pwd import getpwuid from ranger.core.linemode import * from ranger.core.shared import FileManagerAware, SettingsAware from ranger.ext.shell_escape import shell_escape -from ranger.ext.spawn import spawn +from ranger.ext import spawn from ranger.ext.lazy_property import lazy_property from ranger.ext.human_readable import human_readable @@ -131,7 +131,7 @@ class FileSystemObject(FileManagerAware, SettingsAware): @lazy_property def filetype(self): try: - return spawn(["file", '-Lb', '--mime-type', self.path]) + return spawn.check_output(["file", '-Lb', '--mime-type', self.path]) except OSError: return "" diff --git a/ranger/core/actions.py b/ranger/core/actions.py index c119c501..1b35e57b 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -13,6 +13,7 @@ from inspect import cleandoc from stat import S_IEXEC from hashlib import sha1 from sys import version_info +from logging import getLogger import ranger from ranger.ext.direction import Direction @@ -31,6 +32,8 @@ from ranger.core.linemode import DEFAULT_LINEMODE MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>" +log = getLogger(__name__) + class _MacroTemplate(string.Template): """A template for substituting macros in commands""" @@ -152,7 +155,7 @@ class Actions(FileManagerAware, SettingsAware): elif bad is True and ranger.arg.debug: raise Exception(str(text)) text = str(text) - self.log.appendleft(text) + log.debug("Command notify invoked: [Bad: {0}, Text: '{1}']".format(bad, text)) if self.ui and self.ui.is_on: self.ui.status.notify(" ".join(text.split("\n")), duration=duration, bad=bad) @@ -344,9 +347,10 @@ class Actions(FileManagerAware, SettingsAware): Load a config file. """ filename = os.path.expanduser(filename) + log.debug("Sourcing config file '{0}'".format(filename)) with open(filename, 'r') as f: for line in f: - line = line.lstrip().rstrip("\r\n") + line = line.strip(" \r\n") if line.startswith("#") or not line.strip(): continue try: @@ -419,7 +423,7 @@ class Actions(FileManagerAware, SettingsAware): Example: self.move(down=4, pages=True) # moves down by 4 pages. self.move(to=2, pages=True) # moves to page 2. - self.move(to=1, percentage=True) # moves to 80% + self.move(to=80, percentage=True) # moves to 80% """ cwd = self.thisdir direction = Direction(kw) @@ -869,11 +873,13 @@ class Actions(FileManagerAware, SettingsAware): self.notify("Could not find manpage.", bad=True) def display_log(self): + logs = list(self.get_log()) pager = self.ui.open_pager() - if self.log: - pager.set_source(["Message Log:"] + list(self.log)) + if logs: + pager.set_source(["Message Log:"] + logs) else: pager.set_source(["Message Log:", "No messages!"]) + pager.move(to=100, percentage=True) def display_file(self): if not self.thisfile or not self.thisfile.is_file: diff --git a/ranger/core/fm.py b/ranger/core/fm.py index a08de2e1..5d630464 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -5,6 +5,7 @@ from time import time from collections import deque +import logging import mimetypes import os.path import pwd @@ -25,8 +26,10 @@ from ranger.core.metadata import MetadataManager from ranger.ext.rifle import Rifle from ranger.container.directory import Directory from ranger.ext.signals import SignalDispatcher -from ranger import __version__ from ranger.core.loader import Loader +from ranger.ext import logutils + +log = logging.getLogger(__name__) class FM(Actions, SignalDispatcher): @@ -50,7 +53,6 @@ class FM(Actions, SignalDispatcher): self.ui = ui self.start_paths = paths self.directories = dict() - self.log = deque(maxlen=1000) self.bookmarks = bookmarks self.current_tab = 1 self.tabs = {} @@ -71,10 +73,6 @@ class FM(Actions, SignalDispatcher): self.hostname = socket.gethostname() self.home_path = os.path.expanduser('~') - self.log.appendleft('ranger {0} started! Process ID is {1}.' - .format(__version__, os.getpid())) - self.log.appendleft('Running on Python ' + sys.version.replace('\n', '')) - mimetypes.knownfiles.append(os.path.expanduser('~/.mime.types')) mimetypes.knownfiles.append(self.relpath('data/mime.types')) self.mimetypes = mimetypes.MimeTypes() @@ -205,6 +203,15 @@ class FM(Actions, SignalDispatcher): if debug: raise + def get_log(self): + """Return the current log + + The log is returned as a list of string + """ + for log in logutils.log_queue: + for line in log.split('\n'): + yield line + def _get_image_displayer(self): if self.settings.preview_images_method == "w3m": return W3MImageDisplayer() diff --git a/ranger/core/linemode.py b/ranger/core/linemode.py index a8ce4e6d..c7839723 100644 --- a/ranger/core/linemode.py +++ b/ranger/core/linemode.py @@ -7,7 +7,7 @@ import sys from abc import * from datetime import datetime from ranger.ext.human_readable import human_readable -from ranger.ext.spawn import spawn +from ranger.ext import spawn DEFAULT_LINEMODE = "filename" @@ -100,7 +100,7 @@ class FileInfoLinemode(LinemodeBase): if not file.is_directory: from subprocess import Popen, PIPE, CalledProcessError try: - fileinfo = spawn(["file", "-bL", file.path]).strip() + fileinfo = spawn.check_output(["file", "-bL", file.path]).strip() except CalledProcessError: return "unknown" if sys.version_info[0] >= 3: diff --git a/ranger/core/main.py b/ranger/core/main.py index 93c71e81..b0e3b600 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -6,6 +6,10 @@ import os.path import sys import tempfile +from ranger import __version__ +from logging import getLogger + +log = getLogger(__name__) def main(): @@ -15,6 +19,14 @@ def main(): from ranger.container.settings import Settings from ranger.core.shared import FileManagerAware, SettingsAware from ranger.core.fm import FM + from ranger.ext.logutils import setup_logging + + ranger.arg = arg = parse_arguments() + setup_logging(debug=arg.debug, logfile=arg.logfile) + + log.info("Ranger version {0}".format(__version__)) + log.info('Running on Python ' + sys.version.replace('\n', '')) + log.info("Process ID is {0}".format(os.getpid())) try: locale.setlocale(locale.LC_ALL, '') @@ -31,7 +43,9 @@ def main(): if 'SHELL' not in os.environ: os.environ['SHELL'] = 'sh' - ranger.arg = arg = parse_arguments() + log.debug("config dir: '{0}'".format(arg.confdir)) + log.debug("cache dir: '{0}'".format(arg.cachedir)) + if arg.copy_config is not None: fm = FM() fm.copy_config_files(arg.copy_config) @@ -73,8 +87,6 @@ def main(): "deprecated.\nPlease use the standalone file launcher " "'rifle' instead.\n") - def print_function(string): - print(string) from ranger.ext.rifle import Rifle fm = FM() if not arg.clean and os.path.isfile(fm.confpath('rifle.conf')): @@ -112,7 +124,7 @@ def main(): if fm.username == 'root': fm.settings.preview_files = False fm.settings.use_preview_script = False - fm.log.appendleft("Running as root, disabling the file previews.") + log.info("Running as root, disabling the file previews.") if not arg.debug: from ranger.ext import curses_interrupt_handler curses_interrupt_handler.install_interrupt_handler() @@ -199,6 +211,8 @@ def parse_arguments(): help="activate debug mode") parser.add_option('-c', '--clean', action='store_true', help="don't touch/require any config files. ") + parser.add_option('--logfile', type='string', metavar='file', + help="log file to use, '-' for stderr") parser.add_option('-r', '--confdir', type='string', metavar='dir', default=default_confdir, help="change the configuration directory. (%default)") @@ -265,14 +279,18 @@ def load_settings(fm, clean): allow_access_to_confdir(ranger.arg.confdir, True) # Load custom commands - if os.path.exists(fm.confpath('commands.py')): + custom_comm_path = fm.confpath('commands.py') + if os.path.exists(custom_comm_path): old_bytecode_setting = sys.dont_write_bytecode sys.dont_write_bytecode = True try: import commands fm.commands.load_commands_from_module(commands) - except ImportError: - pass + except ImportError as e: + log.debug("Failed to import custom commands from '{0}'".format(custom_comm_path)) + log.exception(e) + else: + log.debug("Loaded custom commands from '{0}'".format(custom_comm_path)) sys.dont_write_bytecode = old_bytecode_setting allow_access_to_confdir(ranger.arg.confdir, False) @@ -297,6 +315,7 @@ def load_settings(fm, clean): pass else: if not os.path.exists(fm.confpath('plugins', '__init__.py')): + log.debug("Creating missing '__init__.py' file in plugin folder") f = open(fm.confpath('plugins', '__init__.py'), 'w') f.close() @@ -313,11 +332,12 @@ def load_settings(fm, clean): else: module = importlib.import_module('plugins.' + plugin) fm.commands.load_commands_from_module(module) - fm.log.appendleft("Loaded plugin '%s'." % plugin) - except Exception: - import traceback - fm.log.extendleft(reversed(traceback.format_exc().splitlines())) - fm.notify("Error in plugin '%s'" % plugin, bad=True) + log.debug("Loaded plugin '{0}'".format(plugin)) + except Exception as e: + mex = "Error while loading plugin '{0}'".format(plugin) + log.error(mex) + log.exception(e) + fm.notify(mex, bad=True) ranger.fm = None # COMPAT: Load the outdated options.py @@ -360,6 +380,8 @@ def allow_access_to_confdir(confdir, allow): print("To run ranger without the need for configuration") print("files, use the --clean option.") raise SystemExit() + else: + log.debug("Created config directory '{0}'".format(confdir)) if confdir not in sys.path: sys.path[0:0] = [confdir] else: diff --git a/ranger/ext/logutils.py b/ranger/ext/logutils.py new file mode 100644 index 00000000..0de6c333 --- /dev/null +++ b/ranger/ext/logutils.py @@ -0,0 +1,78 @@ +import logging +from collections import deque + +LOG_FORMAT = "[%(levelname)s] %(message)s" +LOG_FORMAT_EXT = "%(asctime)s,%(msecs)d [%(name)s] |%(levelname)s| %(message)s" +LOG_DATA_FORMAT = "%H:%M:%S" + + +class QueueHandler(logging.Handler): + """ + This handler store logs events into a queue. + """ + + def __init__(self, queue): + """ + Initialize an instance, using the passed queue. + """ + logging.Handler.__init__(self) + self.queue = queue + + def enqueue(self, record): + """ + Enqueue a log record. + """ + self.queue.append(record) + + def emit(self, record): + self.enqueue(self.format(record)) + + +log_queue = deque(maxlen=1000) +concise_formatter = logging.Formatter(fmt=LOG_FORMAT, datefmt=LOG_DATA_FORMAT) +extended_formatter = logging.Formatter(fmt=LOG_FORMAT_EXT, datefmt=LOG_DATA_FORMAT) + + +def setup_logging(debug=True, logfile=None): + """ + All the produced logs using the standard logging function + will be collected in a queue by the `queue_handler` as well + as outputted on the standard error `stderr_handler`. + + The verbosity and the format of the log message is + controlled by the `debug` parameter + + - debug=False: + a concise log format will be used, debug messsages will be discarded + and only important message will be passed to the `stderr_handler` + + - debug=True: + an extended log format will be used, all messages will be processed + by both the handlers + """ + root_logger = logging.getLogger() + + if debug: + # print all logging in extended format + log_level = logging.DEBUG + formatter = extended_formatter + else: + # print only warning and critical message + # in a concise format + log_level = logging.INFO + formatter = concise_formatter + + handlers = [] + handlers.append(QueueHandler(log_queue)) + if logfile: + if logfile is '-': + handlers.append(logging.StreamHandler()) + else: + handlers.append(logging.FileHandler(logfile)) + + for handler in handlers: + handler.setLevel(log_level) + handler.setFormatter(formatter) + root_logger.addHandler(handler) + + root_logger.setLevel(0) diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py index 393d48d9..04abfbd2 100644 --- a/ranger/ext/spawn.py +++ b/ranger/ext/spawn.py @@ -1,11 +1,50 @@ # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. +from os import devnull from subprocess import Popen, PIPE, CalledProcessError ENCODING = 'utf-8' -def spawn(*args, **kwargs): +def check_output(popenargs, **kwargs): + """Runs a program, waits for its termination and returns its output + + This function is functionally identical to python 2.7's subprocess.check_output, + but is favored due to python 2.6 compatibility. + + Will be run through a shell if `popenargs` is a string, otherwise the command + is executed directly. + + The keyword argument `decode` determines if the output shall be decoded + with the encoding UTF-8. + + Further keyword arguments are passed to Popen. + """ + + do_decode = kwargs.pop('decode', True) + kwargs.setdefault('stdout', PIPE) + kwargs.setdefault('shell', isinstance(popenargs, str)) + + if 'stderr' in kwargs: + process = Popen(popenargs, **kwargs) + stdout, _ = process.communicate() + else: + with open(devnull, mode='w') as fd_devnull: + process = Popen(popenargs, stderr=fd_devnull, **kwargs) + stdout, _ = process.communicate() + + if process.returncode != 0: + error = CalledProcessError(process.returncode, popenargs) + error.output = stdout + raise error + + if do_decode and stdout is not None: + stdout = stdout.decode(ENCODING) + + return stdout + + +def spawn(*popenargs, **kwargs): """Runs a program, waits for its termination and returns its stdout This function is similar to python 2.7's subprocess.check_output, @@ -17,41 +56,15 @@ def spawn(*args, **kwargs): spawn(command, arg1, arg2...) spawn([command, arg1, arg2]) - The string will be run through a shell, otherwise the command is executed - directly. + Will be run through a shell if `popenargs` is a string, otherwise the command + is executed directly. - The keyword argument "decode" determines if the output shall be decoded - with the encoding '%s'. + The keyword argument `decode` determines if the output shall be decoded + with the encoding UTF-8. Further keyword arguments are passed to Popen. - """ % (ENCODING, ) - - if len(args) == 1: - popen_arguments = args[0] - shell = isinstance(popen_arguments, str) - else: - popen_arguments = args - shell = False - - if 'decode' in kwargs: - do_decode = kwargs['decode'] - del kwargs['decode'] - else: - do_decode = True - if 'stdout' not in kwargs: - kwargs['stdout'] = PIPE - if 'shell' not in kwargs: - kwargs['shell'] = shell - - process = Popen(popen_arguments, **kwargs) - stdout, stderr = process.communicate() - return_value = process.poll() - if return_value: - error = CalledProcessError(return_value, popen_arguments[0]) - error.output = stdout - raise error - - if do_decode: - return stdout.decode(ENCODING) + """ + if len(popenargs) == 1: + return check_output(popenargs[0], **kwargs) else: - return stdout + return check_output(list(popenargs), **kwargs) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index cfdc1e3b..fa00777a 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -7,7 +7,9 @@ import os import subprocess import threading import time -from ranger.ext.spawn import spawn +from logging import getLogger + +from ranger.ext import spawn # Python2 compatibility try: @@ -20,6 +22,9 @@ except NameError: FileNotFoundError = OSError # pylint: disable=redefined-builtin +LOG = getLogger(__name__) + + class VcsError(Exception): """VCS exception""" pass @@ -117,20 +122,17 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes if path is None: path = self.path - with open(os.devnull, 'w') as devnull: - try: - if catchout: - output = spawn(cmd, cwd=path, stderr=devnull, - decode=not retbytes) - if (not retbytes and rstrip_newline and - output.endswith('\n')): - if rstrip_newline and output.endswith('\n'): - return output[:-1] - return output - else: - subprocess.check_call(cmd, cwd=path, stdout=devnull, stderr=devnull) - except (subprocess.CalledProcessError, FileNotFoundError): - raise VcsError('{0:s}: {1:s}'.format(str(cmd), path)) + try: + if catchout: + output = spawn.check_output(cmd, cwd=path, decode=not retbytes) + if not retbytes and rstrip_newline and output.endswith('\n'): + return output[:-1] + return output + else: + with open(os.devnull, mode='w') as fd_devnull: + subprocess.check_call(cmd, cwd=path, stdout=fd_devnull, stderr=fd_devnull) + except (subprocess.CalledProcessError, FileNotFoundError): + raise VcsError('{0:s}: {1:s}'.format(str(cmd), path)) def _get_repotype(self, path): """Get type for path""" @@ -234,7 +236,9 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method self.branch = self.data_branch() self.obj.vcsremotestatus = self.data_status_remote() self.obj.vcsstatus = self.data_status_root() - except VcsError: + except VcsError as error: + LOG.exception(error) + self.obj.fm.notify('VCS Exception: View log for more info', bad=True) return False self.rootinit = True return True @@ -247,7 +251,9 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method self.status_subpaths = self.data_status_subpaths() self.obj.vcsremotestatus = self.data_status_remote() self.obj.vcsstatus = self._status_root() - except VcsError: + except VcsError as error: + LOG.exception(error) + self.obj.fm.notify('VCS Exception: View log for more info', bad=True) return False self.rootinit = True self.updatetime = time.time() @@ -470,10 +476,9 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut column.need_redraw = True self.ui.status.need_redraw = True self.ui.redraw() - except Exception: # pylint: disable=broad-except - import traceback - self.ui.fm.log.extendleft(reversed(traceback.format_exc().splitlines())) - self.ui.fm.notify('VCS Exception', bad=True) + except Exception as error: # pylint: disable=broad-except + LOG.exception(error) + self.ui.fm.notify('VCS Exception: View log for more info', bad=True) def pause(self): """Pause thread""" diff --git a/ranger/gui/widgets/view_multipane.py b/ranger/gui/widgets/view_multipane.py index 7b77b3db..f8efd80f 100644 --- a/ranger/gui/widgets/view_multipane.py +++ b/ranger/gui/widgets/view_multipane.py @@ -42,11 +42,11 @@ class ViewMultipane(ViewBase): def resize(self, y, x, hei, wid): ViewBase.resize(self, y, x, hei, wid) - column_width = int(float(wid) / len(self.columns)) + column_width = int((float(wid) - len(self.columns) + 1) / len(self.columns)) left = 0 top = 0 for i, column in enumerate(self.columns): - column.resize(top, left, hei, max(1, column_width - 1)) - left += column_width + column.resize(top, left, hei, max(1, column_width)) + left += column_width + 1 column.need_redraw = True self.need_redraw = True |