diff options
| -rw-r--r-- | LICENSE | 6 | ||||
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | changes/feature_2566-logger_window | 1 | ||||
| -rw-r--r-- | data/images/oxygen-icons/dialog-error.png | bin | 0 -> 1527 bytes | |||
| -rw-r--r-- | data/images/oxygen-icons/dialog-information.png | bin | 0 -> 1636 bytes | |||
| -rw-r--r-- | data/images/oxygen-icons/dialog-warning.png | bin | 0 -> 1354 bytes | |||
| -rw-r--r-- | data/images/oxygen-icons/document-save-as.png | bin | 0 -> 2152 bytes | |||
| -rw-r--r-- | data/images/oxygen-icons/edit-bomb.png | bin | 0 -> 1635 bytes | |||
| -rw-r--r-- | data/images/oxygen-icons/script-error.png | bin | 0 -> 1709 bytes | |||
| -rw-r--r-- | data/resources/loggerwindow.qrc | 11 | ||||
| -rw-r--r-- | src/leap/app.py | 15 | ||||
| -rw-r--r-- | src/leap/gui/loggerwindow.py | 124 | ||||
| -rw-r--r-- | src/leap/gui/mainwindow.py | 37 | ||||
| -rw-r--r-- | src/leap/gui/ui/loggerwindow.ui | 155 | ||||
| -rw-r--r-- | src/leap/gui/ui/mainwindow.ui | 10 | ||||
| -rw-r--r-- | src/leap/util/leap_log_handler.py | 99 | 
16 files changed, 454 insertions, 8 deletions
| @@ -691,4 +691,8 @@ data/images/Globe.png  Author: Everaldo Coelho  License: LGPL - http://www.gnu.org/licenses/lgpl.html  WebSite: http://www.everaldo.com/ ----
\ No newline at end of file +--- +data/images/oxygen-icons/ + +License: LGPL - http://www.gnu.org/licenses/lgpl.html +Website: http://www.oxygen-icons.org/ @@ -20,9 +20,9 @@ TRANSLAT_DIR = data/translations  PROJFILE = data/leap_client.pro  #UI files to compile -UI_FILES = mainwindow.ui wizard.ui +UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui  #Qt resource files to compile -RESOURCES = mainwindow.qrc locale.qrc +RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc  #pyuic4 and pyrcc4 binaries  PYUIC = pyside-uic diff --git a/changes/feature_2566-logger_window b/changes/feature_2566-logger_window new file mode 100644 index 00000000..c236aa82 --- /dev/null +++ b/changes/feature_2566-logger_window @@ -0,0 +1 @@ +  o Add centraliced logging facility, log history in a window, closes issue #2566 diff --git a/data/images/oxygen-icons/dialog-error.png b/data/images/oxygen-icons/dialog-error.pngBinary files differ new file mode 100644 index 00000000..01bc922d --- /dev/null +++ b/data/images/oxygen-icons/dialog-error.png diff --git a/data/images/oxygen-icons/dialog-information.png b/data/images/oxygen-icons/dialog-information.pngBinary files differ new file mode 100644 index 00000000..ee59e170 --- /dev/null +++ b/data/images/oxygen-icons/dialog-information.png diff --git a/data/images/oxygen-icons/dialog-warning.png b/data/images/oxygen-icons/dialog-warning.pngBinary files differ new file mode 100644 index 00000000..8e913378 --- /dev/null +++ b/data/images/oxygen-icons/dialog-warning.png diff --git a/data/images/oxygen-icons/document-save-as.png b/data/images/oxygen-icons/document-save-as.pngBinary files differ new file mode 100644 index 00000000..9695a564 --- /dev/null +++ b/data/images/oxygen-icons/document-save-as.png diff --git a/data/images/oxygen-icons/edit-bomb.png b/data/images/oxygen-icons/edit-bomb.pngBinary files differ new file mode 100644 index 00000000..2b36224c --- /dev/null +++ b/data/images/oxygen-icons/edit-bomb.png diff --git a/data/images/oxygen-icons/script-error.png b/data/images/oxygen-icons/script-error.pngBinary files differ new file mode 100644 index 00000000..c7ace707 --- /dev/null +++ b/data/images/oxygen-icons/script-error.png diff --git a/data/resources/loggerwindow.qrc b/data/resources/loggerwindow.qrc new file mode 100644 index 00000000..847ca9a1 --- /dev/null +++ b/data/resources/loggerwindow.qrc @@ -0,0 +1,11 @@ +<RCC> +  <qresource prefix="/"> +    <file>../images/oxygen-icons/edit-bomb.png</file> +    <file>../images/oxygen-icons/document-save-as.png</file> +    <file>../images/oxygen-icons/dialog-information.png</file> +    <file>../images/oxygen-icons/script-error.png</file> +    <file>../images/oxygen-icons/dialog-error.png</file> +    <file>../images/oxygen-icons/dialog-information.png</file> +    <file>../images/oxygen-icons/dialog-warning.png</file> +  </qresource> +</RCC> diff --git a/src/leap/app.py b/src/leap/app.py index 797cea8a..03552edb 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -26,6 +26,7 @@ from PySide import QtCore, QtGui  from leap.common.events import server as event_server  from leap.util import __version__ as VERSION  from leap.util import leap_argparse +from leap.util.leap_log_handler import LeapLogHandler  from leap.gui import locale_rc  from leap.gui import twisted_main  from leap.gui.mainwindow import MainWindow @@ -33,6 +34,7 @@ from leap.platform_init import IS_MAC  from leap.platform_init.locks import we_are_the_one_and_only  from leap.services.tx import leap_services +  import codecs  codecs.register(lambda name: codecs.lookup('utf-8')                  if name == 'cp65001' else None) @@ -75,16 +77,23 @@ def main():      else:          level = logging.WARNING +    # Console logger      logger = logging.getLogger(name='leap')      logger.setLevel(level)      console = logging.StreamHandler()      console.setLevel(level) -    formatter = logging.Formatter( -        '%(asctime)s ' -        '- %(name)s - %(levelname)s - %(message)s') +    log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' +    formatter = logging.Formatter(log_format)      console.setFormatter(formatter)      logger.addHandler(console) +    # LEAP custom handler +    leap_handler = LeapLogHandler() +    leap_handler.setLevel(level) +    logger.addHandler(leap_handler) + +    logger.debug('Leap handler plugged!') +      if not we_are_the_one_and_only():          # leap-client is already running          logger.warning("Tried to launch more than one instance " diff --git a/src/leap/gui/loggerwindow.py b/src/leap/gui/loggerwindow.py new file mode 100644 index 00000000..dd724ac7 --- /dev/null +++ b/src/leap/gui/loggerwindow.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# loggerwindow.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +""" +History log window +""" +import logging + +from PySide import QtGui +from ui_loggerwindow import Ui_LoggerWindow +from leap.common.check import leap_assert, leap_assert_type +from leap.util.leap_log_handler import LeapLogHandler + +logger = logging.getLogger(__name__) + + +class LoggerWindow(QtGui.QWidget): +    """ +    Window that displays a history of the logged messages in the app. +    """ +    def __init__(self, handler): +        """ +        Initialize the widget with the custom handler. + +        :param handler: Custom handler that supports history and signal. +        :type handler: LeapLogHandler. +        """ +        QtGui.QWidget.__init__(self) +        leap_assert(handler, "We need a handler for the logger window") +        leap_assert_type(handler, LeapLogHandler) + +        # Load UI +        self.ui = Ui_LoggerWindow() +        self.ui.setupUi(self) + +        # Make connections +        self.ui.btnSave.clicked.connect(self._save_log_to_file) +        self.ui.btnDebug.toggled.connect(self._load_history), +        self.ui.btnInfo.toggled.connect(self._load_history), +        self.ui.btnWarning.toggled.connect(self._load_history), +        self.ui.btnError.toggled.connect(self._load_history), +        self.ui.btnCritical.toggled.connect(self._load_history) + +        # Load logging history and connect logger with the widget +        self._logging_handler = handler +        self._connect_to_handler() +        self._load_history() + +    def _connect_to_handler(self): +        """ +        This method connects the loggerwindow with the handler through a +        signal communicate the logger events. +        """ +        self._logging_handler.new_log.connect(self._add_log_line) + +    def _add_log_line(self, log): +        """ +        Adds a line to the history, only if it's in the desired levels to show. + +        :param log: a log record to be inserted in the widget +        :type log: a dict with RECORD_KEY and MESSAGE_KEY. +            the record contains the LogRecord of the logging module, +            the message contains the formatted message for the log. +        """ +        level = log[LeapLogHandler.RECORD_KEY].levelname +        message = log[LeapLogHandler.MESSAGE_KEY] + +        if self._logs_to_display[level]: +            self.ui.txtLogHistory.append(message) + +    def _load_history(self): +        """ +        Load the previous logged messages in the widget. +        They are stored in the custom handler. +        """ +        self._set_logs_to_display() +        self.ui.txtLogHistory.clear() +        history = self._logging_handler.log_history +        for line in history: +            self._add_log_line(line) + +    def _set_logs_to_display(self): +        """ +        Sets the logs_to_display dict getting the toggled options from the ui +        """ +        self._logs_to_display = { +            'DEBUG': self.ui.btnDebug.isChecked(), +            'INFO': self.ui.btnInfo.isChecked(), +            'WARNING': self.ui.btnWarning.isChecked(), +            'ERROR': self.ui.btnError.isChecked(), +            'CRITICAL': self.ui.btnCritical.isChecked() +        } + +    def _save_log_to_file(self): +        """ +        Lets the user save the current log to a file +        """ +        fileName, filtr = QtGui.QFileDialog.getSaveFileName( +            self, self.tr("Save As")) + +        if fileName: +            try: +                with open(fileName, 'w') as output: +                    output.write(self.ui.txtLogHistory.toPlainText()) +                    output.write('\n') +                logger.debug('Log saved in %s' % (fileName, )) +            except IOError, e: +                logger.error("Error saving log file: %r" % (e, )) +        else: +            logger.debug('Log not saved!') diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index a8aa1b1c..12187f51 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -35,6 +35,7 @@ from leap.common.events import events_pb2 as proto  from leap.config.leapsettings import LeapSettings  from leap.config.providerconfig import ProviderConfig  from leap.crypto.srpauth import SRPAuth +from leap.gui.loggerwindow import LoggerWindow  from leap.gui.wizard import Wizard  from leap.services.eip.eipbootstrapper import EIPBootstrapper  from leap.services.eip.eipconfig import EIPConfig @@ -208,6 +209,7 @@ class MainWindow(QtGui.QMainWindow):          self.ui.action_about_leap.triggered.connect(self._about)          self.ui.action_quit.triggered.connect(self.quit)          self.ui.action_wizard.triggered.connect(self._launch_wizard) +        self.ui.action_show_logs.triggered.connect(self._show_logger_window)          self.raise_window.connect(self._do_raise_mainwindow)          # Used to differentiate between real quits and close to tray @@ -249,6 +251,8 @@ class MainWindow(QtGui.QMainWindow):          self._wizard = None          self._wizard_firstrun = False +        self._logger_window = None +          self._bypass_checks = bypass_checks          self._soledad = None @@ -282,6 +286,35 @@ class MainWindow(QtGui.QMainWindow):          self._wizard.exec_()          self._wizard = None +    def _get_leap_logging_handler(self): +        """ +        Gets the leap handler from the top level logger + +        :return: a logging handler or None +        :rtype: LeapLogHandler or None +        """ +        from leap.util.leap_log_handler import LeapLogHandler +        leap_logger = logging.getLogger('leap') +        for h in leap_logger.handlers: +            if isinstance(h, LeapLogHandler): +                return h +        return None + +    def _show_logger_window(self): +        """ +        Displays the window with the history of messages logged until now +        and displays the new ones on arrival. +        """ +        if self._logger_window is None: +            leap_log_handler = self._get_leap_logging_handler() +            if leap_log_handler is None: +                logger.error('Leap logger handler not found') +            else: +                self._logger_window = LoggerWindow(handler=leap_log_handler) +                self._logger_window.show() +        else: +            self._logger_window.show() +      def _remember_state_changed(self, state):          enable = True if state == QtCore.Qt.Checked else False          self.ui.chkAutoLogin.setEnabled(enable) @@ -1101,6 +1134,10 @@ class MainWindow(QtGui.QMainWindow):          self._really_quit = True          if self._wizard:              self._wizard.close() + +        if self._logger_window: +            self._logger_window.close() +          self.close()          if self._quit_callback: diff --git a/src/leap/gui/ui/loggerwindow.ui b/src/leap/gui/ui/loggerwindow.ui new file mode 100644 index 00000000..28325cdf --- /dev/null +++ b/src/leap/gui/ui/loggerwindow.ui @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LoggerWindow</class> + <widget class="QWidget" name="LoggerWindow"> +  <property name="geometry"> +   <rect> +    <x>0</x> +    <y>0</y> +    <width>648</width> +    <height>469</height> +   </rect> +  </property> +  <property name="windowTitle"> +   <string>Logs</string> +  </property> +  <property name="windowIcon"> +   <iconset resource="../../../../data/resources/mainwindow.qrc"> +    <normaloff>:/images/leap-color-big.png</normaloff>:/images/leap-color-big.png</iconset> +  </property> +  <layout class="QGridLayout" name="gridLayout"> +   <item row="2" column="0" colspan="2"> +    <widget class="QTextBrowser" name="txtLogHistory"/> +   </item> +   <item row="0" column="0" colspan="2"> +    <layout class="QHBoxLayout" name="horizontalLayout_2"> +     <item> +      <widget class="QPushButton" name="btnDebug"> +       <property name="text"> +        <string>Debug</string> +       </property> +       <property name="icon"> +        <iconset resource="../../../../data/resources/loggerwindow.qrc"> +         <normaloff>:/images/oxygen-icons/script-error.png</normaloff>:/images/oxygen-icons/script-error.png</iconset> +       </property> +       <property name="checkable"> +        <bool>true</bool> +       </property> +       <property name="checked"> +        <bool>true</bool> +       </property> +       <property name="flat"> +        <bool>true</bool> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QPushButton" name="btnInfo"> +       <property name="text"> +        <string>Info</string> +       </property> +       <property name="icon"> +        <iconset resource="../../../../data/resources/loggerwindow.qrc"> +         <normaloff>:/images/oxygen-icons/dialog-information.png</normaloff>:/images/oxygen-icons/dialog-information.png</iconset> +       </property> +       <property name="checkable"> +        <bool>true</bool> +       </property> +       <property name="checked"> +        <bool>true</bool> +       </property> +       <property name="flat"> +        <bool>true</bool> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QPushButton" name="btnWarning"> +       <property name="text"> +        <string>Warning</string> +       </property> +       <property name="icon"> +        <iconset resource="../../../../data/resources/loggerwindow.qrc"> +         <normaloff>:/images/oxygen-icons/dialog-warning.png</normaloff>:/images/oxygen-icons/dialog-warning.png</iconset> +       </property> +       <property name="checkable"> +        <bool>true</bool> +       </property> +       <property name="checked"> +        <bool>true</bool> +       </property> +       <property name="flat"> +        <bool>true</bool> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QPushButton" name="btnError"> +       <property name="text"> +        <string>Error</string> +       </property> +       <property name="icon"> +        <iconset resource="../../../../data/resources/loggerwindow.qrc"> +         <normaloff>:/images/oxygen-icons/dialog-error.png</normaloff>:/images/oxygen-icons/dialog-error.png</iconset> +       </property> +       <property name="checkable"> +        <bool>true</bool> +       </property> +       <property name="checked"> +        <bool>true</bool> +       </property> +       <property name="flat"> +        <bool>true</bool> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QPushButton" name="btnCritical"> +       <property name="text"> +        <string>Critical</string> +       </property> +       <property name="icon"> +        <iconset resource="../../../../data/resources/loggerwindow.qrc"> +         <normaloff>:/images/oxygen-icons/edit-bomb.png</normaloff>:/images/oxygen-icons/edit-bomb.png</iconset> +       </property> +       <property name="checkable"> +        <bool>true</bool> +       </property> +       <property name="checked"> +        <bool>true</bool> +       </property> +       <property name="flat"> +        <bool>true</bool> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QPushButton" name="btnSave"> +       <property name="text"> +        <string>Save to file</string> +       </property> +       <property name="icon"> +        <iconset resource="../../../../data/resources/loggerwindow.qrc"> +         <normaloff>:/images/oxygen-icons/document-save-as.png</normaloff>:/images/oxygen-icons/document-save-as.png</iconset> +       </property> +      </widget> +     </item> +    </layout> +   </item> +  </layout> + </widget> + <tabstops> +  <tabstop>btnDebug</tabstop> +  <tabstop>btnInfo</tabstop> +  <tabstop>btnWarning</tabstop> +  <tabstop>btnError</tabstop> +  <tabstop>btnCritical</tabstop> +  <tabstop>btnSave</tabstop> +  <tabstop>txtLogHistory</tabstop> + </tabstops> + <resources> +  <include location="../../../../data/resources/loggerwindow.qrc"/> +  <include location="../../../../data/resources/mainwindow.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index b42ff180..fdf5c167 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -396,7 +396,7 @@       <x>0</x>       <y>0</y>       <width>415</width> -     <height>25</height> +     <height>21</height>      </rect>     </property>     <widget class="QMenu" name="menuSession"> @@ -417,9 +417,10 @@     </widget>     <widget class="QMenu" name="menuSettings">      <property name="title"> -     <string>S&ettings</string> +     <string>&Utils</string>      </property>      <addaction name="action_wizard"/> +    <addaction name="action_show_logs"/>     </widget>     <addaction name="menuSession"/>     <addaction name="menuSettings"/> @@ -451,6 +452,11 @@      <string>&Wizard</string>     </property>    </action> +  <action name="action_show_logs"> +   <property name="text"> +    <string>Show &logs</string> +   </property> +  </action>   </widget>   <tabstops>    <tabstop>lnUser</tabstop> diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py new file mode 100644 index 00000000..0e598032 --- /dev/null +++ b/src/leap/util/leap_log_handler.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# leap_log_handler.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +""" +Custom handler for the logger window. +""" +import logging + +from PySide import QtCore + + +class LeapLogHandler(logging.Handler, QtCore.QObject): +    """ +    Custom logging handler. It emits Qt signals so it can be plugged to a gui. +    Also stores an history of logs that can be fetched after connect to a gui. +    """ +    # All dicts returned are of the form +    # {'record': LogRecord, 'message': str} +    new_log = QtCore.Signal(dict) + +    MESSAGE_KEY = 'message' +    RECORD_KEY = 'record' + +    def __init__(self): +        logging.Handler.__init__(self) +        QtCore.QObject.__init__(self) + +        self._log_history = [] + +    def _set_format(self, logging_level): +        """ +        Sets the log format depending on the parameter. +        It uses html and css to set the colors for the logs. + +        :param logging_level: the debug level to define the color. +        :type logging_level: str. +        """ +        html_style = { +            'DEBUG': "color: blue", +            'INFO': "color: black", +            'WARNING': "color: black; background: yellow;", +            'ERROR': "color: red", +            'CRITICAL': "color: red; font-weight: bold;" +        } + +        style_open = "<span style='" + html_style[logging_level] + "'>" +        style_close = "</span>" +        time = "%(asctime)s" +        name = style_open + "%(name)s" +        level = "%(levelname)s" +        message = "%(message)s" + style_close +        format_attrs = [time, name, level, message] +        log_format = ' - '.join(format_attrs) +        formatter = logging.Formatter(log_format) +        self.setFormatter(formatter) + +    def emit(self, logRecord): +        """ +        This method is fired every time that a record is logged by the +        logging module. +        This method reimplements logging.Handler.emit that is fired +        in every logged message. +        QObject.emit gets in the way on the PySide signal model but we +        workarouded that issue. + +        :param logRecord: the record emitted by the logging module. +        :type logRecord: logging.LogRecord. +        """ +        self._set_format(logRecord.levelname) +        log = self.format(logRecord) +        log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log} +        self._log_history.append(log_item) + +        # WARNING: the new-style connection does NOT work because PySide +        # translates the emit method to self.emit, and that collides with +        # the emit method for logging.Handler +        # self.new_log.emit(log_item) +        QtCore.QObject.emit(self, QtCore.SIGNAL('new_log(PyObject)'), log_item) + +    @property +    def log_history(self): +        """ +        Returns the history of the logged messages. +        """ +        return self._log_history | 
