Replace Qt based single application stuff with Listener/Client from multiprocessing

This commit is contained in:
Kovid Goyal 2009-05-15 08:56:53 -07:00
parent 29c172b95e
commit 17f4e28d82
4 changed files with 65 additions and 208 deletions

View File

@ -461,12 +461,6 @@ class ResizableDialog(QDialog):
nw = min(self.width(), nw) nw = min(self.width(), nw)
self.resize(nw, nh) self.resize(nw, nh)
try:
from calibre.utils.single_qt_application import SingleApplication
SingleApplication
except:
SingleApplication = None
gui_thread = None gui_thread = None
class Application(QApplication): class Application(QApplication):

View File

@ -2,8 +2,10 @@ from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''The main GUI''' '''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 xml.parsers.expat import ExpatError
from Queue import Queue, Empty
from threading import Thread
from functools import partial from functools import partial
from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \ from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ 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,\ initialize_file_icon_provider, question_dialog,\
pixmap_to_data, choose_dir, ORG_NAME, \ pixmap_to_data, choose_dir, ORG_NAME, \
set_sidebar_directories, Dispatcher, \ set_sidebar_directories, Dispatcher, \
SingleApplication, Application, available_height, \ Application, available_height, \
max_available_height, config, info_dialog, \ max_available_height, config, info_dialog, \
available_width, GetMetadata available_width, GetMetadata
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror 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.library.database2 import LibraryDatabase2, CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
ADDRESS = r'\\.\pipe\CalibreGUI' if iswindows else \
os.path.expanduser('~/.calibre-gui.socket')
class SaveMenu(QMenu): class SaveMenu(QMenu):
def __init__(self, parent): def __init__(self, parent):
@ -58,6 +63,32 @@ class SaveMenu(QMenu):
def do(self, ext, *args): def do(self, ext, *args):
self.emit(SIGNAL('save_fmt(PyQt_PyObject)'), ext) 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): class Main(MainWindow, Ui_MainWindow, DeviceGUI):
'The main GUI' 'The main GUI'
@ -71,17 +102,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.default_thumbnail = (pixmap.width(), pixmap.height(), self.default_thumbnail = (pixmap.width(), pixmap.height(),
pixmap_to_data(pixmap)) 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 self.preferences_action, self.quit_action = actions
MainWindow.__init__(self, opts, parent) MainWindow.__init__(self, opts, parent)
# Initialize fontconfig in a separate thread as this can be a lengthy # Initialize fontconfig in a separate thread as this can be a lengthy
# process if run for the first time on this machine # process if run for the first time on this machine
self.fc = __import__('calibre.utils.fontconfig', fromlist=1) self.fc = __import__('calibre.utils.fontconfig', fromlist=1)
self.single_instance = single_instance self.listener = Listener(listener)
if self.single_instance is not None: self.check_messages_timer = QTimer()
self.connect(self.single_instance, self.connect(self.check_messages_timer, SIGNAL('timeout()'),
SIGNAL('message_received(PyQt_PyObject)'), self.another_instance_wants_to_talk)
self.another_instance_wants_to_talk) self.check_messages_timer.start(1000)
Ui_MainWindow.__init__(self) Ui_MainWindow.__init__(self)
self.setupUi(self) self.setupUi(self)
@ -563,7 +594,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.cover_flow.currentSlide() != index.row(): self.cover_flow.currentSlide() != index.row():
self.cover_flow.setCurrentSlide(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:'): if msg.startswith('launched:'):
argv = eval(msg[len('launched:'):]) argv = eval(msg[len('launched:'):])
if len(argv) > 1: if len(argv) > 1:
@ -1567,6 +1602,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
def shutdown(self, write_settings=True): def shutdown(self, write_settings=True):
if write_settings: if write_settings:
self.write_settings() self.write_settings()
self.check_messages_timer.stop()
self.listener.close()
self.job_manager.server.close() self.job_manager.server.close()
self.device_manager.keep_going = False self.device_manager.keep_going = False
self.cover_cache.stop() self.cover_cache.stop()
@ -1645,8 +1682,6 @@ path_to_ebook to the database.
return parser return parser
def main(args=sys.argv): def main(args=sys.argv):
from calibre.utils.lock import singleinstance
pid = os.fork() if False and islinux else -1 pid = os.fork() if False and islinux else -1
if pid <= 0: if pid <= 0:
parser = option_parser() parser = option_parser()
@ -1659,23 +1694,26 @@ def main(args=sys.argv):
app.setWindowIcon(QIcon(':/library')) app.setWindowIcon(QIcon(':/library'))
QCoreApplication.setOrganizationName(ORG_NAME) QCoreApplication.setOrganizationName(ORG_NAME)
QCoreApplication.setApplicationName(APP_UID) QCoreApplication.setApplicationName(APP_UID)
single_instance = None if SingleApplication is None else \ from multiprocessing.connection import Listener, Client
SingleApplication('calibre GUI') try:
if not singleinstance('calibre GUI'): listener = Listener(address=ADDRESS)
if len(args) > 1: except socket.error, err:
args[1] = os.path.abspath(args[1]) try:
if single_instance is not None and \ conn = Client(ADDRESS)
single_instance.is_running() and \ if len(args) > 1:
single_instance.send_message('launched:'+repr(args)): args[1] = os.path.abspath(args[1])
return 0 conn.send('launched:'+repr(args))
extra = '' if iswindows else \ conn.close()
('If you\'re sure it is not running, delete the file ' except:
'%s.'%os.path.expanduser('~/.calibre_calibre GUI.lock')) extra = '' if iswindows else \
QMessageBox.critical(None, _('Cannot Start ')+__appname__, _('If you\'re sure it is not running, delete the file %s')\
%ADDRESS
QMessageBox.critical(None, _('Cannot Start ')+__appname__,
_('<p>%s is already running. %s</p>')%(__appname__, extra)) _('<p>%s is already running. %s</p>')%(__appname__, extra))
return 1 return 1
initialize_file_icon_provider() initialize_file_icon_provider()
main = Main(single_instance, opts, actions) main = Main(listener, opts, actions)
sys.excepthook = main.unhandled_exception sys.excepthook = main.unhandled_exception
if len(args) > 1: if len(args) > 1:
main.add_filesystem_book(args[1]) main.add_filesystem_book(args[1])

View File

@ -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_()

3
todo
View File

@ -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. * 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