# -*- coding: utf-8 -*-
# logwindow.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 .
"""
History log window
"""
import cgi
from PySide import QtCore, QtGui
import logbook
from logbook.queues import ZeroMQSubscriber
from ui_loggerwindow import Ui_LoggerWindow
from leap.bitmask.logs import LOG_FORMAT
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY
from leap.bitmask.util import pastebin
logger = get_logger()
# log history global variable used to store received logs through different
# opened instances of this window
_LOGS_HISTORY = []
class QtLogHandler(logbook.Handler, logbook.StringFormatterHandlerMixin):
"""
Custom log handler which emits a log record with the message properly
formatted using a Qt Signal.
"""
class _QtSignaler(QtCore.QObject):
"""
inline class used to hold the `new_log` Signal, if this is used
directly in the outside class it fails due how PySide works.
This is the message we get if not use this method:
TypeError: Error when calling the metaclass bases
metaclass conflict: the metaclass of a derived class must be a
(non-strict) subclass of the metaclasses of all its bases
"""
new_log = QtCore.Signal(object)
def emit(self, data):
"""
emit the `new_log` Signal with the given `data` parameter.
:param data: the data to emit along with the signal.
:type data: object
"""
# 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)'), data)
def __init__(self, level=logbook.NOTSET, format_string=None,
encoding=None, filter=None, bubble=False):
logbook.Handler.__init__(self, level, filter, bubble)
logbook.StringFormatterHandlerMixin.__init__(self, format_string)
self.qt = self._QtSignaler()
def __enter__(self):
return logbook.Handler.__enter__(self)
def __exit__(self, exc_type, exc_value, tb):
return logbook.Handler.__exit__(self, exc_type, exc_value, tb)
def emit(self, record):
"""
Emit the specified logging record using a Qt Signal.
Also add it to the history in order to be able to access it later.
:param record: the record to emit
:type record: logbook.LogRecord
"""
global _LOGS_HISTORY
record.msg = self.format(record)
# NOTE: not optimal approach, we may want to look at
# bisect.insort with a custom approach to use key or
# http://code.activestate.com/recipes/577197-sortedcollection/
# Sort logs on arrival, logs transmitted over zmq may arrive unsorted.
_LOGS_HISTORY.append(record)
_LOGS_HISTORY = sorted(_LOGS_HISTORY, key=lambda r: r.time)
# XXX: emitting the record on arrival does not allow us to sort here so
# in the GUI the logs may arrive with with some time sort problem.
# We should implement a sort-on-arrive for the log window.
# Maybe we should switch to a tablewidget item that sort automatically
# by timestamp.
# As a user workaround you can close/open the log window
self.qt.emit(record)
class LoggerWindow(QtGui.QDialog):
"""
Window that displays a history of the logged messages in the app.
"""
_paste_ok = QtCore.Signal(object)
_paste_error = QtCore.Signal(object)
def __init__(self, parent):
"""
Initialize the widget.
"""
QtGui.QDialog.__init__(self, parent)
# 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)
self.ui.leFilterBy.textEdited.connect(self._filter_by)
self.ui.cbCaseInsensitive.stateChanged.connect(self._load_history)
self.ui.btnPastebin.clicked.connect(self._pastebin_this)
self._paste_ok.connect(self._pastebin_ok)
self._paste_error.connect(self._pastebin_err)
self._current_filter = ""
self._current_history = ""
self._set_logs_to_display()
self._my_handler = QtLogHandler(format_string=LOG_FORMAT)
self._my_handler.qt.new_log.connect(self._add_log_line)
self._load_history()
self._connect_to_logbook()
def _connect_to_logbook(self):
"""
Run in the background the log receiver.
"""
subscriber = ZeroMQSubscriber('tcp://127.0.0.1:5000', multi=True)
self._logbook_controller = subscriber.dispatch_in_background(
self._my_handler)
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: Logbook.LogRecord.
"""
html_style = {
logbook.DEBUG: "background: #CDFFFF;",
logbook.INFO: "background: white;",
logbook.WARNING: "background: #FFFF66;",
logbook.ERROR: "background: red; color: white;",
logbook.CRITICAL: "background: red; color: white; font: bold;"
}
level = log.level
message = cgi.escape(log.msg)
if self._logs_to_display[level]:
open_tag = "
"
open_tag += ""
close_tag = " |
"
message = open_tag + message + close_tag
filter_by = self._current_filter
msg = message
if self.ui.cbCaseInsensitive.isChecked():
msg = msg.upper()
filter_by = filter_by.upper()
if msg.find(filter_by) != -1:
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()
current_history = []
for record in _LOGS_HISTORY:
self._add_log_line(record)
current_history.append(record.msg)
self._current_history = "\n".join(current_history)
def _set_logs_to_display(self):
"""
Sets the logs_to_display dict getting the toggled options from the ui
"""
self._logs_to_display = {
logbook.DEBUG: self.ui.btnDebug.isChecked(),
logbook.INFO: self.ui.btnInfo.isChecked(),
logbook.WARNING: self.ui.btnWarning.isChecked(),
logbook.ERROR: self.ui.btnError.isChecked(),
logbook.CRITICAL: self.ui.btnCritical.isChecked()
}
def _filter_by(self, text):
"""
Sets the text to use for filtering logs in the log window.
:param text: the text to compare with the logs when filtering.
:type text: str
"""
self._current_filter = text
self._load_history()
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"),
options=QtGui.QFileDialog.DontUseNativeDialog)
if fileName:
try:
with open(fileName, 'w') as output:
history = self.ui.txtLogHistory.toPlainText()
# Chop some \n.
# html->plain adds several \n because the html is made
# using table cells.
history = history.replace('\n\n\n', '\n')
output.write(history)
logger.debug('Log saved in %s' % (fileName, ))
except IOError, e:
logger.error("Error saving log file: %r" % (e, ))
else:
logger.debug('Log not saved!')
def _set_pastebin_sending(self, sending):
"""
Define the status of the pastebin button.
Change the text and enable/disable according to the current action.
:param sending: if we are sending to pastebin or not.
:type sending: bool
"""
if sending:
self.ui.btnPastebin.setText(self.tr("Sending to pastebin..."))
self.ui.btnPastebin.setEnabled(False)
else:
self.ui.btnPastebin.setText(self.tr("Send to Pastebin.com"))
self.ui.btnPastebin.setEnabled(True)
def _pastebin_ok(self, link):
"""
Handle a successful paste.
:param link: the recently created pastebin link.
:type link: str
"""
self._set_pastebin_sending(False)
msg = self.tr("Your pastebin link {0}")
msg = msg.format(link)
# We save the dialog in an instance member to avoid dialog being
# deleted right after we exit this method
self._msgBox = msgBox = QtGui.QMessageBox(
QtGui.QMessageBox.Information, self.tr("Pastebin OK"), msg)
msgBox.setWindowModality(QtCore.Qt.NonModal)
msgBox.show()
def _pastebin_err(self, failure):
"""
Handle a failure in paste.
:param failure: the exception that made the paste fail.
:type failure: Exception
"""
self._set_pastebin_sending(False)
logger.error(repr(failure))
msg = self.tr("Sending logs to Pastebin failed!")
if isinstance(failure, pastebin.PostLimitError):
msg = self.tr('Maximum amount of submissions reached for today.')
# We save the dialog in an instance member to avoid dialog being
# deleted right after we exit this method
self._msgBox = msgBox = QtGui.QMessageBox(
QtGui.QMessageBox.Critical, self.tr("Pastebin Error"), msg)
msgBox.setWindowModality(QtCore.Qt.NonModal)
msgBox.show()
def _pastebin_this(self):
"""
Send the current log history to pastebin.com and gives the user a link
to see it.
"""
def do_pastebin():
"""
Send content to pastebin and return the link.
"""
content = self._current_history
pb = pastebin.PastebinAPI()
try:
link = pb.paste(PASTEBIN_API_DEV_KEY, content,
paste_name="Bitmask log",
paste_expire_date='1M')
# convert to 'raw' link
link = "http://pastebin.com/raw.php?i=" + link.split('/')[-1]
self._paste_ok.emit(link)
except Exception as e:
self._paste_error.emit(e)
self._set_pastebin_sending(True)
self._paste_thread = QtCore.QThread()
self._paste_thread.run = lambda: do_pastebin()
self._paste_thread.start()
def closeEvent(self, e):
"""
Disconnect logger on close.
"""
self._disconnect_logger()
e.accept()
def reject(self):
"""
Disconnect logger on reject.
"""
self._disconnect_logger()
QtGui.QDialog.reject(self)
def _disconnect_logger(self):
"""
Stop the background thread that receives messages through zmq, also
close the subscriber socket.
This allows us to re-create the subscriber when we reopen this window
without getting an error at trying to connect twice to the zmq port.
"""
self._logbook_controller.stop()
self._logbook_controller.subscriber.close()