Repository: qutebrowser/qutebrowser

Issue:
QtOSError when key config is unreadable
When doing `qutebrowser --basedir base`, then `chmod 000 base/config/keys.conf` and starting it again we get:

```
==== Debug log ====
[...]
11:04:39 ERROR    keyboard   keyconf:_read:309 Failed to read key bindings!
Traceback (most recent call last):
  File "/home/florian/proj/qutebrowser/git/qutebrowser/config/parsers/keyconf.py", line 286, in _read
    with open(self._configfile, 'r', encoding='utf-8') as f:
PermissionError: [Errno 13] Permission denied: 'base/config/keys.conf'
[...]
11:04:39 DEBUG    init       app:init:161 Init done!
11:04:39 DEBUG    save       savemanager:save:98 Save of key-config requested - dirty True, save_on_exit False, is_exit False, force False -> True
11:04:39 DEBUG    destroy    keyconf:save:148 Saving key config to base/config/keys.conf
11:04:39 ERROR    misc       crashsignal:exception_hook:211 Uncaught exception
Traceback (most recent call last):
  File "/home/florian/proj/qutebrowser/git/qutebrowser/misc/savemanager.py", line 100, in save
    self._save_handler()
  File "/home/florian/proj/qutebrowser/git/qutebrowser/config/parsers/keyconf.py", line 149, in save
    with qtutils.savefile_open(self._configfile, encoding='utf-8') as f:
  File "/usr/lib/python3.4/contextlib.py", line 59, in __enter__
    return next(self.gen)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/utils/qtutils.py", line 204, in savefile_open
    raise QtOSError(f)
qutebrowser.utils.qtutils.QtOSError: Existing file base/config/keys.conf is not writable
```

When `qutebrowser.conf` is unreadable we get an error message as expected.



Discussion:
A similar one:

```
10:28:10 DEBUG    init       app:init:161 Init done!
10:28:10 DEBUG    save       savemanager:save:98 Save of key-config requested - dirty True, save_on_exit False, is_exit False, force False -> True
10:28:10 DEBUG    destroy    keyconf:save:148 Saving key config to /home/moepi/.config/qutebrowser/keys.conf
10:28:10 ERROR    misc       crashsignal:exception_hook:211 Uncaught exception
Traceback (most recent call last):
  File "/home/moepi/Versioning/qutebrowser/qutebrowser/misc/savemanager.py", line 100, in save
    self._save_handler()
  File "/home/moepi/Versioning/qutebrowser/qutebrowser/config/parsers/keyconf.py", line 151, in save
    f.write(data)
  File "/usr/lib/python3.5/contextlib.py", line 66, in __exit__
    next(self.gen)
  File "/home/moepi/Versioning/qutebrowser/qutebrowser/utils/qtutils.py", line 219, in savefile_open
    raise QtOSError(f, msg="Commit failed!")
qutebrowser.utils.qtutils.QtOSError: Commit failed!
```


Resolving patch:
diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py
--- a/qutebrowser/config/parsers/keyconf.py
+++ b/qutebrowser/config/parsers/keyconf.py
@@ -142,9 +142,14 @@ def _str_section_desc(self, sectname):
     def save(self):
         """Save the key config file."""
         log.destroy.debug("Saving key config to {}".format(self._configfile))
-        with qtutils.savefile_open(self._configfile, encoding='utf-8') as f:
-            data = str(self)
-            f.write(data)
+
+        try:
+            with qtutils.savefile_open(self._configfile,
+                                       encoding='utf-8') as f:
+                data = str(self)
+                f.write(data)
+        except OSError as e:
+            message.error("Could not save key config: {}".format(e))
 
     @cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
                        no_replace_variables=True)
@@ -252,6 +257,7 @@ def _load_default(self, *, only_new=False):
         """
         # {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
         bindings_to_add = collections.OrderedDict()
+        mark_dirty = False
 
         for sectname, sect in configdata.KEY_DATA.items():
             sectname = self._normalize_sectname(sectname)
@@ -261,6 +267,7 @@ def _load_default(self, *, only_new=False):
                     if not only_new or self._is_new(sectname, command, e):
                         assert e not in bindings_to_add[sectname]
                         bindings_to_add[sectname][e] = command
+                        mark_dirty = True
 
         for sectname, sect in bindings_to_add.items():
             if not sect:
@@ -271,7 +278,7 @@ def _load_default(self, *, only_new=False):
                     self._add_binding(sectname, keychain, command)
             self.changed.emit(sectname)
 
-        if bindings_to_add:
+        if mark_dirty:
             self._mark_config_dirty()
 
     def _is_new(self, sectname, command, keychain):


Test patch: