Do not use a separate process to draw the splash screen

I've now had a couple of reports of calibre hanging randomly on windows
during startup with the splash screen enabled. And in any case with Qt
5.4.1 there is a simpler workaround for the splash screen not rendering
issue
This commit is contained in:
Kovid Goyal 2015-04-19 10:00:07 +05:30
parent 7f73fe505e
commit d4d0ccd970
3 changed files with 39 additions and 185 deletions

View File

@ -5,7 +5,9 @@ import sys, os, time, socket, traceback, re
from functools import partial
import apsw
from PyQt5.Qt import (QCoreApplication, QIcon, QObject, QTimer)
from PyQt5.Qt import (
QCoreApplication, QIcon, QObject, QTimer, Qt, QSplashScreen, QBrush,
QColor, QPixmap)
from calibre import prints, plugins, force_unicode
from calibre.constants import (iswindows, __appname__, isosx, DEBUG, islinux,
@ -15,7 +17,6 @@ from calibre.gui2 import (
ORG_NAME, APP_UID, initialize_file_icon_provider, Application, choose_dir,
error_dialog, question_dialog, gprefs, setup_gui_option_parser)
from calibre.gui2.main_window import option_parser as _option_parser
from calibre.gui2.splash import SplashScreen
from calibre.utils.config import prefs, dynamic
if iswindows:
@ -157,6 +158,34 @@ class EventAccumulator(object):
def __call__(self, ev):
self.events.append(ev)
class SplashScreen(QSplashScreen):
def __init__(self):
self.drawn_once = False
QSplashScreen.__init__(self, QPixmap(I('library.png')))
def drawContents(self, painter):
self.drawn_once = True
painter.setBackgroundMode(Qt.OpaqueMode)
painter.setBackground(QBrush(QColor(0xee, 0xee, 0xee)))
painter.setPen(Qt.black)
painter.setRenderHint(painter.TextAntialiasing, True)
painter.drawText(self.rect().adjusted(5, 5, -5, -5), Qt.AlignLeft, self.message())
def show_message(self, msg):
self.showMessage(msg)
self.wait_for_draw()
def wait_for_draw(self):
# Without this the splash screen is not painted on linux and windows
self.drawn_once = False
st = time.time()
while not self.drawn_once and (time.time() - st < 0.1):
Application.instance().processEvents()
def show(self):
QSplashScreen.show(self)
class GuiRunner(QObject):
'''Make sure an event loop is running before starting the main work of
initialization'''
@ -178,8 +207,9 @@ class GuiRunner(QObject):
main = self.main = Main(self.opts, gui_debug=self.gui_debug)
if self.splash_screen is not None:
self.splash_screen.show_message(_('Initializing user interface...'))
self.splash_screen.finish(main)
with gprefs: # Only write gui.json after initialization is complete
main.initialize(self.library_path, db, self.listener, self.actions, splash_screen=self.splash_screen)
main.initialize(self.library_path, db, self.listener, self.actions)
self.splash_screen = None
if DEBUG:
prints('Started up in %.2f seconds'%(time.time() -
@ -197,22 +227,14 @@ class GuiRunner(QObject):
add_filesystem_book(event)
self.app.file_event_hook = add_filesystem_book
def hide_splash_screen(self):
if self.splash_screen is not None:
with self.app: # Disable quit on last window closed
self.splash_screen.hide()
self.splash_screen = None
def choose_dir(self, initial_dir):
self.hide_splash_screen()
return choose_dir(None, 'choose calibre library',
return choose_dir(self.splash_screen, 'choose calibre library',
_('Choose a location for your new calibre e-book library'),
default_dir=initial_dir)
def show_error(self, title, msg, det_msg=''):
self.hide_splash_screen()
with self.app:
error_dialog(self.main, title, msg, det_msg=det_msg, show=True)
error_dialog(self.splash_screen, title, msg, det_msg=det_msg, show=True)
def initialization_failed(self):
print 'Catastrophic failure initializing GUI, bailing out...'
@ -255,9 +277,8 @@ class GuiRunner(QObject):
try:
db = LibraryDatabase(self.library_path)
except apsw.Error:
self.hide_splash_screen()
with self.app:
repair = question_dialog(None, _('Corrupted database'),
repair = question_dialog(self.splash_screen, _('Corrupted database'),
_('The library database at %s appears to be corrupted. Do '
'you want calibre to try and rebuild it automatically? '
'The rebuild may not be completely successful. '
@ -277,13 +298,13 @@ class GuiRunner(QObject):
self.initialize_db_stage2(db, None)
def show_splash_screen(self):
self.splash_screen = SplashScreen(get_debug_executable())
self.splash_screen = SplashScreen()
self.splash_screen.show()
self.splash_screen.show_message(_('Starting %s: Loading books...') % __appname__)
def initialize(self, *args):
if gprefs['show_splash_screen']:
self.show_splash_screen()
self.library_path = get_library_path(self)
if not self.library_path:
self.initialization_failed()

View File

@ -1,165 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import os, subprocess, time, struct, sys, errno
from threading import Thread
from Queue import Queue
from functools import partial
from PyQt5.Qt import QApplication, QSplashScreen, pyqtSignal, QBrush, QColor, Qt, QPixmap
from calibre.constants import isosx, DEBUG
from calibre.utils.ipc import eintr_retry_call
class SplashScreen(Thread):
daemon = True
def __init__(self, debug_executable):
Thread.__init__(self)
self.queue = Queue()
try:
self.launch_process(debug_executable)
except Exception:
import traceback
traceback.print_exc()
self.process = None
self.keep_going = True
if self.process is not None:
self.start()
self.show_message = partial(self._rpc, 'show_message')
self.hide = partial(self._rpc, 'hide')
def launch_process(self, debug_executable):
from calibre.utils.ipc.simple_worker import start_pipe_worker
if DEBUG:
args = {'stdout':None, 'stderr': None}
else:
args = {'stdout':open(os.devnull, 'wb'), 'stderr':subprocess.STDOUT}
self.process = start_pipe_worker('from calibre.gui2.splash import main; main()', **args)
def _rpc(self, name, *args):
self.queue.put(('_' + name, args))
def run(self):
while self.keep_going:
try:
func, args = self.queue.get()
if func == '_hide':
self.keep_going = False
getattr(self, func)(*args)
except Exception:
import traceback
traceback.print_exc()
self.terminate_worker()
def terminate_worker(self):
if self.process is None:
return
self.process.stdin.close()
# Give the worker two seconds to exit naturally
c = 20
while self.process.poll() is None and c > 0:
time.sleep(0.1)
c -= 1
if self.process.poll() is None:
try:
self.process.terminate()
except EnvironmentError as e:
if getattr(e, 'errno', None) != errno.ESRCH: # ESRCH ==> process does not exist anymore
import traceback
traceback.print_exc()
self.process.wait()
def send(self, msg):
if self.process is not None and not self.process.stdin.closed:
if not isinstance(msg, bytes):
msg = msg.encode('utf-8')
eintr_retry_call(self.process.stdin.write, struct.pack(b'>L', len(msg)) + msg)
def _show_message(self, msg):
self.send(msg)
def _hide(self):
if self.process is not None:
self.process.stdin.close()
def read(amount):
ans = b''
left = amount
while left > 0:
raw = eintr_retry_call(sys.stdin.read, left)
if len(raw) == 0:
raise EOFError('')
left -= len(raw)
ans += raw
return ans
def run_loop(splash_screen):
shutdown = splash_screen.shutdown.emit
try:
while True:
raw = read(4)
mlen = struct.unpack(b'>L', raw)[0]
msg = read(mlen).decode('utf-8')
if not msg:
return shutdown()
splash_screen.show_message.emit(msg)
except EOFError:
pass
except:
import traceback
traceback.print_exc()
return shutdown()
class CalibreSplashScreen(QSplashScreen):
shutdown = pyqtSignal()
show_message = pyqtSignal(object)
def __init__(self):
QSplashScreen.__init__(self, QPixmap(I('library.png')))
def drawContents(self, painter):
painter.setBackgroundMode(Qt.OpaqueMode)
painter.setBackground(QBrush(QColor(0xee, 0xee, 0xee)))
painter.setPen(Qt.black)
painter.setRenderHint(painter.TextAntialiasing, True)
painter.drawText(self.rect().adjusted(5, 5, -5, -5), Qt.AlignLeft, self.message())
def main():
os.closerange(3, 256)
app = QApplication([])
s = CalibreSplashScreen()
s.show_message.connect(s.showMessage, type=Qt.QueuedConnection)
s.shutdown.connect(app.quit, type=Qt.QueuedConnection)
s.show()
t = Thread(target=run_loop, args=(s,))
t.daemon = True
t.start()
app.exec_()
if isosx:
# Showing the splash screen in a separate process doesn't work on OS X and
# I can't be bothered to figure out why
del SplashScreen
class SplashScreen(CalibreSplashScreen):
def __init__(self, *args):
CalibreSplashScreen.__init__(self)
self.show()
def show(self):
CalibreSplashScreen.show(self)
QApplication.instance().processEvents()
QApplication.instance().flush()
def show_message(self, msg):
CalibreSplashScreen.showMessage(self, msg)

View File

@ -209,7 +209,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
else:
stmap[st.name] = st
def initialize(self, library_path, db, listener, actions, show_gui=True, splash_screen=None):
def initialize(self, library_path, db, listener, actions, show_gui=True):
opts = self.opts
self.preferences_action, self.quit_action = actions
self.library_path = library_path
@ -335,8 +335,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if show_gui:
self.show()
if splash_screen is not None:
splash_screen.hide()
if self.system_tray_icon is not None and self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows()