From 17f4e28d8283ee025c27349c7f35780ea9f3be8c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 May 2009 08:56:53 -0700 Subject: [PATCH] Replace Qt based single application stuff with Listener/Client from multiprocessing --- src/calibre/gui2/__init__.py | 6 - src/calibre/gui2/main.py | 88 ++++++++--- src/calibre/utils/single_qt_application.py | 176 --------------------- todo | 3 +- 4 files changed, 65 insertions(+), 208 deletions(-) delete mode 100644 src/calibre/utils/single_qt_application.py diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 2a5a02495a..3fbc3a9e10 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -461,12 +461,6 @@ class ResizableDialog(QDialog): nw = min(self.width(), nw) self.resize(nw, nh) -try: - from calibre.utils.single_qt_application import SingleApplication - SingleApplication -except: - SingleApplication = None - gui_thread = None class Application(QApplication): diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 8f632ad10e..93a8cfc20a 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -2,8 +2,10 @@ from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' '''The main GUI''' -import os, sys, textwrap, collections, traceback, time +import os, sys, textwrap, collections, traceback, time, socket from xml.parsers.expat import ExpatError +from Queue import Queue, Empty +from threading import Thread from functools import partial from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \ QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ @@ -20,7 +22,7 @@ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \ initialize_file_icon_provider, question_dialog,\ pixmap_to_data, choose_dir, ORG_NAME, \ set_sidebar_directories, Dispatcher, \ - SingleApplication, Application, available_height, \ + Application, available_height, \ max_available_height, config, info_dialog, \ available_width, GetMetadata from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror @@ -45,6 +47,9 @@ from calibre.ebooks import BOOK_EXTENSIONS from calibre.library.database2 import LibraryDatabase2, CoverCache from calibre.gui2.dialogs.confirm_delete import confirm +ADDRESS = r'\\.\pipe\CalibreGUI' if iswindows else \ + os.path.expanduser('~/.calibre-gui.socket') + class SaveMenu(QMenu): def __init__(self, parent): @@ -58,6 +63,32 @@ class SaveMenu(QMenu): def do(self, ext, *args): self.emit(SIGNAL('save_fmt(PyQt_PyObject)'), ext) +class Listener(Thread): + + def __init__(self, listener): + Thread.__init__(self) + self.daemon = True + self.listener, self.queue = listener, Queue() + self._run = True + self.start() + + def run(self): + while self._run: + try: + conn = self.listener.accept() + msg = conn.recv() + self.queue.put(msg) + except: + continue + + def close(self): + self._run = False + try: + self.listener.close() + except: + pass + + class Main(MainWindow, Ui_MainWindow, DeviceGUI): 'The main GUI' @@ -71,17 +102,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.default_thumbnail = (pixmap.width(), pixmap.height(), pixmap_to_data(pixmap)) - def __init__(self, single_instance, opts, actions, parent=None): + def __init__(self, listener, opts, actions, parent=None): self.preferences_action, self.quit_action = actions MainWindow.__init__(self, opts, parent) # Initialize fontconfig in a separate thread as this can be a lengthy # process if run for the first time on this machine self.fc = __import__('calibre.utils.fontconfig', fromlist=1) - self.single_instance = single_instance - if self.single_instance is not None: - self.connect(self.single_instance, - SIGNAL('message_received(PyQt_PyObject)'), - self.another_instance_wants_to_talk) + self.listener = Listener(listener) + self.check_messages_timer = QTimer() + self.connect(self.check_messages_timer, SIGNAL('timeout()'), + self.another_instance_wants_to_talk) + self.check_messages_timer.start(1000) Ui_MainWindow.__init__(self) self.setupUi(self) @@ -563,7 +594,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.cover_flow.currentSlide() != index.row(): self.cover_flow.setCurrentSlide(index.row()) - def another_instance_wants_to_talk(self, msg): + def another_instance_wants_to_talk(self): + try: + msg = self.listener.queue.get_nowait() + except Empty: + return if msg.startswith('launched:'): argv = eval(msg[len('launched:'):]) if len(argv) > 1: @@ -1567,6 +1602,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): def shutdown(self, write_settings=True): if write_settings: self.write_settings() + self.check_messages_timer.stop() + self.listener.close() self.job_manager.server.close() self.device_manager.keep_going = False self.cover_cache.stop() @@ -1645,8 +1682,6 @@ path_to_ebook to the database. return parser def main(args=sys.argv): - from calibre.utils.lock import singleinstance - pid = os.fork() if False and islinux else -1 if pid <= 0: parser = option_parser() @@ -1659,23 +1694,26 @@ def main(args=sys.argv): app.setWindowIcon(QIcon(':/library')) QCoreApplication.setOrganizationName(ORG_NAME) QCoreApplication.setApplicationName(APP_UID) - single_instance = None if SingleApplication is None else \ - SingleApplication('calibre GUI') - if not singleinstance('calibre GUI'): - if len(args) > 1: - args[1] = os.path.abspath(args[1]) - if single_instance is not None and \ - single_instance.is_running() and \ - single_instance.send_message('launched:'+repr(args)): - return 0 - extra = '' if iswindows else \ - ('If you\'re sure it is not running, delete the file ' - '%s.'%os.path.expanduser('~/.calibre_calibre GUI.lock')) - QMessageBox.critical(None, _('Cannot Start ')+__appname__, + from multiprocessing.connection import Listener, Client + try: + listener = Listener(address=ADDRESS) + except socket.error, err: + try: + conn = Client(ADDRESS) + if len(args) > 1: + args[1] = os.path.abspath(args[1]) + conn.send('launched:'+repr(args)) + conn.close() + except: + extra = '' if iswindows else \ + _('If you\'re sure it is not running, delete the file %s')\ + %ADDRESS + QMessageBox.critical(None, _('Cannot Start ')+__appname__, _('

%s is already running. %s

')%(__appname__, extra)) return 1 + initialize_file_icon_provider() - main = Main(single_instance, opts, actions) + main = Main(listener, opts, actions) sys.excepthook = main.unhandled_exception if len(args) > 1: main.add_filesystem_book(args[1]) diff --git a/src/calibre/utils/single_qt_application.py b/src/calibre/utils/single_qt_application.py deleted file mode 100644 index 846736c507..0000000000 --- a/src/calibre/utils/single_qt_application.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env python -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' -__docformat__ = 'restructuredtext en' - -''' -Enforces running of only a single application instance and allows for messaging between -applications using a local socket. -''' -import atexit, os - -from PyQt4.QtCore import QByteArray, QDataStream, QIODevice, SIGNAL, QObject, Qt, QString -from PyQt4.QtNetwork import QLocalSocket, QLocalServer - -timeout_read = 5000 -timeout_connect = 500 - -def write_message(socket, message, timeout = 5000): - block = QByteArray() - out = QDataStream(block, QIODevice.WriteOnly) - - out.writeInt32(0) - out.writeString(QString(message)) - out.device().seek(0) - out.writeInt32(len(message)) - - socket.write(block) - - return getattr(socket, 'state', lambda : None)() == QLocalSocket.ConnectedState and \ - bool(socket.waitForBytesWritten(timeout)) - -def read_message(socket): - if getattr(socket, 'state', lambda : None)() != QLocalSocket.ConnectedState: - return '' - - while socket.bytesAvailable() < 4: - if not socket.waitForReadyRead(timeout_read): - return '' - - message = '' - ins = QDataStream(socket) - block_size = ins.readInt32() - while socket.bytesAvailable() < block_size: - if not socket.waitForReadyRead(timeout_read): - return message - return str(ins.readString()) - -class Connection(QObject): - - def __init__(self, socket, name): - QObject.__init__(self) - self.socket = socket - self.name = name - self.magic = self.name + ':' - self.connect(self.socket, SIGNAL('readyRead()'), self.read_msg, Qt.QueuedConnection) - self.write_succeeded = write_message(self.socket, self.name) - self.connect(self.socket, SIGNAL('disconnected()'), self.disconnected) - if not self.write_succeeded: - self.socket.abort() - - def read_msg(self): - while self.socket.bytesAvailable() > 0: - msg = read_message(self.socket) - if msg.startswith(self.magic): - self.emit(SIGNAL('message_received(PyQt_PyObject)'), msg[len(self.magic):]) - - def disconnected(self): - self.emit(SIGNAL('disconnected()')) - - -class LocalServer(QLocalServer): - - def __init__(self, server_id, parent=None): - QLocalServer.__init__(self, parent) - self.server_id = str(server_id) - self.mr = lambda x : self.emit(SIGNAL('message_received(PyQt_PyObject)'), x) - self.connections = [] - self.connect(self, SIGNAL('newConnection()'), self.new_connection) - - def new_connection(self): - socket = self.nextPendingConnection() - conn = Connection(socket, self.server_id) - if conn.socket.state() != QLocalSocket.UnconnectedState: - self.connect(conn, SIGNAL('message_received(PyQt_PyObject)'), self.mr) - self.connect(conn, SIGNAL('disconnected()'), self.free) - self.connections.append(conn) - - def free(self): - pop = [] - for conn in self.connections: - if conn.socket.state() == QLocalSocket.UnconnectedState: - pop.append(conn) - - for conn in pop: - self.connections.remove(conn) - - def listen(self, name): - if not QLocalServer.listen(self, name): - try: - os.unlink(self.fullServerName()) - except: - pass - return QLocalServer.listen(self, name) - return True - - -def send_message(msg, name, server_name='calibre_server', timeout=5000): - socket = QLocalSocket() - socket.connectToServer(server_name) - if socket.waitForConnected(timeout_connect): - if read_message(socket) == name: - write_message(socket, name+':'+msg, timeout) - -class SingleApplication(QObject): - - def __init__(self, name, parent=None, server_name='calibre_server'): - QObject.__init__(self, parent) - self.name = name - self.server_name = server_name - self.running = False - self.mr = lambda x : self.emit(SIGNAL('message_received(PyQt_PyObject)'), x) - - # Check if server is already running - self.socket = QLocalSocket(self) - self.socket.connectToServer(self.server_name) - if self.socket.waitForConnected(timeout_connect): - msg = read_message(self.socket) - if msg == self.name: - self.running = True - - - # Start server - self.server = None - if not self.running: - self.socket.abort() - self.socket = None - self.server = LocalServer(self.name, self) - self.connect(self.server, SIGNAL('message_received(PyQt_PyObject)'), - self.mr, Qt.QueuedConnection) - - if not self.server.listen(self.server_name): - self.server = None - if self.server is not None: - atexit.register(self.server.close) - - - def is_running(self, name=None): - return self.running if name is None else SingleApplication().is_running() - - def send_message(self, msg, timeout=3000): - return self.running and write_message(self.socket, self.name+':'+msg, timeout) - -if __name__ == '__main__': - from PyQt4.Qt import QWidget, QApplication - class Test(QWidget): - - def __init__(self, sa): - QWidget.__init__(self) - self.sa = sa - self.connect(sa, SIGNAL('message_received(PyQt_PyObject)'), self.mr) - - def mr(self, msg): - print 'Message received:', msg - - app = QApplication([]) - app.connect(app, SIGNAL('lastWindowClosed()'), app.quit) - sa = SingleApplication('test SA') - if sa.is_running(): - sa.send_message('test message') - else: - widget = Test(sa) - widget.show() - app.exec_() - - - \ No newline at end of file diff --git a/todo b/todo index 25e164de88..abcb72d250 100644 --- a/todo +++ b/todo @@ -3,5 +3,6 @@ * Rationalize books table. Add a pubdate column, remove the uri column (and associated support in add_books) and convert series_index to a float. -* Replace single application stuff with Listener from multiprocessing +* Refactor save to disk into separate process +* Testing framework