qutebrowser/qutebrowser/app.py

926 lines
36 KiB
Python
Raw Normal View History

2014-06-19 09:04:37 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2015-01-03 15:51:31 +01:00
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-02-06 14:01:23 +01:00
#
# 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/>.
2014-02-17 12:23:52 +01:00
"""Initialization of qutebrowser and application-wide things."""
2014-05-09 11:06:05 +02:00
import os
2014-05-13 18:01:10 +02:00
import sys
2014-06-24 06:43:52 +02:00
import subprocess
2014-05-09 11:06:05 +02:00
import configparser
2014-07-30 17:50:12 +02:00
import signal
2015-03-29 19:45:00 +02:00
import pdb
2014-08-26 19:10:14 +02:00
import bdb
import base64
import functools
import traceback
2014-10-19 18:19:36 +02:00
import faulthandler
import datetime
2014-05-09 11:06:05 +02:00
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
2014-12-28 14:35:28 +01:00
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon
2014-10-06 22:14:57 +02:00
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
QObject, Qt, QSocketNotifier)
2015-03-31 19:02:42 +02:00
try:
import hunter
except ImportError:
hunter = None
2014-02-17 08:56:33 +01:00
import qutebrowser
2014-12-28 15:05:49 +01:00
import qutebrowser.resources # pylint: disable=unused-import
from qutebrowser.completion.models import instances as completionmodels
2014-09-29 19:56:13 +02:00
from qutebrowser.commands import cmdutils, runners
from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import quickmarks, cookies, cache, adblock, history
from qutebrowser.browser.network import qutescheme, proxy, networkmanager
from qutebrowser.mainwindow import mainwindow
from qutebrowser.misc import (crashdialog, readline, ipc, earlyinit,
savemanager, sessions)
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
2014-09-28 00:18:57 +02:00
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
2015-03-31 19:02:42 +02:00
objreg, usertypes, standarddir)
2014-09-29 08:52:00 +02:00
# We import utilcmds to run the cmdutils.register decorators.
2013-12-14 22:15:16 +01:00
2014-01-28 23:04:02 +01:00
2014-06-04 13:38:53 +02:00
class Application(QApplication):
2014-02-07 20:21:50 +01:00
2014-06-04 13:38:53 +02:00
"""Main application instance.
2014-02-07 20:21:50 +01:00
2014-02-18 16:38:13 +01:00
Attributes:
2014-09-23 22:13:10 +02:00
_args: ArgumentParser instance.
2014-02-18 16:38:13 +01:00
_shutting_down: True if we're currently shutting down.
_quit_status: The current quitting status.
_crashdlg: The crash dialog currently open.
_crashlogfile: A file handler to the fatal crash logfile.
2014-09-28 22:13:14 +02:00
_event_filter: The EventFilter for the application.
_signal_notifier: A QSocketNotifier used for signals on Unix.
_signal_timer: A QTimer used to poll for signals on Windows.
geometry: The geometry of the last closed main window.
2014-02-18 16:38:13 +01:00
"""
def __init__(self, args):
"""Constructor.
Args:
Argument namespace from argparse.
"""
# pylint: disable=too-many-statements
2014-05-06 10:53:38 +02:00
self._quit_status = {
'crash': True,
'tabs': False,
'main': False,
2014-05-06 10:53:38 +02:00
}
self.geometry = None
2014-02-18 16:38:13 +01:00
self._shutting_down = False
self._crashdlg = None
self._crashlogfile = None
2014-09-28 00:18:57 +02:00
qt_args = qtutils.get_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args)
2014-02-06 10:25:22 +01:00
sys.excepthook = self._exception_hook
2014-01-28 14:44:12 +01:00
2014-09-23 22:13:10 +02:00
self._args = args
2014-09-24 07:10:17 +02:00
objreg.register('args', args)
2014-09-28 00:18:57 +02:00
objreg.register('app', self)
if self._args.version:
print(version.version())
print()
print()
print(qutebrowser.__copyright__)
print()
print(version.GPL_BOILERPLATE.strip())
sys.exit(0)
try:
2015-01-16 11:34:15 +01:00
sent = ipc.send_to_running_instance(self._args.command)
if sent:
sys.exit(0)
log.init.debug("Starting IPC server...")
ipc.init()
except ipc.IPCError as e:
text = ('{}\n\nMaybe another instance is running but '
'frozen?'.format(e))
msgbox = QMessageBox(QMessageBox.Critical, "Error while "
"connecting to running instance!", text)
msgbox.exec_()
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
2014-06-23 06:37:47 +02:00
log.init.debug("Starting init...")
2014-09-28 00:18:57 +02:00
self.setQuitOnLastWindowClosed(False)
self.setOrganizationName("qutebrowser")
self.setApplicationName("qutebrowser")
self.setApplicationVersion(qutebrowser.__version__)
2014-12-28 14:35:28 +01:00
self._init_icon()
2014-08-26 19:10:14 +02:00
utils.actute_warning()
try:
self._init_modules()
except (OSError, UnicodeDecodeError) as e:
msgbox = QMessageBox(
QMessageBox.Critical, "Error while initializing!",
"Error while initializing: {}".format(e))
msgbox.exec_()
sys.exit(1)
QTimer.singleShot(0, self._process_args)
2014-09-28 00:18:57 +02:00
log.init.debug("Initializing eventfilter...")
2014-09-28 22:13:14 +02:00
self._event_filter = modeman.EventFilter(self)
self.installEventFilter(self._event_filter)
2014-09-28 00:18:57 +02:00
log.init.debug("Connecting signals...")
self._connect_signals()
log.init.debug("Setting up signal handlers...")
self._setup_signals()
2014-09-28 00:18:57 +02:00
QDesktopServices.setUrlHandler('http', self.open_desktopservices_url)
QDesktopServices.setUrlHandler('https', self.open_desktopservices_url)
QDesktopServices.setUrlHandler('qute', self.open_desktopservices_url)
2014-09-28 00:18:57 +02:00
log.init.debug("Init done!")
if self._crashdlg is not None:
self._crashdlg.raise_()
state_config = objreg.get('state-config')
try:
fooled = state_config['general']['fooled']
except KeyError:
fooled = False
if datetime.date.today() == datetime.date(2015, 4, 1) and not fooled:
message.info('current', "Happy April's fools! Use :fooled to turn "
"this off.")
2014-09-28 00:18:57 +02:00
def __repr__(self):
return utils.get_repr(self)
def _init_modules(self):
"""Initialize all 'modules' which need to be initialized."""
# pylint: disable=too-many-statements
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(self)
objreg.register('save-manager', save_manager)
save_manager.add_saveable('window-geometry', self._save_geometry)
save_manager.add_saveable('version', self._save_version)
log.init.debug("Initializing network...")
networkmanager.init()
2014-09-28 00:18:57 +02:00
log.init.debug("Initializing readline-bridge...")
readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge)
log.init.debug("Initializing directories...")
2015-02-22 19:13:51 +01:00
standarddir.init(self._args)
2014-06-23 06:37:47 +02:00
log.init.debug("Initializing config...")
2015-02-22 19:13:51 +01:00
config.init()
save_manager.init_autosave()
log.init.debug("Initializing web history...")
history.init()
2014-06-23 06:37:47 +02:00
log.init.debug("Initializing crashlog...")
self._handle_segfault()
log.init.debug("Initializing sessions...")
session_manager = sessions.SessionManager(self)
objreg.register('session-manager', session_manager)
log.init.debug("Initializing js-bridge...")
js_bridge = qutescheme.JSBridge(self)
objreg.register('js-bridge', js_bridge)
2014-06-23 06:37:47 +02:00
log.init.debug("Initializing websettings...")
2014-05-08 22:33:24 +02:00
websettings.init()
Basic host blocker implementation. Squashed commit of the following: commit 8d38e9c2f13778fe21cc2c1d61c5d26907b469ef Author: Florian Bruhin <git@the-compiler.org> Date: Mon Nov 24 21:04:30 2014 +0100 Fix default adblock lists. commit a734b87e753daec28fa0c6643af8de088041179a Author: Florian Bruhin <git@the-compiler.org> Date: Mon Nov 24 21:04:14 2014 +0100 Fix error output on adblock errors. commit 15bc10f605ebb86c12523bb81b55d1440fcacdbf Author: Florian Bruhin <git@the-compiler.org> Date: Mon Nov 24 07:38:47 2014 +0100 Update adblock docks commit 475d530ee50f52295c2171208c6a0278750cf4b4 Author: Florian Bruhin <git@the-compiler.org> Date: Mon Nov 24 07:38:39 2014 +0100 Remove adblock ram calculation commit 0e52cc3ec93912fd3fa3ecf6de833aef0c540a60 Author: Florian Bruhin <git@the-compiler.org> Date: Mon Nov 24 07:03:11 2014 +0100 Adjust docs for hosts-file. commit 941df242d38300d7e42a26f2e67cfd668c6f8590 Author: Florian Bruhin <git@the-compiler.org> Date: Mon Nov 24 06:55:40 2014 +0100 Add logging for host blocking. commit 4ca1561ccf32affb8439113f4f1c222d7dee1e14 Author: Florian Bruhin <git@the-compiler.org> Date: Mon Nov 24 00:11:59 2014 +0100 Don't do adblock updates automatically after changing config. commit 26d4e86ca393d4a1a5fd8fac7729ce9c2e7cd240 Author: Florian Bruhin <git@the-compiler.org> Date: Thu Nov 20 19:58:32 2014 +0100 adblock: Don't delete blocked hosts between files. commit c7af08c68adf3c135d5a2fc746eb6d3c715b0711 Author: Florian Bruhin <git@the-compiler.org> Date: Thu Nov 20 19:58:22 2014 +0100 adblock: Add comments for rewinding. commit 9af87313088cd79ab8c022bcee9308511d6328f0 Author: Florian Bruhin <git@the-compiler.org> Date: Thu Nov 20 19:56:39 2014 +0100 adblock: Rewind files properly. commit 7f3dadc04ba68c92d6789ecf5822f7b2dfc367a0 Author: Florian Bruhin <git@the-compiler.org> Date: Thu Nov 20 19:40:18 2014 +0100 Add untested file support. commit 334c01867d510b599d94c9397097a1d789c0c64c Author: Florian Bruhin <git@the-compiler.org> Date: Wed Nov 19 22:33:25 2014 +0100 adblock: Handle download errors correctly commit 3a9af4cd4a903386dc323640f1d52e03383b3aa7 Author: Florian Bruhin <git@the-compiler.org> Date: Wed Nov 19 22:23:04 2014 +0100 adblock: Add error handling for invalid input. commit 5934c3c027ff9205adb512aab396ca913b31e50d Author: Florian Bruhin <git@the-compiler.org> Date: Wed Nov 19 22:22:47 2014 +0100 adblock: Handle done_count correctly. commit 3ba5e83fda9b7d764bfcf1e4b1aa479daae39791 Author: Florian Bruhin <git@the-compiler.org> Date: Wed Nov 19 21:18:10 2014 +0100 Read adblock files when they finished downloading. commit 86a5ecf194445e2ebe0cf22b59e24223387880c5 Author: Florian Bruhin <git@the-compiler.org> Date: Wed Nov 19 20:51:25 2014 +0100 Add hosts-file.net to host-block-lists. commit 121f0bff953497449c32e45e671b71d954beed4c Author: Florian Bruhin <git@the-compiler.org> Date: Wed Nov 19 20:51:13 2014 +0100 Add zip-file support to adblock.py. commit f1c0e67cb9d890a44a1d1f545ace997f0a453f91 Author: Florian Bruhin <git@the-compiler.org> Date: Mon Nov 17 07:41:14 2014 +0100 print line counts for debugging commit 75dbb8964fd44862abc378ef26fb47f8cda6061e Author: Florian Bruhin <git@the-compiler.org> Date: Fri Nov 14 19:22:20 2014 +0100 Use statusbar messages for adblock commit 74d9142d831496e02c5baae72c2723e320af1778 Author: Florian Bruhin <git@the-compiler.org> Date: Fri Nov 14 08:34:46 2014 +0100 Add basic adblock functionality.
2014-11-24 21:05:09 +01:00
log.init.debug("Initializing adblock...")
host_blocker = adblock.HostBlocker()
host_blocker.read_hosts()
objreg.register('host-blocker', host_blocker)
2014-06-23 06:37:47 +02:00
log.init.debug("Initializing quickmarks...")
quickmark_manager = quickmarks.QuickmarkManager()
objreg.register('quickmark-manager', quickmark_manager)
2014-06-23 06:37:47 +02:00
log.init.debug("Initializing proxy...")
2014-05-05 22:07:41 +02:00
proxy.init()
2014-06-23 06:37:47 +02:00
log.init.debug("Initializing cookies...")
2014-09-23 23:05:55 +02:00
cookie_jar = cookies.CookieJar(self)
2014-09-24 07:10:17 +02:00
objreg.register('cookie-jar', cookie_jar)
2014-09-01 19:42:21 +02:00
log.init.debug("Initializing cache...")
diskcache = cache.DiskCache(self)
2014-09-24 07:10:17 +02:00
objreg.register('cache', diskcache)
log.init.debug("Initializing completions...")
completionmodels.init()
2014-09-23 23:31:17 +02:00
2014-12-28 14:35:28 +01:00
def _init_icon(self):
"""Initialize the icon of qutebrowser."""
icon = QIcon()
for size in (16, 24, 32, 48, 64, 96, 128, 256, 512):
2014-12-28 15:05:49 +01:00
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
pixmap = QPixmap(filename)
2014-12-28 14:35:28 +01:00
qtutils.ensure_not_null(pixmap)
icon.addPixmap(pixmap)
qtutils.ensure_not_null(icon)
self.setWindowIcon(icon)
def _handle_segfault(self):
"""Handle a segfault from a previous run."""
logname = os.path.join(standarddir.data(), 'crash.log')
try:
# First check if an old logfile exists.
if os.path.exists(logname):
with open(logname, 'r', encoding='ascii') as f:
data = f.read()
os.remove(logname)
self._init_crashlogfile()
if data:
# Crashlog exists and has data in it, so something crashed
# previously.
self._crashdlg = crashdialog.get_fatal_crash_dialog(
self._args.debug, data)
self._crashdlg.show()
else:
# There's no log file, so we can use this to display crashes to
# the user on the next start.
self._init_crashlogfile()
except OSError:
log.init.exception("Error while handling crash log file!")
self._init_crashlogfile()
def _init_crashlogfile(self):
"""Start a new logfile and redirect faulthandler to it."""
logname = os.path.join(standarddir.data(), 'crash.log')
try:
self._crashlogfile = open(logname, 'w', encoding='ascii')
except OSError:
log.init.exception("Error while opening crash log file!")
else:
earlyinit.init_faulthandler(self._crashlogfile)
def _process_args(self):
"""Open startpage etc. and process commandline args."""
config_obj = objreg.get('config')
for sect, opt, val in self._args.temp_settings:
try:
config_obj.set('temp', sect, opt, val)
except (configexc.Error, configparser.Error) as e:
message.error('current', "set: {} - {}".format(
e.__class__.__name__, e))
if not self._args.override_restore:
self._load_session(self._args.session)
session_manager = objreg.get('session-manager')
if not session_manager.did_load:
log.init.debug("Initializing main window...")
window = mainwindow.MainWindow()
if not self._args.nowindow:
window.show()
self.setActiveWindow(window)
self.process_pos_args(self._args.command)
self._open_startpage()
self._open_quickstart()
def _load_session(self, name):
"""Load the default session.
Args:
name: The name of the session to load, or None to read state file.
"""
state_config = objreg.get('state-config')
if name is None:
try:
name = state_config['general']['session']
except KeyError:
# No session given as argument and none in the session file ->
# start without loading a session
return
session_manager = objreg.get('session-manager')
try:
session_manager.load(name)
except sessions.SessionNotFoundError:
message.error('current', "Session {} not found!".format(name))
except sessions.SessionError as e:
message.error('current', "Failed to load session {}: {}".format(
name, e))
try:
del state_config['general']['session']
except KeyError:
pass
def _get_window(self, via_ipc, force_window=False, force_tab=False):
"""Helper function for process_pos_args to get a window id.
2014-10-14 10:10:24 +02:00
Args:
via_ipc: Whether the request was made via IPC.
force_window: Whether to force opening in a window.
force_tab: Whether to force opening in a tab.
2014-10-14 10:10:24 +02:00
"""
if force_window and force_tab:
raise ValueError("force_window and force_tab are mutually "
"exclusive!")
if not via_ipc:
2014-10-14 10:10:24 +02:00
# Initial main window
2014-10-14 07:37:12 +02:00
return 0
2014-10-14 10:10:24 +02:00
window_to_raise = None
open_target = config.get('general', 'new-instance-open-target')
if (open_target == 'window' or force_window) and not force_tab:
window = mainwindow.MainWindow()
window.show()
win_id = window.win_id
2014-10-14 10:10:24 +02:00
window_to_raise = window
else:
2014-10-14 07:37:12 +02:00
try:
window = objreg.last_window()
except objreg.NoWindow:
# There is no window left, so we open a new one
window = mainwindow.MainWindow()
window.show()
win_id = window.win_id
window_to_raise = window
2014-10-14 10:10:24 +02:00
win_id = window.win_id
if open_target != 'tab-silent':
window_to_raise = window
if window_to_raise is not None:
window_to_raise.setWindowState(window.windowState() &
~Qt.WindowMinimized |
Qt.WindowActive)
window_to_raise.raise_()
window_to_raise.activateWindow()
self.alert(window_to_raise)
2014-10-14 10:10:24 +02:00
return win_id
2014-10-14 07:37:12 +02:00
def process_pos_args(self, args, via_ipc=False, cwd=None):
"""Process positional commandline args.
URLs to open have no prefix, commands to execute begin with a colon.
2014-06-24 21:36:00 +02:00
Args:
args: A list of arguments to process.
via_ipc: Whether the arguments were transmitted over IPC.
2014-11-30 19:22:35 +01:00
cwd: The cwd to use for fuzzy_url.
"""
2015-03-17 19:08:45 +01:00
if via_ipc and not args:
win_id = self._get_window(via_ipc, force_window=True)
2014-10-14 10:10:24 +02:00
self._open_startpage(win_id)
return
win_id = None
2014-06-24 21:36:00 +02:00
for cmd in args:
if cmd.startswith(':'):
if win_id is None:
win_id = self._get_window(via_ipc, force_tab=True)
log.init.debug("Startup cmd {}".format(cmd))
2014-09-29 19:56:13 +02:00
commandrunner = runners.CommandRunner(win_id)
commandrunner.run_safely_init(cmd[1:])
elif not cmd:
log.init.debug("Empty argument")
win_id = self._get_window(via_ipc, force_window=True)
else:
win_id = self._get_window(via_ipc)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
log.init.debug("Startup URL {}".format(cmd))
try:
url = urlutils.fuzzy_url(cmd, cwd, relative=True)
2014-06-20 23:57:52 +02:00
except urlutils.FuzzyUrlError as e:
2014-09-28 22:13:14 +02:00
message.error(0, "Error in startup argument '{}': "
"{}".format(cmd, e))
else:
tabbed_browser.tabopen(url, background=False)
2014-10-14 10:10:24 +02:00
def _open_startpage(self, win_id=None):
"""Open startpage.
The startpage is never opened if the given windows are not empty.
Args:
win_id: If None, open startpage in all empty windows.
If set, open the startpage in the given window.
"""
if win_id is not None:
window_ids = [win_id]
else:
window_ids = objreg.window_registry
for cur_win_id in window_ids:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
2014-10-14 10:10:24 +02:00
window=cur_win_id)
if tabbed_browser.count() == 0:
log.init.debug("Opening startpage")
for urlstr in config.get('general', 'startpage'):
try:
url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.FuzzyUrlError as e:
message.error(0, "Error when opening startpage: "
2014-09-29 20:36:53 +02:00
"{}".format(e))
tabbed_browser.tabopen(QUrl('about:blank'))
else:
tabbed_browser.tabopen(url)
2014-01-29 15:30:19 +01:00
def _open_quickstart(self):
"""Open quickstart if it's the first start."""
state_config = objreg.get('state-config')
try:
quickstart_done = state_config['general']['quickstart-done'] == '1'
except KeyError:
quickstart_done = False
if not quickstart_done:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
tabbed_browser.tabopen(
QUrl('http://www.qutebrowser.org/quickstart.html'))
state_config['general']['quickstart-done'] = '1'
def _setup_signals(self):
"""Set up signal handlers.
On Windows this uses a QTimer to periodically hand control over to
Python so it can handle signals.
On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get
notified.
"""
2014-07-30 18:11:22 +02:00
signal.signal(signal.SIGINT, self.interrupt)
signal.signal(signal.SIGTERM, self.interrupt)
2015-03-16 23:32:49 +01:00
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
import fcntl
read_fd, write_fd = os.pipe()
for fd in (read_fd, write_fd):
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
self._signal_notifier = QSocketNotifier(
read_fd, QSocketNotifier.Read, self)
self._signal_notifier.activated.connect(self._handle_signal_wakeup)
signal.set_wakeup_fd(write_fd)
else:
self._signal_timer = usertypes.Timer(self, 'python_hacks')
self._signal_timer.start(1000)
self._signal_timer.timeout.connect(lambda: None)
@pyqtSlot()
def _handle_signal_wakeup(self):
2015-03-26 07:08:13 +01:00
"""Handle a newly arrived signal.
This gets called via self._signal_notifier when there's a signal.
Python will get control here, so the signal will get handled.
"""
log.destroy.debug("Handling signal wakeup!")
self._signal_notifier.setEnabled(False)
read_fd = self._signal_notifier.socket()
try:
os.read(read_fd, 1)
except OSError:
log.destroy.exception("Failed to read wakeup fd.")
self._signal_notifier.setEnabled(True)
2014-04-21 15:20:41 +02:00
def _connect_signals(self):
"""Connect all signals to their slots."""
2014-09-24 07:10:17 +02:00
config_obj = objreg.get('config')
2015-02-17 07:45:06 +01:00
self.lastWindowClosed.connect(self.on_last_window_closed)
2014-09-23 22:28:28 +02:00
config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
self.focusChanged.connect(self.on_focus_changed)
self.focusChanged.connect(message.on_focus_changed)
def _get_widgets(self):
2014-06-17 23:04:58 +02:00
"""Get a string list of all widgets."""
widgets = self.allWidgets()
2015-03-23 07:04:50 +01:00
widgets.sort(key=repr)
return [repr(w) for w in widgets]
2014-06-17 23:04:58 +02:00
2014-09-23 06:39:23 +02:00
def _get_pyqt_objects(self, lines, obj, depth=0):
"""Recursive method for get_all_objects to get Qt objects."""
2014-06-17 23:04:58 +02:00
for kid in obj.findChildren(QObject):
lines.append(' ' * depth + repr(kid))
2014-09-23 06:39:23 +02:00
self._get_pyqt_objects(lines, kid, depth + 1)
def get_all_objects(self):
"""Get all children of an object recursively as a string."""
2014-09-23 07:48:34 +02:00
output = ['']
2014-09-25 08:03:04 +02:00
widget_lines = self._get_widgets()
widget_lines = [' ' + e for e in widget_lines]
widget_lines.insert(0, "Qt widgets - {} objects".format(
len(widget_lines)))
output += widget_lines
pyqt_lines = []
self._get_pyqt_objects(pyqt_lines, self)
2014-09-23 07:48:34 +02:00
pyqt_lines = [' ' + e for e in pyqt_lines]
pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(
len(pyqt_lines)))
output += pyqt_lines
output += ['']
2014-10-05 21:50:14 +02:00
output += objreg.dump_objects()
2014-09-23 07:48:34 +02:00
return '\n'.join(output)
2014-06-17 23:04:58 +02:00
2014-09-29 20:36:53 +02:00
def _recover_pages(self, forgiving=False):
2014-02-17 20:30:09 +01:00
"""Try to recover all open pages.
Called from _exception_hook, so as forgiving as possible.
2014-09-29 20:36:53 +02:00
Args:
forgiving: Whether to ignore exceptions.
2014-02-19 10:58:32 +01:00
Return:
A list containing a list for each window, which in turn contain the
opened URLs.
2014-02-17 20:30:09 +01:00
"""
pages = []
for win_id in objreg.window_registry:
win_pages = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
for tab in tabbed_browser.widgets():
try:
2014-09-29 20:36:53 +02:00
urlstr = tab.cur_url.toString(
QUrl.RemovePassword | QUrl.FullyEncoded)
2014-09-29 20:36:53 +02:00
if urlstr:
win_pages.append(urlstr)
2015-03-11 20:14:39 +01:00
except Exception:
2014-09-29 20:36:53 +02:00
if forgiving:
log.destroy.exception("Error while recovering tab")
else:
raise
pages.append(win_pages)
2014-02-17 20:30:09 +01:00
return pages
def _save_geometry(self):
"""Save the window geometry to the state config."""
if self.geometry is not None:
state_config = objreg.get('state-config')
geom = base64.b64encode(self.geometry).decode('ASCII')
state_config['geometry']['mainwindow'] = geom
2014-12-22 23:47:43 +01:00
def _save_version(self):
"""Save the current version to the state config."""
state_config = objreg.get('state-config')
state_config['general']['version'] = qutebrowser.__version__
def _destroy_crashlogfile(self):
"""Clean up the crash log file and delete it."""
if self._crashlogfile is None:
return
# We use sys.__stderr__ instead of sys.stderr here so this will still
# work when sys.stderr got replaced, e.g. by "Python Tools for Visual
# Studio".
if sys.__stderr__ is not None:
faulthandler.enable(sys.__stderr__)
else:
faulthandler.disable()
try:
self._crashlogfile.close()
os.remove(self._crashlogfile.name)
except OSError:
log.destroy.exception("Could not remove crash log!")
2014-12-21 18:11:35 +01:00
def _exception_hook(self, exctype, excvalue, tb): # noqa
2014-01-29 15:30:19 +01:00
"""Handle uncaught python exceptions.
It'll try very hard to write all open tabs to a file, and then exit
gracefully.
"""
2014-06-02 23:29:01 +02:00
exc = (exctype, excvalue, tb)
2014-05-07 17:29:28 +02:00
if not self._quit_status['crash']:
log.misc.error("ARGH, there was an exception while the crash "
"dialog is already shown:", exc_info=exc)
return
log.misc.error("Uncaught exception", exc_info=exc)
2014-12-06 00:39:33 +01:00
is_ignored_exception = (exctype is bdb.BdbQuit or
not issubclass(exctype, Exception))
2015-03-29 19:45:00 +02:00
if self._args.pdb_postmortem:
pdb.post_mortem(tb)
if (is_ignored_exception or self._args.no_crash_dialog or
self._args.pdb_postmortem):
2014-12-06 00:39:33 +01:00
# pdb exit, KeyboardInterrupt, ...
status = 0 if is_ignored_exception else 2
try:
self.shutdown(status)
return
except Exception:
log.init.exception("Error while shutting down")
self.quit()
return
2014-05-07 17:29:28 +02:00
self._quit_status['crash'] = False
2014-01-28 14:44:12 +01:00
try:
2014-09-29 20:36:53 +02:00
pages = self._recover_pages(forgiving=True)
except Exception:
log.destroy.exception("Error while recovering pages")
2014-02-17 20:30:09 +01:00
pages = []
2014-01-30 20:42:47 +01:00
try:
cmd_history = objreg.get('command-history')[-5:]
except Exception:
log.destroy.exception("Error while getting history: {}")
cmd_history = []
2014-01-30 20:42:47 +01:00
2014-06-17 23:04:58 +02:00
try:
objects = self.get_all_objects()
except Exception:
log.destroy.exception("Error while getting objects")
2014-06-17 23:04:58 +02:00
objects = ""
try:
objreg.get('ipc-server').ignored = True
except Exception:
log.destroy.exception("Error while ignoring ipc")
try:
2015-02-17 07:45:06 +01:00
self.lastWindowClosed.disconnect(self.on_last_window_closed)
except TypeError:
log.destroy.exception("Error while preventing shutdown")
2014-02-05 11:40:30 +01:00
QApplication.closeAllWindows()
self._crashdlg = crashdialog.ExceptionCrashDialog(
self._args.debug, pages, cmd_history, exc, objects)
ret = self._crashdlg.exec_()
2014-01-30 20:42:47 +01:00
if ret == QDialog.Accepted: # restore
2014-05-14 08:56:42 +02:00
self.restart(shutdown=False, pages=pages)
2014-05-07 17:29:28 +02:00
# We might risk a segfault here, but that's better than continuing to
# run in some undefined state, so we only do the most needed shutdown
# here.
qInstallMessageHandler(None)
self._destroy_crashlogfile()
2014-05-07 17:29:28 +02:00
sys.exit(1)
2014-02-17 20:39:15 +01:00
def _get_restart_args(self, pages):
"""Get the current working directory and args to relaunch qutebrowser.
Args:
pages: The pages to re-open.
Return:
An (args, cwd) tuple.
args: The commandline as a list of strings.
cwd: The current working directory as a string.
"""
if os.path.basename(sys.argv[0]) == 'qutebrowser':
# Launched via launcher script
args = [sys.argv[0]]
cwd = None
elif hasattr(sys, 'frozen'):
2014-06-24 06:52:49 +02:00
args = [sys.executable]
cwd = os.path.abspath(os.path.dirname(sys.executable))
else:
args = [sys.executable, '-m', 'qutebrowser']
cwd = os.path.join(os.path.abspath(os.path.dirname(
qutebrowser.__file__)), '..')
if not os.path.isdir(cwd):
# Probably running from an python egg. Let's fallback to
# cwd=None and see if that works out.
# See https://github.com/The-Compiler/qutebrowser/issues/323
cwd = None
2014-06-24 06:43:52 +02:00
for arg in sys.argv[1:]:
if arg.startswith('-'):
# We only want to preserve options on a restart.
args.append(arg)
# Add all open pages so they get reopened.
page_args = []
for win in pages:
page_args.extend(win)
page_args.append('')
if page_args:
args.extend(page_args[:-1])
2014-06-24 06:52:49 +02:00
log.destroy.debug("args: {}".format(args))
log.destroy.debug("cwd: {}".format(cwd))
return args, cwd
@cmdutils.register(instance='app', ignore_args=True)
def restart(self, shutdown=True, pages=None):
"""Restart qutebrowser while keeping existing tabs open."""
if pages is None:
pages = self._recover_pages()
log.destroy.debug("sys.executable: {}".format(sys.executable))
log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv))
log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen')))
2014-06-24 06:43:52 +02:00
# Open a new process and immediately shutdown the existing one
2014-11-23 17:56:14 +01:00
try:
args, cwd = self._get_restart_args(pages)
2014-11-23 17:56:14 +01:00
if cwd is None:
subprocess.Popen(args)
else:
subprocess.Popen(args, cwd=cwd)
2014-12-26 15:19:52 +01:00
except OSError:
log.destroy.exception("Failed to restart")
else:
2014-11-23 17:56:14 +01:00
if shutdown:
self.shutdown()
2014-05-14 08:56:42 +02:00
@cmdutils.register(instance='app', maxsplit=0, debug=True)
2014-06-17 11:12:55 +02:00
def debug_pyeval(self, s):
2015-03-31 20:49:29 +02:00
"""Evaluate a python string and display the results as a web page.
2014-01-29 15:30:19 +01:00
//
2014-06-17 11:12:55 +02:00
We have this here rather in utils.debug so the context of eval makes
more sense and because we don't want to import much stuff in the utils.
2014-02-07 20:21:50 +01:00
2014-02-19 10:58:32 +01:00
Args:
s: The string to evaluate.
2014-01-29 15:30:19 +01:00
"""
2014-01-19 23:54:22 +01:00
try:
2015-03-11 20:14:39 +01:00
r = eval(s)
2014-01-19 23:54:22 +01:00
out = repr(r)
2015-03-11 20:14:39 +01:00
except Exception:
out = traceback.format_exc()
2014-02-21 07:18:04 +01:00
qutescheme.pyeval_output = out
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
2014-01-30 14:58:32 +01:00
2014-09-23 22:06:46 +02:00
@cmdutils.register(instance='app')
2014-06-25 22:22:30 +02:00
def report(self):
"""Report a bug in qutebrowser."""
pages = self._recover_pages()
cmd_history = objreg.get('command-history')[-5:]
2014-06-25 22:22:30 +02:00
objects = self.get_all_objects()
self._crashdlg = crashdialog.ReportDialog(pages, cmd_history, objects)
2014-06-25 22:22:30 +02:00
self._crashdlg.show()
2014-07-30 18:11:22 +02:00
def interrupt(self, signum, _frame):
2014-08-02 01:53:27 +02:00
"""Handler for signals to gracefully shutdown (SIGINT/SIGTERM).
This calls self.shutdown and remaps the signal to call
self.interrupt_forcefully the next time.
"""
2014-07-30 18:11:22 +02:00
log.destroy.info("SIGINT/SIGTERM received, shutting down!")
log.destroy.info("Do the same again to forcefully quit.")
2014-07-30 18:11:22 +02:00
signal.signal(signal.SIGINT, self.interrupt_forcefully)
signal.signal(signal.SIGTERM, self.interrupt_forcefully)
2014-08-02 00:53:30 +02:00
# If we call shutdown directly here, we get a segfault.
2014-08-26 19:10:14 +02:00
QTimer.singleShot(0, functools.partial(self.shutdown, 128 + signum))
2014-07-30 18:11:22 +02:00
2014-07-30 18:56:01 +02:00
def interrupt_forcefully(self, signum, _frame):
2014-08-02 01:53:27 +02:00
"""Interrupt forcefully on the second SIGINT/SIGTERM request.
This skips our shutdown routine and calls QApplication:exit instead.
It then remaps the signals to call self.interrupt_really_forcefully the
next time.
"""
2014-07-30 18:11:22 +02:00
log.destroy.info("Forceful quit requested, goodbye cruel world!")
log.destroy.info("Do the same again to quit with even more force.")
signal.signal(signal.SIGINT, self.interrupt_really_forcefully)
signal.signal(signal.SIGTERM, self.interrupt_really_forcefully)
# This *should* work without a QTimer, but because of the trouble in
# self.interrupt we're better safe than sorry.
2014-08-26 19:10:14 +02:00
QTimer.singleShot(0, functools.partial(self.exit, 128 + signum))
def interrupt_really_forcefully(self, signum, _frame):
2014-08-02 01:53:27 +02:00
"""Interrupt with even more force on the third SIGINT/SIGTERM request.
This doesn't run *any* Qt cleanup and simply exits via Python.
It will most likely lead to a segfault.
"""
log.destroy.info("WHY ARE YOU DOING THIS TO ME? :(")
sys.exit(128 + signum)
2014-07-30 18:11:22 +02:00
2015-02-17 07:45:06 +01:00
@cmdutils.register(instance='app', name='wq',
completion=[usertypes.Completion.sessions])
def save_and_quit(self, name='default'):
"""Save open pages and quit.
Args:
name: The name of the session.
"""
self.shutdown(session=name)
@pyqtSlot()
2015-02-17 07:45:06 +01:00
def on_last_window_closed(self):
"""Slot which gets invoked when the last window was closed."""
self.shutdown(last_window=True)
2015-02-17 07:45:06 +01:00
@cmdutils.register(instance='app', name=['quit', 'q'], ignore_args=True)
def shutdown(self, status=0, session=None, last_window=False):
"""Quit qutebrowser.
2014-07-30 18:11:22 +02:00
Args:
status: The status code to exit with.
2015-02-17 07:45:06 +01:00
session: A session name if saving should be forced.
last_window: If the shutdown was triggered due to the last window
closing.
"""
if self._shutting_down:
return
self._shutting_down = True
2015-02-17 07:45:06 +01:00
log.destroy.debug("Shutting down with status {}, session {}..."
.format(status, session))
2015-02-17 07:45:06 +01:00
session_manager = objreg.get('session-manager')
if session is not None:
session_manager.save(session, last_window=last_window,
load_next_time=True)
2015-02-17 07:45:06 +01:00
elif config.get('general', 'save-session'):
session_manager.save('default', last_window=last_window,
load_next_time=True)
2014-09-28 22:13:14 +02:00
deferrer = False
for win_id in objreg.window_registry:
prompter = objreg.get('prompter', None, scope='window',
window=win_id)
if prompter is not None and prompter.shutdown():
deferrer = True
if deferrer:
# If shutdown was called while we were asking a question, we're in
2015-03-31 20:49:29 +02:00
# a still sub-eventloop (which gets quit now) and not in the main
# one.
# This means we need to defer the real shutdown to when we're back
# in the real main event loop, or we'll get a segfault.
log.destroy.debug("Deferring real shutdown because question was "
"active.")
2014-08-26 19:10:14 +02:00
QTimer.singleShot(0, functools.partial(self._shutdown, status))
else:
# If we have no questions to shut down, we are already in the real
# event loop, so we can shut down immediately.
self._shutdown(status)
def _shutdown(self, status): # noqa
"""Second stage of shutdown."""
log.destroy.debug("Stage 2 of shutting down...")
2014-07-31 23:09:59 +02:00
# Remove eventfilter
2014-09-23 19:57:51 +02:00
try:
2014-07-31 23:09:59 +02:00
log.destroy.debug("Removing eventfilter...")
2014-09-28 22:13:14 +02:00
self.removeEventFilter(self._event_filter)
except AttributeError:
2014-09-23 19:57:51 +02:00
pass
# Close all windows
QApplication.closeAllWindows()
2014-10-13 20:36:23 +02:00
# Shut down IPC
try:
objreg.get('ipc-server').shutdown()
except KeyError:
pass
2014-06-03 16:48:21 +02:00
# Save everything
2014-09-23 22:28:28 +02:00
try:
save_manager = objreg.get('save-manager')
2014-09-23 22:28:28 +02:00
except KeyError:
log.destroy.debug("Save manager not initialized yet, so not "
"saving anything.")
2014-09-23 22:28:28 +02:00
else:
for key in save_manager.saveables:
try:
save_manager.save(key, is_exit=True)
except OSError as e:
msgbox = QMessageBox(
QMessageBox.Critical, "Error while saving!",
"Error while saving {}: {}".format(key, e))
msgbox.exec_()
# Re-enable faulthandler to stdout, then remove crash log
log.destroy.debug("Deactiving crash log...")
self._destroy_crashlogfile()
# If we don't kill our custom handler here we might get segfaults
2014-07-30 18:11:35 +02:00
log.destroy.debug("Deactiving message handler...")
qInstallMessageHandler(None)
# Now we can hopefully quit without segfaults
2014-08-02 19:40:24 +02:00
log.destroy.debug("Deferring QApplication::exit...")
2015-03-31 20:49:29 +02:00
# We use a singleshot timer to exit here to minimize the likelihood of
# segfaults.
2014-08-26 19:10:14 +02:00
QTimer.singleShot(0, functools.partial(self.exit, status))
2014-08-02 19:40:24 +02:00
def on_focus_changed(self, _old, new):
"""Register currently focused main window in the object registry."""
if new is None:
window = None
else:
window = new.window()
if window is None or not isinstance(window, mainwindow.MainWindow):
try:
objreg.delete('last-focused-main-window')
except KeyError:
pass
else:
objreg.register('last-focused-main-window', window, update=True)
@pyqtSlot(QUrl)
def open_desktopservices_url(self, url):
"""Handler to open an URL via QDesktopServices."""
win_id = self._get_window(via_ipc=True, force_window=False)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(url)
2014-08-02 19:40:24 +02:00
def exit(self, status):
"""Extend QApplication::exit to log the event."""
log.destroy.debug("Now calling QApplication::exit.")
2014-09-23 22:13:10 +02:00
if self._args.debug_exit:
2015-03-31 19:02:42 +02:00
if hunter is None:
print("Not logging late shutdown because hunter could not be "
"imported!", file=sys.stderr)
else:
print("Now logging late shutdown.", file=sys.stderr)
hunter.trace()
2014-08-02 19:40:24 +02:00
super().exit(status)