summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--ranger/__init__.py35
-rw-r--r--ranger/api/commands.py5
-rw-r--r--ranger/core/actions.py12
-rw-r--r--ranger/core/fm.py19
-rw-r--r--ranger/core/main.py46
-rw-r--r--ranger/ext/logutils.py78
-rw-r--r--ranger/ext/vcs/vcs.py7
7 files changed, 143 insertions, 59 deletions
diff --git a/ranger/__init__.py b/ranger/__init__.py
index 4d8e4afe..9c4f8922 100644
--- a/ranger/__init__.py
+++ b/ranger/__init__.py
@@ -29,44 +29,9 @@ CACHEDIR = os.path.expanduser("~/.cache/ranger")
 USAGE = '%prog [options] [path]'
 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/core/actions.py b/ranger/core/actions.py
index 5775a8bb..297f5c8e 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,6 +347,7 @@ 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.strip(" \r\n")
@@ -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/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/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index cfdc1e3b..9c2a8b5a 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -7,6 +7,7 @@ import os
 import subprocess
 import threading
 import time
+from logging import getLogger
 from ranger.ext.spawn import spawn
 
 # Python2 compatibility
@@ -19,6 +20,7 @@ try:
 except NameError:
     FileNotFoundError = OSError  # pylint: disable=redefined-builtin
 
+log = getLogger(__name__)
 
 class VcsError(Exception):
     """VCS exception"""
@@ -470,9 +472,8 @@ 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()))
+            except Exception as e:  # pylint: disable=broad-except
+                log.exception(e)
                 self.ui.fm.notify('VCS Exception', bad=True)
 
     def pause(self):