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)
self.resize(nw, nh)
try:
from calibre.utils.single_qt_application import SingleApplication
SingleApplication
except:
SingleApplication = None
gui_thread = None
class Application(QApplication):

View File

@ -2,8 +2,10 @@ from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''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__,
_('<p>%s is already running. %s</p>')%(__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])

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.
* Replace single application stuff with Listener from multiprocessing
* Refactor save to disk into separate process
* Testing framework