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)
|
||||
self.resize(nw, nh)
|
||||
|
||||
try:
|
||||
from calibre.utils.single_qt_application import SingleApplication
|
||||
SingleApplication
|
||||
except:
|
||||
SingleApplication = None
|
||||
|
||||
gui_thread = None
|
||||
|
||||
class Application(QApplication):
|
||||
|
@ -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.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'):
|
||||
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])
|
||||
if single_instance is not None and \
|
||||
single_instance.is_running() and \
|
||||
single_instance.send_message('launched:'+repr(args)):
|
||||
return 0
|
||||
conn.send('launched:'+repr(args))
|
||||
conn.close()
|
||||
except:
|
||||
extra = '' if iswindows else \
|
||||
('If you\'re sure it is not running, delete the file '
|
||||
'%s.'%os.path.expanduser('~/.calibre_calibre GUI.lock'))
|
||||
_('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])
|
||||
|
@ -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.
|
||||
|
||||
* 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