mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Replace Qt based single application stuff with Listener/Client from multiprocessing
This commit is contained in:
parent
29c172b95e
commit
17f4e28d82
@ -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):
|
||||||
|
@ -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)
|
||||||
|
except socket.error, err:
|
||||||
|
try:
|
||||||
|
conn = Client(ADDRESS)
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
args[1] = os.path.abspath(args[1])
|
args[1] = os.path.abspath(args[1])
|
||||||
if single_instance is not None and \
|
conn.send('launched:'+repr(args))
|
||||||
single_instance.is_running() and \
|
conn.close()
|
||||||
single_instance.send_message('launched:'+repr(args)):
|
except:
|
||||||
return 0
|
|
||||||
extra = '' if iswindows else \
|
extra = '' if iswindows else \
|
||||||
('If you\'re sure it is not running, delete the file '
|
_('If you\'re sure it is not running, delete the file %s')\
|
||||||
'%s.'%os.path.expanduser('~/.calibre_calibre GUI.lock'))
|
%ADDRESS
|
||||||
QMessageBox.critical(None, _('Cannot Start ')+__appname__,
|
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])
|
||||||
|
@ -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
3
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.
|
* 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user