From 43c5dc3bf667931c522be6d07c2465099f2672e3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 31 Jan 2015 22:56:23 +0100 Subject: [PATCH] Refactor saving logic, only save stuff if modified. Fixes #113. See #11. --- doc/help/commands.asciidoc | 10 +- qutebrowser/app.py | 63 ++------ qutebrowser/browser/cookies.py | 22 ++- qutebrowser/browser/quickmarks.py | 7 +- qutebrowser/config/config.py | 22 +-- qutebrowser/config/configdata.py | 5 - qutebrowser/config/parsers/line.py | 21 ++- qutebrowser/mainwindow/statusbar/command.py | 4 +- qutebrowser/misc/cmdhistory.py | 13 +- qutebrowser/misc/consolewidget.py | 2 +- qutebrowser/misc/miscwidgets.py | 2 +- qutebrowser/misc/savemanager.py | 151 ++++++++++++++++++++ qutebrowser/utils/log.py | 1 + 13 files changed, 238 insertions(+), 85 deletions(-) create mode 100644 qutebrowser/misc/savemanager.py diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 32ae8e9a5..652bc1725 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -32,7 +32,7 @@ |<>|Repeat a given command. |<>|Report a bug in qutebrowser. |<>|Restart qutebrowser while keeping existing tabs open. -|<>|Save the config file. +|<>|Save configs and state. |<>|Search for a text on the current page. |<>|Set an option. |<>|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 diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 5eba097cd..cfd3d196f 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -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() diff --git a/qutebrowser/browser/cookies.py b/qutebrowser/browser/cookies.py index 4426d4dbb..679617c4f 100644 --- a/qutebrowser/browser/cookies.py +++ b/qutebrowser/browser/cookies.py @@ -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() diff --git a/qutebrowser/browser/quickmarks.py b/qutebrowser/browser/quickmarks.py index 0e6766fcc..c08e4ca6a 100644 --- a/qutebrowser/browser/quickmarks.py +++ b/qutebrowser/browser/quickmarks.py @@ -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.""" diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 8c70112c9..e55ca950f 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -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: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index a5336a397..a9c51d14d 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -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. diff --git a/qutebrowser/config/parsers/line.py b/qutebrowser/config/parsers/line.py index 19ccc368a..db5872028 100644 --- a/qutebrowser/config/parsers/line.py +++ b/qutebrowser/config/parsers/line.py @@ -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: diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index 59bd66691..4b118714c 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -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) diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py index e2b5daf92..2c8ada291 100644 --- a/qutebrowser/misc/cmdhistory.py +++ b/qutebrowser/misc/cmdhistory.py @@ -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() diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index c4095c9f4..1edff16f6 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -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) diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index b36fb0959..c91589b60 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -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) diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py new file mode 100644 index 000000000..2a0d4ecac --- /dev/null +++ b/qutebrowser/misc/savemanager.py @@ -0,0 +1,151 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""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)) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 0d50575fb..91ec7c821 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -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