mirror of
https://github.com/olivierkes/manuskript.git
synced 2024-05-10 16:02:33 +12:00
Friendly logging for end users (#859)
* Changed default logging behaviour We now log by default to a timestamped file in $datadir/logs/. No longer shall restarting Manuskript after a crash wipe a very useful logfile. Logs older than 35 days in the $datadir/logs/ directory are pruned during startup. In case of subtle corruption detected a few weeks after the fact, relevant logs might still exist to explain what had happened... yet it does not come at the cost of infinitely gobbling up a users storage space, either. The --logfile (-L) argument can now utilize strftime() specifiers. A special modifier %# is also supported which will insert the process id. Besides being an added factor of uniqueness for a filename, it can also be relevant to help identify the log file belonging to a misbehaving Manuskript process. * Added support-related items to Help menu The 'Technical Support' item should lead to a landing page that will guide the user to the most efficient way to resolve their problem. How to report bugs and submit logs would be one of those. The 'Locate Log File' item should open a file manager window with the logfile of this session highlighted. Because Manuskript is still writing to it, we first remind them of its limited use until Manuskript is closed. This approach was chosen because users might want to locate the file prior to reproducing a bug, or because they'd like to look at other logs from previous sessions. * Updated translation files and added german translation Co-authored-by: TheJackiMonster <thejackimonster@gmail.com>
This commit is contained in:
parent
0d67c15c9c
commit
b2817b5f08
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -7,8 +7,8 @@ import sys
|
|||
import pathlib
|
||||
from random import *
|
||||
|
||||
from PyQt5.QtCore import Qt, QRect, QStandardPaths, QObject, QRegExp, QDir
|
||||
from PyQt5.QtCore import QUrl, QTimer
|
||||
from PyQt5.QtCore import Qt, QRect, QStandardPaths, QObject, QProcess, QRegExp
|
||||
from PyQt5.QtCore import QDir, QUrl, QTimer
|
||||
from PyQt5.QtGui import QBrush, QIcon, QPainter, QColor, QImage, QPixmap
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtWidgets import qApp, QFileDialog
|
||||
|
@ -540,5 +540,41 @@ def getGitRevisionAsString(base_path, short=False):
|
|||
LOGGER.warning("Failed to obtain Git revision: %s", e)
|
||||
return "#ERROR"
|
||||
|
||||
def showInFolder(path, open_file_as_fallback=False):
|
||||
'''
|
||||
Show a file or folder in explorer/finder, highlighting it where possible.
|
||||
Source: https://stackoverflow.com/a/46019091/3388962
|
||||
'''
|
||||
path = os.path.abspath(path)
|
||||
dirPath = path if os.path.isdir(path) else os.path.dirname(path)
|
||||
if sys.platform == 'win32':
|
||||
args = []
|
||||
args.append('/select,')
|
||||
args.append(QDir.toNativeSeparators(path))
|
||||
if QProcess.startDetached('explorer', args):
|
||||
return True
|
||||
elif sys.platform == 'darwin':
|
||||
args = []
|
||||
args.append('-e')
|
||||
args.append('tell application "Finder"')
|
||||
args.append('-e')
|
||||
args.append('activate')
|
||||
args.append('-e')
|
||||
args.append('select POSIX file "%s"' % path)
|
||||
args.append('-e')
|
||||
args.append('end tell')
|
||||
args.append('-e')
|
||||
args.append('return')
|
||||
if not QProcess.execute('/usr/bin/osascript', args):
|
||||
return True
|
||||
#if not QtCore.QProcess.execute('/usr/bin/open', [dirPath]):
|
||||
# return
|
||||
# TODO: Linux is not implemented. It has many file managers (nautilus, xdg-open, etc.)
|
||||
# each of which needs special ways to highlight a file in a file manager window.
|
||||
|
||||
# Fallback.
|
||||
return QDesktopServices.openUrl(QUrl(path if open_file_as_fallback else dirPath))
|
||||
|
||||
|
||||
# Spellchecker loads writablePath from this file, so we need to load it after they get defined
|
||||
from manuskript.functions.spellchecker import Spellchecker
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
|
@ -48,6 +49,62 @@ def setUp(console_level=logging.WARN):
|
|||
LOGGER.debug("Logging to STDERR.")
|
||||
|
||||
|
||||
def getDefaultLogFile():
|
||||
"""Returns a filename to log to inside {datadir}/logs/.
|
||||
|
||||
It also prunes old logs so that we do not hog disk space excessively over time.
|
||||
"""
|
||||
# Ensure logs directory exists.
|
||||
logsPath = os.path.join(writablePath(), "logs")
|
||||
os.makedirs(logsPath, exist_ok=True)
|
||||
# Prune irrelevant log files. They are only kept for 35 days.
|
||||
try: # Guard against os.scandir() in the name of paranoia.
|
||||
now = time.time()
|
||||
with os.scandir(logsPath) as it:
|
||||
for f in it:
|
||||
try: # Avoid triggering outer try-except inside loop.
|
||||
if f.is_dir():
|
||||
continue # If a subdirectory exists for whatever reason, don't touch it.
|
||||
if (now - f.stat().st_ctime) // (24 * 3600) >= 35:
|
||||
os.remove(f)
|
||||
except OSError:
|
||||
continue # Fail silently, but make sure we check other files.
|
||||
except OSError:
|
||||
pass # Fail silently. Don't explode and prevent Manuskript from starting.
|
||||
return os.path.join(logsPath, "%Y-%m-%d_%H-%M-%S_manuskript#%#.log")
|
||||
|
||||
|
||||
def formatLogName(formatString, pid=None, now=None):
|
||||
"""A minor hack on top of `strftime()` to support an identifier for the process ID.
|
||||
|
||||
We want to support this in case some genius manages to start two manuskript processes
|
||||
during the exact same second, causing a conflict in log filenames.
|
||||
|
||||
Additionally, there is a tiny chance that the pid could actually end up relevant when
|
||||
observing strange behaviour with a Manuskript process but having multiple instances open.
|
||||
"""
|
||||
if pid == None:
|
||||
pid = os.getpid()
|
||||
if now == None:
|
||||
now = time.localtime()
|
||||
|
||||
# Replace %# that is NOT preceded by %. Although this is not a perfect solution,
|
||||
# it is good enough because it is unlikely anyone would want to format '%pid'.
|
||||
lidx = 0
|
||||
while True: # This could be neater with the := operator of Python 3.8 ...
|
||||
fidx = formatString.find("%#", lidx)
|
||||
if fidx == -1:
|
||||
break
|
||||
elif (fidx == 0) or (formatString[fidx-1] != "%"):
|
||||
formatString = formatString[:fidx] + str(pid) + formatString[fidx+2:]
|
||||
lidx = fidx + len(str(pid)) - 2
|
||||
else: # skip and avoid endless loop
|
||||
lidx = fidx + 1
|
||||
|
||||
# Finally apply strftime normally.
|
||||
return time.strftime(formatString, now)
|
||||
|
||||
|
||||
def logToFile(file_level=logging.DEBUG, logfile=None):
|
||||
"""Sets up the FileHandler that logs to a file.
|
||||
|
||||
|
@ -57,7 +114,9 @@ def logToFile(file_level=logging.DEBUG, logfile=None):
|
|||
To log file: >DEBUG, timestamped. (All the details.)"""
|
||||
|
||||
if logfile is None:
|
||||
logfile = os.path.join(writablePath(), "manuskript.log")
|
||||
logfile = getDefaultLogFile()
|
||||
|
||||
logfile = formatLogName(logfile)
|
||||
|
||||
# Log with extreme prejudice; everything goes to the log file.
|
||||
# Because Qt gave me a megabyte-sized logfile while testing, it
|
||||
|
@ -77,6 +136,16 @@ def logToFile(file_level=logging.DEBUG, logfile=None):
|
|||
except Exception as ex:
|
||||
LOGGER.warning("Cannot log to file '%s'. Reason: %s", logfile, ex)
|
||||
|
||||
|
||||
def getLogFilePath():
|
||||
"""Extracts a filename we are logging to from the first FileHandler we find."""
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
return handler.baseFilename
|
||||
return None
|
||||
|
||||
|
||||
# Log uncaught and unraisable exceptions.
|
||||
|
||||
# Uncaught exceptions trigger moments before a thread is terminated due to
|
||||
|
|
|
@ -13,9 +13,10 @@ from PyQt5.QtWidgets import QMainWindow, QHeaderView, qApp, QMenu, QActionGroup,
|
|||
|
||||
from manuskript import settings
|
||||
from manuskript.enums import Character, PlotStep, Plot, World, Outline
|
||||
from manuskript.functions import wordCount, appPath, findWidgetsOfClass
|
||||
from manuskript.functions import wordCount, appPath, findWidgetsOfClass, openURL, showInFolder
|
||||
import manuskript.functions as F
|
||||
from manuskript import loadSave
|
||||
from manuskript.logging import getLogFilePath
|
||||
from manuskript.models.characterModel import characterModel
|
||||
from manuskript.models import outlineModel
|
||||
from manuskript.models.plotModel import plotModel
|
||||
|
@ -179,6 +180,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||
|
||||
# Main Menu:: Tool
|
||||
self.actToolFrequency.triggered.connect(self.frequencyAnalyzer)
|
||||
self.actSupport.triggered.connect(self.support)
|
||||
self.actLocateLog.triggered.connect(self.locateLogFile)
|
||||
self.actAbout.triggered.connect(self.about)
|
||||
|
||||
self.makeUIConnections()
|
||||
|
@ -1179,6 +1182,50 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||
r2 = self.geometry()
|
||||
win.move(r2.center() - QPoint(r.width()/2, r.height()/2))
|
||||
|
||||
def support(self):
|
||||
openURL("https://github.com/olivierkes/manuskript/wiki/Technical-Support")
|
||||
|
||||
def locateLogFile(self):
|
||||
logfile = getLogFilePath()
|
||||
|
||||
# Make sure we are even logging to a file.
|
||||
if not logfile:
|
||||
QMessageBox(QMessageBox.Information,
|
||||
self.tr("Sorry!"),
|
||||
"<p><b>" +
|
||||
self.tr("This session is not being logged.") +
|
||||
"</b></p>",
|
||||
QMessageBox.Ok).exec()
|
||||
return
|
||||
|
||||
# Remind user that log files are at their best once they are complete.
|
||||
msg = QMessageBox(QMessageBox.Information,
|
||||
self.tr("A log file is a Work in Progress!"),
|
||||
"<p><b>" +
|
||||
self.tr("The log file \"{}\" will continue to be written to until Manuskript is closed.").format(os.path.basename(logfile)) +
|
||||
"</b></p>" +
|
||||
"<p>" +
|
||||
self.tr("It will now be displayed in your file manager, but is of limited use until you close Manuskript.") +
|
||||
"</p>",
|
||||
QMessageBox.Ok)
|
||||
|
||||
ret = msg.exec()
|
||||
|
||||
# Open the filemanager.
|
||||
if ret == QMessageBox.Ok:
|
||||
if not showInFolder(logfile):
|
||||
# If everything convenient fails, at least make sure the user can browse to its location manually.
|
||||
QMessageBox(QMessageBox.Critical,
|
||||
self.tr("Error!"),
|
||||
"<p><b>" +
|
||||
self.tr("An error was encountered while trying to show the log file below in your file manager.") +
|
||||
"</b></p>" +
|
||||
"<p>" +
|
||||
logfile +
|
||||
"</p>",
|
||||
QMessageBox.Ok).exec()
|
||||
|
||||
|
||||
def about(self):
|
||||
self.dialog = aboutDialog(mw=self)
|
||||
self.dialog.setFixedSize(self.dialog.size())
|
||||
|
|
|
@ -381,7 +381,7 @@ class Ui_MainWindow(object):
|
|||
self.scrollAreaPersoInfos.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
|
||||
self.scrollAreaPersoInfos.setObjectName("scrollAreaPersoInfos")
|
||||
self.scrollAreaPersoInfosWidget = QtWidgets.QWidget()
|
||||
self.scrollAreaPersoInfosWidget.setGeometry(QtCore.QRect(0, 0, 453, 695))
|
||||
self.scrollAreaPersoInfosWidget.setGeometry(QtCore.QRect(0, 0, 429, 719))
|
||||
self.scrollAreaPersoInfosWidget.setObjectName("scrollAreaPersoInfosWidget")
|
||||
self.formLayout_8 = QtWidgets.QFormLayout(self.scrollAreaPersoInfosWidget)
|
||||
self.formLayout_8.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
|
||||
|
@ -757,7 +757,7 @@ class Ui_MainWindow(object):
|
|||
self.treeWorld.setRootIsDecorated(False)
|
||||
self.treeWorld.setObjectName("treeWorld")
|
||||
self.treeWorld.header().setVisible(False)
|
||||
self.treeWorld.header().setDefaultSectionSize(25)
|
||||
self.treeWorld.header().setDefaultSectionSize(35)
|
||||
self.verticalLayout_32.addWidget(self.treeWorld)
|
||||
self.horizontalLayout_19 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_19.setObjectName("horizontalLayout_19")
|
||||
|
@ -1042,7 +1042,7 @@ class Ui_MainWindow(object):
|
|||
self.horizontalLayout_2.addWidget(self.stack)
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
self.menubar = QtWidgets.QMenuBar(MainWindow)
|
||||
self.menubar.setGeometry(QtCore.QRect(0, 0, 1112, 24))
|
||||
self.menubar.setGeometry(QtCore.QRect(0, 0, 1112, 21))
|
||||
self.menubar.setObjectName("menubar")
|
||||
self.menuFile = QtWidgets.QMenu(self.menubar)
|
||||
self.menuFile.setObjectName("menuFile")
|
||||
|
@ -1287,6 +1287,10 @@ class Ui_MainWindow(object):
|
|||
icon = QtGui.QIcon.fromTheme("edit-find")
|
||||
self.actSearch.setIcon(icon)
|
||||
self.actSearch.setObjectName("actSearch")
|
||||
self.actSupport = QtWidgets.QAction(MainWindow)
|
||||
self.actSupport.setObjectName("actSupport")
|
||||
self.actLocateLog = QtWidgets.QAction(MainWindow)
|
||||
self.actLocateLog.setObjectName("actLocateLog")
|
||||
self.menuFile.addAction(self.actOpen)
|
||||
self.menuFile.addAction(self.menuRecents.menuAction())
|
||||
self.menuFile.addAction(self.actSave)
|
||||
|
@ -1298,6 +1302,10 @@ class Ui_MainWindow(object):
|
|||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actQuit)
|
||||
self.menuHelp.addAction(self.actShowHelp)
|
||||
self.menuHelp.addSeparator()
|
||||
self.menuHelp.addAction(self.actSupport)
|
||||
self.menuHelp.addAction(self.actLocateLog)
|
||||
self.menuHelp.addSeparator()
|
||||
self.menuHelp.addAction(self.actAbout)
|
||||
self.menuTools.addAction(self.actSpellcheck)
|
||||
self.menuTools.addAction(self.actToolFrequency)
|
||||
|
@ -1656,6 +1664,13 @@ class Ui_MainWindow(object):
|
|||
self.actFormatBlockquote.setText(_translate("MainWindow", "B&lockquote"))
|
||||
self.actSearch.setText(_translate("MainWindow", "Search"))
|
||||
self.actSearch.setShortcut(_translate("MainWindow", "Ctrl+F"))
|
||||
self.actSupport.setText(_translate("MainWindow", "&Technical Support"))
|
||||
self.actSupport.setToolTip(_translate("MainWindow", "How to obtain technical support for Manuskript."))
|
||||
self.actSupport.setShortcut(_translate("MainWindow", "F1"))
|
||||
self.actLocateLog.setText(_translate("MainWindow", "&Locate log file..."))
|
||||
self.actLocateLog.setIconText(_translate("MainWindow", "Locate log file"))
|
||||
self.actLocateLog.setToolTip(_translate("MainWindow", "Locate the diagnostic log file used for this session."))
|
||||
self.actLocateLog.setShortcut(_translate("MainWindow", "Shift+F1"))
|
||||
from manuskript.ui.cheatSheet import cheatSheet
|
||||
from manuskript.ui.editors.mainEditor import mainEditor
|
||||
from manuskript.ui.search import search
|
||||
|
|
|
@ -815,8 +815,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>453</width>
|
||||
<height>695</height>
|
||||
<width>429</width>
|
||||
<height>719</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_8">
|
||||
|
@ -1567,7 +1567,7 @@
|
|||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="headerDefaultSectionSize">
|
||||
<number>25</number>
|
||||
<number>35</number>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -2115,7 +2115,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1112</width>
|
||||
<height>24</height>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
|
@ -2147,6 +2147,10 @@
|
|||
<string>&Help</string>
|
||||
</property>
|
||||
<addaction name="actShowHelp"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actSupport"/>
|
||||
<addaction name="actLocateLog"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actAbout"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuTools">
|
||||
|
@ -2841,7 +2845,8 @@
|
|||
</action>
|
||||
<action name="actSearch">
|
||||
<property name="icon">
|
||||
<iconset theme="edit-find"/>
|
||||
<iconset theme="edit-find">
|
||||
<normaloff>.</normaloff>.</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
|
@ -2850,6 +2855,31 @@
|
|||
<string>Ctrl+F</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actSupport">
|
||||
<property name="text">
|
||||
<string>&Technical Support</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>How to obtain technical support for Manuskript.</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F1</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actLocateLog">
|
||||
<property name="text">
|
||||
<string>&Locate log file...</string>
|
||||
</property>
|
||||
<property name="iconText">
|
||||
<string>Locate log file</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Locate the diagnostic log file used for this session.</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Shift+F1</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
|
Loading…
Reference in a new issue