# Copyright 2014-2021 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 . """Initialization of qutebrowser and application-wide things. The run() function will get called once early initialization (in qutebrowser.py/earlyinit.py) is done. See the qutebrowser.py docstring for details about early initialization. As we need to access the config before the QApplication is created, we initialize everything the config needs before the QApplication is created, and then leave it in a partially initialized state (no saving, no config errors shown yet). We then set up the QApplication object and initialize a few more low-level things. After that, init() and _init_modules() take over and initialize the rest. After all initialization is done, the qt_mainloop() function is called, which blocks and spins the Qt mainloop. """ import os import sys import functools import tempfile import pathlib import datetime import argparse from typing import Iterable, Optional, List, Tuple from qutebrowser.qt import machinery from qutebrowser.qt.widgets import QApplication, QWidget from qutebrowser.qt.gui import QDesktopServices, QPixmap, QIcon from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, QEvent, pyqtSignal, Qt import qutebrowser from qutebrowser.commands import runners from qutebrowser.config import (config, websettings, configfiles, configinit, qtargs) from qutebrowser.browser import (urlmarks, history, browsertab, qtnetworkdownloads, downloads, greasemonkey) from qutebrowser.browser.network import proxy from qutebrowser.browser.webkit import cookies, cache from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.extensions import loader from qutebrowser.keyinput import macros, eventfilter from qutebrowser.mainwindow import mainwindow, prompt, windowundo from qutebrowser.misc import (ipc, savemanager, sessions, crashsignal, earlyinit, sql, cmdhistory, backendproblem, objects, quitter, nativeeventfilter) from qutebrowser.utils import (log, version, message, utils, urlutils, objreg, resources, usertypes, standarddir, error, qtutils, debug) # pylint: disable=unused-import # We import those to run the cmdutils.register decorators. from qutebrowser.mainwindow.statusbar import command from qutebrowser.misc import utilcmds from qutebrowser.browser import commands # pylint: enable=unused-import def run(args): """Initialize everything and run the application.""" if args.temp_basedir: args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-') log.init.debug("Main process PID: {}".format(os.getpid())) log.init.debug("Initializing directories...") standarddir.init(args) resources.preload() log.init.debug("Initializing config...") configinit.early_init(args) log.init.debug("Initializing application...") app = Application(args) objects.qapp = app app.setOrganizationName("qutebrowser") app.setApplicationName("qutebrowser") # Default DesktopFileName is org.qutebrowser.qutebrowser, set in `get_argparser()` app.setDesktopFileName(args.desktop_file_name) app.setApplicationVersion(qutebrowser.__version__) if args.version: print(version.version_info()) sys.exit(usertypes.Exit.ok) quitter.init(args) crashsignal.init(q_app=app, args=args, quitter=quitter.instance) try: server = ipc.send_or_listen(args) except ipc.Error: # ipc.send_or_listen already displays the error message for us. # We didn't really initialize much so far, so we just quit hard. sys.exit(usertypes.Exit.err_ipc) if server is None: if args.backend is not None: log.init.warning( "Backend from the running instance will be used") sys.exit(usertypes.Exit.ok) init(args=args) quitter.instance.shutting_down.connect(server.shutdown) server.got_args.connect( lambda args, target_arg, cwd: process_pos_args(args, cwd=cwd, via_ipc=True, target_arg=target_arg)) ret = qt_mainloop() return ret def qt_mainloop(): """Simple wrapper to get a nicer stack trace for segfaults. WARNING: misc/crashdialog.py checks the stacktrace for this function name, so if this is changed, it should be changed there as well! """ return objects.qapp.exec() def init(*, args: argparse.Namespace) -> None: """Initialize everything.""" log.init.debug("Starting init...") crashsignal.crash_handler.init_faulthandler() objects.qapp.setQuitOnLastWindowClosed(False) quitter.instance.shutting_down.connect(QApplication.closeAllWindows) _init_icon() loader.init() loader.load_components() try: _init_modules(args=args) except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e: error.handle_fatal_exc(e, "Error while initializing!", no_err_windows=args.no_err_windows, pre_text="Error while initializing") sys.exit(usertypes.Exit.err_init) log.init.debug("Initializing eventfilter...") eventfilter.init() log.init.debug("Connecting signals...") objects.qapp.focusChanged.connect(on_focus_changed) _process_args(args) for scheme in ['http', 'https', 'qute']: QDesktopServices.setUrlHandler( scheme, open_desktopservices_url) log.init.debug("Init done!") crashsignal.crash_handler.raise_crashdlg() def _init_icon(): """Initialize the icon of qutebrowser.""" fallback_icon = QIcon() for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]: filename = 'icons/qutebrowser-{size}x{size}.png'.format(size=size) pixmap = QPixmap() pixmap.loadFromData(resources.read_file_binary(filename)) if pixmap.isNull(): log.init.warning("Failed to load {}".format(filename)) else: fallback_icon.addPixmap(pixmap) icon = QIcon.fromTheme('qutebrowser', fallback_icon) if icon.isNull(): log.init.warning("Failed to load icon") else: objects.qapp.setWindowIcon(icon) def _process_args(args): """Open startpage etc. and process commandline args.""" if not args.override_restore: sessions.load_default(args.session) new_window = None if not sessions.session_manager.did_load: log.init.debug("Initializing main window...") private = args.target == 'private-window' if (config.val.content.private_browsing or private) and qtutils.is_single_process(): err = Exception("Private windows are unavailable with " "the single-process process model.") error.handle_fatal_exc(err, 'Cannot start in private mode', no_err_windows=args.no_err_windows) sys.exit(usertypes.Exit.err_init) new_window = mainwindow.MainWindow(private=private) process_pos_args(args.command) _open_startpage() _open_special_pages(args) if new_window is not None and not args.nowindow: new_window.show() objects.qapp.setActiveWindow(new_window) delta = datetime.datetime.now() - earlyinit.START_TIME log.init.debug("Init finished after {}s".format(delta.total_seconds())) def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None): """Process positional commandline args. URLs to open have no prefix, commands to execute begin with a colon. Args: args: A list of arguments to process. via_ipc: Whether the arguments were transmitted over IPC. cwd: The cwd to use for fuzzy_url. target_arg: Command line argument received by a running instance via ipc. If the --target argument was not specified, target_arg will be an empty string. """ new_window_target = ('private-window' if target_arg == 'private-window' else 'window') command_target = config.val.new_instance_open_target if command_target in {'window', 'private-window'}: command_target = 'tab-silent' window: Optional[mainwindow.MainWindow] = None if via_ipc and (not args or args == ['']): window = mainwindow.get_window(via_ipc=via_ipc, target=new_window_target) _open_startpage(window) window.show() window.maybe_raise() return for cmd in args: if cmd.startswith(':'): if window is None: window = mainwindow.get_window(via_ipc=via_ipc, target=command_target) # FIXME preserving old behavior, but we probably shouldn't be # doing this... # See https://github.com/qutebrowser/qutebrowser/issues/5094 window.maybe_raise() log.init.debug("Startup cmd {!r}".format(cmd)) commandrunner = runners.CommandRunner(window.win_id) commandrunner.run_safely(cmd[1:]) elif not cmd: log.init.debug("Empty argument") window = mainwindow.get_window(via_ipc=via_ipc, target=new_window_target) else: if via_ipc and target_arg and target_arg != 'auto': open_target = target_arg else: open_target = None if not cwd: # could also be an empty string due to the PyQt signal cwd = None try: url = urlutils.fuzzy_url(cmd, cwd, relative=True) except urlutils.InvalidUrlError as e: message.error("Error in startup argument '{}': {}".format( cmd, e)) else: window = open_url(url, target=open_target, via_ipc=via_ipc) def open_url(url, target=None, no_raise=False, via_ipc=True): """Open a URL in new window/tab. Args: url: A URL to open. target: same as new_instance_open_target (used as a default). no_raise: suppress target window raising. via_ipc: Whether the arguments were transmitted over IPC. Return: The MainWindow of a window that was used to open the URL. """ target = target or config.val.new_instance_open_target background = target in {'tab-bg', 'tab-bg-silent'} window = mainwindow.get_window(via_ipc=via_ipc, target=target, no_raise=no_raise) log.init.debug("About to open URL: {}".format(url.toDisplayString())) window.tabbed_browser.tabopen(url, background=background, related=False) window.show() window.maybe_raise() return window def _open_startpage(window: Optional[mainwindow.MainWindow] = None) -> None: """Open startpage. The startpage is never opened if the given windows are not empty. Args: window: If None, open startpage in all empty windows. If set, open the startpage in the given window. """ if window is not None: windows: Iterable[mainwindow.MainWindow] = [window] else: windows = objreg.window_registry.values() for cur_window in list(windows): # Copying as the dict could change if cur_window.tabbed_browser.widget.count() == 0: log.init.debug("Opening start pages") for url in config.val.url.start_pages: cur_window.tabbed_browser.tabopen(url) def _open_special_pages(args): """Open special notification pages which are only shown once. Args: args: The argparse namespace. """ if args.basedir is not None: # With --basedir given, don't open anything. return general_sect = configfiles.state['general'] tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') pages: List[Tuple[str, bool, str]] = [ # state, condition, URL ('quickstart-done', True, 'https://www.qutebrowser.org/quickstart.html'), ('config-migration-shown', os.path.exists(os.path.join(standarddir.config(), 'qutebrowser.conf')), 'qute://help/configuring.html'), ('webkit-warning-shown', objects.backend == usertypes.Backend.QtWebKit, 'qute://warning/webkit'), ('session-warning-shown', True, 'qute://warning/sessions'), ('sandboxing-warning-shown', ( hasattr(sys, "frozen") and utils.is_mac and machinery.IS_QT6 and os.environ.get("QTWEBENGINE_DISABLE_SANDBOX") == "1" ), 'qute://warning/sandboxing'), ('qt5-warning-shown', ( machinery.IS_QT5 and machinery.INFO.reason == machinery.SelectionReason.auto and objects.backend != usertypes.Backend.QtWebKit ), 'qute://warning/qt5'), ] if 'quickstart-done' not in general_sect: # New users aren't going to be affected by the Qt 5.15 session change much, as # they aren't used to qutebrowser saving the full back/forward history in # sessions. general_sect['session-warning-shown'] = '1' for state, condition, url in pages: if general_sect.get(state) != '1' and condition: tabbed_browser.tabopen(QUrl(url), background=False) general_sect[state] = '1' # Show changelog on new releases change = configfiles.state.qutebrowser_version_changed if change == configfiles.VersionChange.equal: return setting = config.val.changelog_after_upgrade if not change.matches_filter(setting): log.init.debug( f"Showing changelog is disabled (setting {setting}, change {change})") return try: changelog = resources.read_file('html/doc/changelog.html') except OSError as e: log.init.warning(f"Not showing changelog due to {e}") return qbversion = qutebrowser.__version__ if f'id="v{qbversion}"' not in changelog: log.init.warning("Not showing changelog (anchor not found)") return message.info(f"Showing changelog after upgrade to qutebrowser v{qbversion}.") changelog_url = f'qute://help/changelog.html#v{qbversion}' tabbed_browser.tabopen(QUrl(changelog_url), background=False) def on_focus_changed(_old, new): """Register currently focused main window in the object registry.""" if new is None: return if not isinstance(new, QWidget): log.misc.debug("on_focus_changed called with non-QWidget {!r}".format( new)) return window = new.window() if isinstance(window, mainwindow.MainWindow): objreg.register('last-focused-main-window', window, update=True) # A focused window must also be visible, and in this case we should # consider it as the most recently looked-at window objreg.register('last-visible-main-window', window, update=True) def open_desktopservices_url(url): """Handler to open a URL via QDesktopServices.""" target = config.val.new_instance_open_target window = mainwindow.get_window(via_ipc=True, target=target) window.tabbed_browser.tabopen(url) window.show() window.maybe_raise() # This is effectively a @config.change_filter # However, logging is initialized too early to use that annotation def _on_config_changed(name: str) -> None: if name.startswith('logging.'): log.init_from_config(config.val) def _init_modules(*, args): """Initialize all 'modules' which need to be initialized. Args: args: The argparse namespace. """ log.init.debug("Initializing logging from config...") log.init_from_config(config.val) config.instance.changed.connect(_on_config_changed) log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(objects.qapp) objreg.register('save-manager', save_manager) quitter.instance.shutting_down.connect(save_manager.shutdown) configinit.late_init(save_manager) log.init.debug("Checking backend requirements...") backendproblem.init(args=args, save_manager=save_manager) log.init.debug("Initializing prompts...") prompt.init() log.init.debug("Initializing network...") networkmanager.init() log.init.debug("Initializing proxy...") proxy.init() quitter.instance.shutting_down.connect(proxy.shutdown) log.init.debug("Initializing downloads...") downloads.init() quitter.instance.shutting_down.connect(downloads.shutdown) with debug.log_time("init", "Initializing SQL/history"): try: log.init.debug("Initializing web history...") history.init(db_path=pathlib.Path(standarddir.data()) / 'history.sqlite', parent=objects.qapp) except sql.KnownError as e: error.handle_fatal_exc(e, 'Error initializing SQL', pre_text='Error initializing SQL', no_err_windows=args.no_err_windows) sys.exit(usertypes.Exit.err_init) log.init.debug("Initializing command history...") cmdhistory.init() log.init.debug("Initializing websettings...") websettings.init(args) quitter.instance.shutting_down.connect(websettings.shutdown) log.init.debug("Initializing sessions...") sessions.init(objects.qapp) if not args.no_err_windows: crashsignal.crash_handler.display_faulthandler() log.init.debug("Initializing quickmarks...") quickmark_manager = urlmarks.QuickmarkManager(objects.qapp) objreg.register('quickmark-manager', quickmark_manager) log.init.debug("Initializing bookmarks...") bookmark_manager = urlmarks.BookmarkManager(objects.qapp) objreg.register('bookmark-manager', bookmark_manager) log.init.debug("Initializing cookies...") cookies.init(objects.qapp) log.init.debug("Initializing cache...") cache.init(objects.qapp) log.init.debug("Initializing downloads...") qtnetworkdownloads.init() log.init.debug("Initializing Greasemonkey...") greasemonkey.init() log.init.debug("Misc initialization...") macros.init() windowundo.init() nativeeventfilter.init() class Application(QApplication): """Main application instance. Attributes: _args: ArgumentParser instance. _last_focus_object: The last focused object's repr. Signals: new_window: A new window was created. window_closing: A window is being closed. """ new_window = pyqtSignal(mainwindow.MainWindow) window_closing = pyqtSignal(mainwindow.MainWindow) def __init__(self, args): """Constructor. Args: Argument namespace from argparse. """ self._last_focus_object = None qt_args = qtargs.qt_args(args) log.init.debug("Commandline args: {}".format(sys.argv[1:])) log.init.debug("Parsed: {}".format(args)) log.init.debug("Qt arguments: {}".format(qt_args[1:])) super().__init__(qt_args) objects.args = args log.init.debug("Initializing application...") self.launch_time = datetime.datetime.now() self.focusObjectChanged.connect(self.on_focus_object_changed) if machinery.IS_QT5: # default and removed in Qt 6 self.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True) self.new_window.connect(self._on_new_window) @pyqtSlot(mainwindow.MainWindow) def _on_new_window(self, window): window.tabbed_browser.shutting_down.connect(functools.partial( self.window_closing.emit, window)) @pyqtSlot(QObject) def on_focus_object_changed(self, obj): """Log when the focus object changed.""" output = repr(obj) if self._last_focus_object != output: log.misc.debug("Focus object changed: {}".format(output)) self._last_focus_object = output def event(self, e): """Handle macOS FileOpen events.""" if e.type() != QEvent.Type.FileOpen: return super().event(e) url = e.url() if url.isValid(): open_url(url, no_raise=True) else: message.error("Invalid URL: {}".format(url.errorString())) return True def __repr__(self): return utils.get_repr(self)