Refactor saving logic, only save stuff if modified.

Fixes #113.
See #11.
This commit is contained in:
Florian Bruhin 2015-01-31 22:56:23 +01:00
parent d6e87a2672
commit 43c5dc3bf6
13 changed files with 238 additions and 85 deletions

View File

@ -32,7 +32,7 @@
|<<repeat,repeat>>|Repeat a given command.
|<<report,report>>|Report a bug in qutebrowser.
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|<<save,save>>|Save the config file.
|<<save,save>>|Save configs and state.
|<<search,search>>|Search for a text on the current page.
|<<set,set>>|Set an option.
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
@ -352,7 +352,13 @@ Restart qutebrowser while keeping existing tabs open.
[[save]]
=== save
Save the config file.
Syntax: +:save ['what' ['what' ...]]+
Save configs and state.
==== positional arguments
* +'what'+: What to save (`config`/`key-config`/`cookies`/...). If not given, everything is saved.
[[search]]
=== search

View File

@ -43,7 +43,7 @@ from qutebrowser.config import style, config, websettings
from qutebrowser.browser import quickmarks, cookies, cache, adblock
from qutebrowser.browser.network import qutescheme, proxy
from qutebrowser.mainwindow import mainwindow
from qutebrowser.misc import crashdialog, readline, ipc, earlyinit
from qutebrowser.misc import crashdialog, readline, ipc, earlyinit, savemanager
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
@ -162,14 +162,18 @@ class Application(QApplication):
def _init_modules(self):
"""Initialize all 'modules' which need to be initialized."""
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(self)
objreg.register('save-manager', save_manager)
log.init.debug("Initializing readline-bridge...")
readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge)
log.init.debug("Initializing directories...")
standarddir.init()
log.init.debug("Initializing config...")
config.init(self._args)
save_manager.add_saveable('window-geometry', self._save_geometry)
save_manager.add_saveable('version', self._save_version)
log.init.debug("Initializing crashlog...")
self._handle_segfault()
log.init.debug("Initializing js-bridge...")
@ -741,9 +745,6 @@ class Application(QApplication):
def _shutdown(self, status): # noqa
"""Second stage of shutdown."""
# pylint: disable=too-many-branches, too-many-statements
# FIXME refactor this
# https://github.com/The-Compiler/qutebrowser/issues/113
log.destroy.debug("Stage 2 of shutting down...")
# Remove eventfilter
try:
@ -760,59 +761,19 @@ class Application(QApplication):
pass
# Save everything
try:
config_obj = objreg.get('config')
save_manager = objreg.get('save-manager')
except KeyError:
log.destroy.debug("Config not initialized yet, so not saving "
"anything.")
log.destroy.debug("Save manager not initialized yet, so not "
"saving anything.")
else:
to_save = []
if config.get('general', 'auto-save-config'):
to_save.append(("config", config_obj.save))
for key in save_manager.saveables:
try:
key_config = objreg.get('key-config')
except KeyError:
pass
else:
to_save.append(("keyconfig", key_config.save))
to_save += [("window geometry", self._save_geometry)]
to_save += [("version", self._save_version)]
try:
command_history = objreg.get('command-history')
except KeyError:
pass
else:
to_save.append(("command history", command_history.save))
try:
quickmark_manager = objreg.get('quickmark-manager')
except KeyError:
pass
else:
to_save.append(("command history", quickmark_manager.save))
try:
state_config = objreg.get('state-config')
except KeyError:
pass
else:
to_save.append(("window geometry", state_config.save))
try:
cookie_jar = objreg.get('cookie-jar')
except KeyError:
pass
else:
to_save.append(("cookies", cookie_jar.save))
for what, handler in to_save:
log.destroy.debug("Saving {} (handler: {})".format(
what, utils.qualname(handler)))
try:
handler()
save_manager.save(key, is_exit=True)
except OSError as e:
msgbox = QMessageBox(
QMessageBox.Critical, "Error while saving!",
"Error while saving {}: {}".format(what, e))
"Error while saving {}: {}".format(key, e))
msgbox.exec_()
except AttributeError as e:
log.destroy.warning("Could not save {}.".format(what))
log.destroy.debug(e)
# Re-enable faulthandler to stdout, then remove crash log
log.destroy.debug("Deactiving crash log...")
self._destroy_crashlogfile()

View File

@ -20,7 +20,7 @@
"""Handling of HTTP cookies."""
from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar
from PyQt5.QtCore import QStandardPaths, QDateTime
from PyQt5.QtCore import pyqtSignal, QStandardPaths, QDateTime
from qutebrowser.config import config
from qutebrowser.config.parsers import line as lineparser
@ -29,7 +29,16 @@ from qutebrowser.utils import utils, standarddir, objreg
class RAMCookieJar(QNetworkCookieJar):
"""An in-RAM cookie jar."""
"""An in-RAM cookie jar.
Signals:
changed: Emitted when the cookie store was changed.
"""
changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
def __repr__(self):
return utils.get_repr(self, count=len(self.allCookies()))
@ -47,6 +56,7 @@ class RAMCookieJar(QNetworkCookieJar):
if config.get('content', 'cookies-accept') == 'never':
return False
else:
self.changed.emit()
return super().setCookiesFromUrl(cookies, url)
@ -62,12 +72,15 @@ class CookieJar(RAMCookieJar):
super().__init__(parent)
datadir = standarddir.get(QStandardPaths.DataLocation)
self._linecp = lineparser.LineConfigParser(datadir, 'cookies',
binary=True)
binary=True, parent=self)
cookies = []
for line in self._linecp:
cookies += QNetworkCookie.parseCookies(line)
self.setAllCookies(cookies)
objreg.get('config').changed.connect(self.cookies_store_changed)
objreg.get('save-manager').add_saveable(
'cookies', self.save, self.changed,
config_opt=('content', 'cookies-store'))
def purge_old_cookies(self):
"""Purge expired cookies from the cookie jar."""
@ -80,8 +93,6 @@ class CookieJar(RAMCookieJar):
def save(self):
"""Save cookies to disk."""
if not config.get('content', 'cookies-store'):
return
self.purge_old_cookies()
lines = []
for cookie in self.allCookies():
@ -96,3 +107,4 @@ class CookieJar(RAMCookieJar):
if not config.get('content', 'cookies-store'):
self._linecp.data = []
self._linecp.save()
self.changed.emit()

View File

@ -29,7 +29,7 @@ import collections
from PyQt5.QtCore import pyqtSignal, QStandardPaths, QUrl, QObject
from qutebrowser.utils import message, usertypes, urlutils, standarddir
from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.config.parsers import line as lineparser
@ -52,7 +52,8 @@ class QuickmarkManager(QObject):
self.marks = collections.OrderedDict()
confdir = standarddir.get(QStandardPaths.ConfigLocation)
self._linecp = lineparser.LineConfigParser(confdir, 'quickmarks')
self._linecp = lineparser.LineConfigParser(confdir, 'quickmarks',
parent=self)
for line in self._linecp:
try:
key, url = line.rsplit(maxsplit=1)
@ -60,6 +61,8 @@ class QuickmarkManager(QObject):
message.error(0, "Invalid quickmark '{}'".format(line))
else:
self.marks[key] = url
objreg.get('save-manager').add_saveable('quickmark-manager', self.save,
self.changed)
def save(self):
"""Save the quickmarks to disk."""

View File

@ -119,6 +119,7 @@ def init(args):
Args:
args: The argparse namespace.
"""
save_manager = objreg.get('save-manager')
confdir = standarddir.get(QStandardPaths.ConfigLocation, args)
try:
app = objreg.get('app')
@ -139,6 +140,9 @@ def init(args):
sys.exit(1)
else:
objreg.register('config', config_obj)
save_manager.add_saveable('config', config_obj.save,
config_obj.changed,
config_opt=('general', 'auto-save-config'))
try:
key_config = keyconf.KeyConfigParser(confdir, 'keys.conf')
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
@ -154,15 +158,23 @@ def init(args):
sys.exit(1)
else:
objreg.register('key-config', key_config)
save_manager.add_saveable('key-config', key_config.save,
key_config.changed,
config_opt=('general', 'auto-save-config'))
datadir = standarddir.get(QStandardPaths.DataLocation, args)
state_config = ini.ReadWriteConfigParser(datadir, 'state')
objreg.register('state-config', state_config)
save_manager.add_saveable('state-config', state_config.save)
# We need to import this here because lineparser needs config.
from qutebrowser.config.parsers import line
command_history = line.LineConfigParser(datadir, 'cmd-history',
('completion', 'history-length'))
('completion', 'history-length'),
parent=config_obj)
objreg.register('command-history', command_history)
save_manager.add_saveable('command-history', command_history.save,
command_history.changed)
class ConfigManager(QObject):
@ -577,14 +589,6 @@ class ConfigManager(QObject):
if self._initialized:
self._after_set(sectname, optname)
@cmdutils.register(instance='config', name='save')
def save_command(self):
"""Save the config file."""
try:
self.save()
except OSError as e:
raise cmdexc.CommandError("Could not save config: {}".format(e))
def save(self):
"""Save the config file."""
if self._configdir is None:

View File

@ -41,11 +41,6 @@ FIRST_COMMENT = r"""
# Configfile for qutebrowser.
#
# WARNING:
#
# This config file will be OVERWRITTEN when closing qutebrowser.
# Close qutebrowser before changing this file, or YOUR CHANGES WILL BE LOST.
#
# This configfile is parsed by python's configparser in extended
# interpolation mode. The format is very INI-like, so there are
# categories like [general] with "key = value"-pairs.

View File

@ -21,15 +21,14 @@
import os
import os.path
import collections
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from qutebrowser.utils import log, utils, objreg, qtutils
from qutebrowser.config import config
class LineConfigParser(collections.UserList):
class LineConfigParser(QObject):
"""Parser for configuration files which are simply line-based.
@ -41,9 +40,15 @@ class LineConfigParser(collections.UserList):
_binary: Whether to open the file in binary mode.
_limit: The config section/option used to limit the maximum number of
lines.
Signals:
changed: Emitted when the history was changed.
"""
def __init__(self, configdir, fname, limit=None, binary=False):
changed = pyqtSignal()
def __init__(self, configdir, fname, limit=None, binary=False,
parent=None):
"""Config constructor.
Args:
@ -52,7 +57,7 @@ class LineConfigParser(collections.UserList):
limit: Config tuple (section, option) which contains a limit.
binary: Whether to open the file in binary mode.
"""
super().__init__()
super().__init__(parent)
self._configdir = configdir
self._configfile = os.path.join(self._configdir, fname)
self._fname = fname
@ -71,6 +76,12 @@ class LineConfigParser(collections.UserList):
configdir=self._configdir, fname=self._fname,
limit=self._limit, binary=self._binary)
def __iter__(self):
return iter(self.data)
def __getitem__(self, key):
return self.data[key]
def read(self, filename):
"""Read the data from a file."""
if self._binary:

View File

@ -64,8 +64,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
misc.CommandLineEdit.__init__(self, parent)
misc.MinimalLineEditMixin.__init__(self)
self._win_id = win_id
command_history = objreg.get('command-history')
self.history.handle_private_mode = True
self.history.history = objreg.get('command-history').data
self.history.history = command_history.data
self.history.changed.connect(command_history.changed)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
self.cursorPositionChanged.connect(self.update_completion)
self.textChanged.connect(self.update_completion)

View File

@ -19,7 +19,7 @@
"""Command history for the status bar."""
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log
@ -39,7 +39,7 @@ class HistoryEndReachedError(Exception):
pass
class History:
class History(QObject):
"""Command history.
@ -47,14 +47,20 @@ class History:
handle_private_mode: Whether to ignore history in private mode.
history: A list of executed commands, with newer commands at the end.
_tmphist: Temporary history for history browsing (as NeighborList)
Signals:
changed: Emitted when an entry was added to the history.
"""
def __init__(self, history=None):
changed = pyqtSignal()
def __init__(self, history=None, parent=None):
"""Constructor.
Args:
history: The initial history to set.
"""
super().__init__(parent)
self.handle_private_mode = False
self._tmphist = None
if history is None:
@ -128,3 +134,4 @@ class History:
return
if not self.history or text != self.history[-1]:
self.history.append(text)
self.changed.emit()

View File

@ -69,7 +69,7 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
QCompleter.CaseSensitivelySortedModel)
self.setCompleter(qcompleter)
self._history = cmdhistory.History()
self._history = cmdhistory.History(parent=self)
self.returnPressed.connect(self.on_return_pressed)
@pyqtSlot(str)

View File

@ -58,7 +58,7 @@ class CommandLineEdit(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.history = cmdhistory.History()
self.history = cmdhistory.History(parent=self)
self._validator = _CommandValidator(self)
self.setValidator(self._validator)
self.textEdited.connect(self.on_text_edited)

View File

@ -0,0 +1,151 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser 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.
#
# qutebrowser 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 qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Saving things to disk periodically."""
from PyQt5.QtCore import pyqtSlot, QObject
from qutebrowser.config import config
from qutebrowser.commands import cmdutils
from qutebrowser.utils import utils, log, message
class Saveable:
"""A single thing which can be saved.
Attributes:
_name: The naem of the thing to be saved.
_dirty: Whether the saveable was changed since the last save.
_save_handler: The function to call to save this Saveable.
_save_on_exit: Whether to always save this saveable on exit.
_config_opt: A (section, option) tuple of a config option which decides
whether to autosave or not. None if no such option exists.
"""
def __init__(self, name, save_handler, changed=None, config_opt=None):
self._name = name
self._dirty = False
self._save_handler = save_handler
self._config_opt = config_opt
if changed is not None:
changed.connect(self.mark_dirty)
self._save_on_exit = False
else:
self._save_on_exit = True
def __repr__(self):
return utils.get_repr(self, name=self._name, dirty=self._dirty,
save_handler=self._save_handler,
config_opt=self._config_opt,
save_on_exit=self._save_on_exit)
@pyqtSlot()
def mark_dirty(self):
"""Mark this saveable as dirty (having changes)."""
log.save.debug("Marking {} as dirty.".format(self._name))
self._dirty = True
def save(self, is_exit=False, explicit=False):
"""Save this saveable.
Args:
is_exit: Whether we're currently exiting qutebrowser.
explicit: Whether the user explicitely requested this save.
"""
if (self._config_opt is not None and
(not config.get(*self._config_opt)) and
(not explicit)):
log.save.debug("Not saving {} because autosaving has been "
"disabled by {cfg[0]} -> {cfg[1]}.".format(
self._name, cfg=self._config_opt))
return
do_save = self._dirty or (self._save_on_exit and is_exit)
log.save.debug("Save of {} requested - dirty {}, save_on_exit {}, "
"is_exit {} -> {}".format(
self._name, self._dirty, self._save_on_exit,
is_exit, do_save))
if do_save:
self._save_handler()
self._dirty = False
class SaveManager(QObject):
"""Responsible to save 'saveables' periodically and on exit.
Attributes:
saveables: A dict mapping names to Saveable instances.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.saveables = {}
def __repr__(self):
return utils.get_repr(self, saveables=self.saveables)
def add_saveable(self, name, save, changed=None, config_opt=None):
"""Add a new saveable.
Args:
name: The name to use.
save: The function to call to save this saveable.
changed: The signal emitted when this saveable changed.
config_opt: A (section, option) tuple deciding whether to autosave
or not.
"""
if name in self.saveables:
raise ValueError("Saveable {} already registered!".format(name))
self.saveables[name] = Saveable(name, save, changed, config_opt)
def save(self, name, is_exit=False, explicit=False):
"""Save a saveable by name.
Args:
is_exit: Whether we're currently exiting qutebrowser.
explicit: Whether this save operation was triggered explicitely.
"""
self.saveables[name].save(is_exit=is_exit, explicit=explicit)
@cmdutils.register(instance='save-manager', name='save')
def save_command(self, win_id: {'special': 'win_id'},
*what: {'nargs': '*'}):
"""Save configs and state.
Args:
win_id: The window this command is executed in.
*what: What to save (`config`/`key-config`/`cookies`/...).
If not given, everything is saved.
"""
if what:
explicit = True
else:
what = self.saveables
explicit = False
for key in what:
if key not in self.saveables:
message.error(win_id, "{} is nothing which can be "
"saved".format(key))
else:
try:
self.save(key, explicit=explicit)
except OSError as e:
message.error(win_id, "Could not save {}: "
"{}".format(key, e))

View File

@ -126,6 +126,7 @@ style = logging.getLogger('style')
rfc6266 = logging.getLogger('rfc6266')
ipc = logging.getLogger('ipc')
shlexer = logging.getLogger('shlexer')
save = logging.getLogger('save')
ram_handler = None