Merge branch 'ipc'
This commit is contained in:
commit
ca85dde71f
@ -43,7 +43,8 @@ from qutebrowser.browser import quickmarks, cookies, downloads, cache
|
|||||||
from qutebrowser.widgets import mainwindow, crash
|
from qutebrowser.widgets import mainwindow, crash
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman
|
||||||
from qutebrowser.utils import (log, version, message, readline, utils, qtutils,
|
from qutebrowser.utils import (log, version, message, readline, utils, qtutils,
|
||||||
urlutils, debug, objreg, usertypes, standarddir)
|
urlutils, debug, objreg, usertypes, standarddir,
|
||||||
|
ipc)
|
||||||
# We import utilcmds to run the cmdutils.register decorators.
|
# We import utilcmds to run the cmdutils.register decorators.
|
||||||
from qutebrowser.utils import utilcmds # pylint: disable=unused-import
|
from qutebrowser.utils import utilcmds # pylint: disable=unused-import
|
||||||
|
|
||||||
@ -102,6 +103,10 @@ class Application(QApplication):
|
|||||||
print(version.GPL_BOILERPLATE.strip())
|
print(version.GPL_BOILERPLATE.strip())
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
sent = ipc.send_to_running_instance(self._args.command)
|
||||||
|
if sent:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
log.init.debug("Starting init...")
|
log.init.debug("Starting init...")
|
||||||
self.setQuitOnLastWindowClosed(False)
|
self.setQuitOnLastWindowClosed(False)
|
||||||
self.setOrganizationName("qutebrowser")
|
self.setOrganizationName("qutebrowser")
|
||||||
@ -121,6 +126,9 @@ class Application(QApplication):
|
|||||||
log.init.debug("Applying python hacks...")
|
log.init.debug("Applying python hacks...")
|
||||||
self._python_hacks()
|
self._python_hacks()
|
||||||
|
|
||||||
|
log.init.debug("Starting IPC server...")
|
||||||
|
ipc.init()
|
||||||
|
|
||||||
log.init.debug("Init done!")
|
log.init.debug("Init done!")
|
||||||
|
|
||||||
if self._crashdlg is not None:
|
if self._crashdlg is not None:
|
||||||
@ -218,17 +226,24 @@ class Application(QApplication):
|
|||||||
|
|
||||||
def _open_pages(self):
|
def _open_pages(self):
|
||||||
"""Open startpage etc. and process commandline args."""
|
"""Open startpage etc. and process commandline args."""
|
||||||
self._process_init_args()
|
self.process_args(self._args.command)
|
||||||
self._open_startpage()
|
self._open_startpage()
|
||||||
self._open_quickstart()
|
self._open_quickstart()
|
||||||
|
|
||||||
def _process_init_args(self):
|
def process_args(self, args, ipc=False):
|
||||||
"""Process commandline args.
|
"""Process commandline args.
|
||||||
|
|
||||||
URLs to open have no prefix, commands to execute begin with a colon.
|
URLs to open have no prefix, commands to execute begin with a colon.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: A list of arguments to process.
|
||||||
|
ipc: Whether the arguments were transmitted over IPC.
|
||||||
"""
|
"""
|
||||||
|
if ipc:
|
||||||
|
win_id = mainwindow.MainWindow.spawn()
|
||||||
|
else:
|
||||||
win_id = 0
|
win_id = 0
|
||||||
for cmd in self._args.command:
|
for cmd in args:
|
||||||
if cmd.startswith(':'):
|
if cmd.startswith(':'):
|
||||||
log.init.debug("Startup cmd {}".format(cmd))
|
log.init.debug("Startup cmd {}".format(cmd))
|
||||||
commandrunner = runners.CommandRunner(win_id)
|
commandrunner = runners.CommandRunner(win_id)
|
||||||
@ -621,6 +636,11 @@ class Application(QApplication):
|
|||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=win_id)
|
window=win_id)
|
||||||
tabbed_browser.shutdown()
|
tabbed_browser.shutdown()
|
||||||
|
# Shut down IPC
|
||||||
|
try:
|
||||||
|
objreg.get('ipc-server').shutdown()
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
# Save everything
|
# Save everything
|
||||||
try:
|
try:
|
||||||
config_obj = objreg.get('config')
|
config_obj = objreg.get('config')
|
||||||
|
217
qutebrowser/utils/ipc.py
Normal file
217
qutebrowser/utils/ipc.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2014 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/>.
|
||||||
|
|
||||||
|
"""Utilities for IPC with existing instances."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import getpass
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSlot, QObject
|
||||||
|
from PyQt5.QtNetwork import QLocalSocket, QLocalServer
|
||||||
|
|
||||||
|
from qutebrowser.utils import log, objreg, usertypes
|
||||||
|
|
||||||
|
|
||||||
|
SOCKETNAME = 'qutebrowser-{}'.format(getpass.getuser())
|
||||||
|
CONNECT_TIMEOUT = 100
|
||||||
|
WRITE_TIMEOUT = 1000
|
||||||
|
READ_TIMEOUT = 5000
|
||||||
|
|
||||||
|
|
||||||
|
class IPCError(Exception):
|
||||||
|
|
||||||
|
"""Exception raised when there was a problem with IPC."""
|
||||||
|
|
||||||
|
|
||||||
|
class IPCServer(QObject):
|
||||||
|
|
||||||
|
"""IPC server to which clients connect to.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_timer: A timer to handle timeouts.
|
||||||
|
_server: A QLocalServer to accept new connections.
|
||||||
|
_socket: The QLocalSocket we're currently connected to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
"""Start the IPC server and listen to commands."""
|
||||||
|
super().__init__(parent)
|
||||||
|
self._remove_server()
|
||||||
|
self._timer = usertypes.Timer(self, 'ipc-timeout')
|
||||||
|
self._timer.setInterval(READ_TIMEOUT)
|
||||||
|
self._timer.timeout.connect(self.on_timeout)
|
||||||
|
self._server = QLocalServer(self)
|
||||||
|
ok = self._server.listen(SOCKETNAME)
|
||||||
|
if not ok:
|
||||||
|
raise IPCError("Error while listening to IPC server: {} "
|
||||||
|
"(error {})".format(self._server.errorString(),
|
||||||
|
self._server.serverError()))
|
||||||
|
self._server.newConnection.connect(self.handle_connection)
|
||||||
|
self._socket = None
|
||||||
|
|
||||||
|
def _remove_server(self):
|
||||||
|
"""Remove an existing server."""
|
||||||
|
ok = QLocalServer.removeServer(SOCKETNAME)
|
||||||
|
if not ok:
|
||||||
|
raise IPCError("Error while removing server {}!".format(
|
||||||
|
SOCKETNAME))
|
||||||
|
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def on_error(self, error):
|
||||||
|
"""Convenience method which calls _socket_error on an error."""
|
||||||
|
self._timer.stop()
|
||||||
|
log.ipc.debug("Socket error {}: {}".format(
|
||||||
|
self._socket.error(), self._socket.errorString()))
|
||||||
|
if error != QLocalSocket.PeerClosedError:
|
||||||
|
_socket_error("handling IPC connection", self._socket)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def handle_connection(self):
|
||||||
|
"""Handle a new connection to the server."""
|
||||||
|
if self._socket is not None:
|
||||||
|
log.ipc.debug("Got new connection but ignoring it because we're "
|
||||||
|
"still handling another one.")
|
||||||
|
return
|
||||||
|
socket = self._server.nextPendingConnection()
|
||||||
|
if socket is None:
|
||||||
|
log.ipc.debug("No new connection to handle.")
|
||||||
|
return
|
||||||
|
log.ipc.debug("Client connected.")
|
||||||
|
self._timer.start()
|
||||||
|
self._socket = socket
|
||||||
|
socket.readyRead.connect(self.on_ready_read)
|
||||||
|
if socket.canReadLine():
|
||||||
|
log.ipc.debug("We can read a line immediately.")
|
||||||
|
self.on_ready_read()
|
||||||
|
socket.error.connect(self.on_error)
|
||||||
|
if socket.error() not in (QLocalSocket.UnknownSocketError,
|
||||||
|
QLocalSocket.PeerClosedError):
|
||||||
|
log.ipc.debug("We got an error immediately.")
|
||||||
|
self.on_error(socket.error())
|
||||||
|
socket.disconnected.connect(self.on_disconnected)
|
||||||
|
if socket.state() == QLocalSocket.UnconnectedState:
|
||||||
|
log.ipc.debug("Socket was disconnected immediately.")
|
||||||
|
self.on_disconnected()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def on_disconnected(self):
|
||||||
|
"""Clean up socket when the client disconnected."""
|
||||||
|
log.ipc.debug("Client disconnected.")
|
||||||
|
self._timer.stop()
|
||||||
|
self._socket.deleteLater()
|
||||||
|
self._socket = None
|
||||||
|
# Maybe another connection is waiting.
|
||||||
|
self.handle_connection()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def on_ready_read(self):
|
||||||
|
"""Read json data from the client."""
|
||||||
|
if self._socket is None:
|
||||||
|
# this happened once and I don't know why
|
||||||
|
log.ipc.warn("In on_ready_read with None socket!")
|
||||||
|
return
|
||||||
|
self._timer.start()
|
||||||
|
while self._socket.canReadLine():
|
||||||
|
data = bytes(self._socket.readLine())
|
||||||
|
log.ipc.debug("Read from socket: {}".format(data))
|
||||||
|
try:
|
||||||
|
decoded = data.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
log.ipc.error("Ignoring invalid IPC data.")
|
||||||
|
log.ipc.debug("invalid data: {}".format(binascii.hexlify(data)))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
args = json.loads(decoded)
|
||||||
|
except ValueError:
|
||||||
|
log.ipc.error("Ignoring invalid IPC data.")
|
||||||
|
log.ipc.debug("invalid json: {}".format(decoded.strip()))
|
||||||
|
return
|
||||||
|
log.ipc.debug("Processing: {}".format(decoded))
|
||||||
|
app = objreg.get('app')
|
||||||
|
app.process_args(args, ipc=True)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def on_timeout(self):
|
||||||
|
"""Cancel the current connection if it was idle for too long."""
|
||||||
|
log.ipc.error("IPC connection timed out.")
|
||||||
|
self._socket.close()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Shut down the IPC server cleanly."""
|
||||||
|
if self._socket is not None:
|
||||||
|
self._socket.deleteLater()
|
||||||
|
self._socket = None
|
||||||
|
self._timer.stop()
|
||||||
|
self._server.close()
|
||||||
|
self._server.deleteLater()
|
||||||
|
self._remove_server()
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
"""Initialize the global IPC server."""
|
||||||
|
app = objreg.get('app')
|
||||||
|
server = IPCServer(app)
|
||||||
|
objreg.register('ipc-server', server)
|
||||||
|
|
||||||
|
|
||||||
|
def _socket_error(action, socket):
|
||||||
|
"""Raise an IPCError based on an action and a QLocalSocket.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action: A string like "writing to running instance".
|
||||||
|
socket: A QLocalSocket.
|
||||||
|
"""
|
||||||
|
raise IPCError("Error while {}: {} (error {})".format(
|
||||||
|
action, socket.errorString(), socket.error()))
|
||||||
|
|
||||||
|
|
||||||
|
def send_to_running_instance(cmdlist):
|
||||||
|
"""Try to send a commandline to a running instance.
|
||||||
|
|
||||||
|
Blocks for CONNECT_TIMEOUT ms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmdlist: A list to send (URLs/commands)
|
||||||
|
|
||||||
|
Return:
|
||||||
|
True if connecting was successful, False if no connection was made.
|
||||||
|
"""
|
||||||
|
socket = QLocalSocket()
|
||||||
|
socket.connectToServer(SOCKETNAME)
|
||||||
|
connected = socket.waitForConnected(100)
|
||||||
|
if connected:
|
||||||
|
log.ipc.info("Opening in existing instance")
|
||||||
|
line = json.dumps(cmdlist) + '\n'
|
||||||
|
data = line.encode('utf-8')
|
||||||
|
log.ipc.debug("Writing: {}".format(data))
|
||||||
|
socket.writeData(data)
|
||||||
|
socket.waitForBytesWritten(WRITE_TIMEOUT)
|
||||||
|
if socket.error() != QLocalSocket.UnknownSocketError:
|
||||||
|
_socket_error("writing to running instance", socket)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if socket.error() not in (QLocalSocket.ConnectionRefusedError,
|
||||||
|
QLocalSocket.ServerNotFoundError):
|
||||||
|
_socket_error("connecting to running instance", socket)
|
||||||
|
else:
|
||||||
|
log.ipc.debug("No existing instance present (error {})".format(
|
||||||
|
socket.error()))
|
||||||
|
return False
|
@ -124,6 +124,7 @@ js = logging.getLogger('js') # Javascript console messages
|
|||||||
qt = logging.getLogger('qt') # Warnings produced by Qt
|
qt = logging.getLogger('qt') # Warnings produced by Qt
|
||||||
style = logging.getLogger('style')
|
style = logging.getLogger('style')
|
||||||
rfc6266 = logging.getLogger('rfc6266')
|
rfc6266 = logging.getLogger('rfc6266')
|
||||||
|
ipc = logging.getLogger('ipc')
|
||||||
|
|
||||||
|
|
||||||
ram_handler = None
|
ram_handler = None
|
||||||
|
Loading…
Reference in New Issue
Block a user