The Headless Horseman rides!

On linux all worker processes/command line tools now use the headless QPA
plugin when using Qt based code. This means that they no longer require
a running X server or xvfb.
This commit is contained in:
Kovid Goyal 2014-05-10 17:01:51 +05:30
parent d525d9b846
commit d1292bbc0f
10 changed files with 37 additions and 89 deletions

View File

@ -6,11 +6,6 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
# QT5XX: Implement a minimal QPA plugin to allow headless operation, see the
# minimal example in the Qt source code and see
# https://github.com/ariya/phantomjs/pull/173 for info on how to enable fonts
# with fontconfig (probably needed for PDF output and SVG rendering)
# QT5XX: Add a import checker that looks for all from PyQt5.* imports and runs
# them to check that they work. This can probably be made part of python
# setup.py check.

View File

@ -28,9 +28,8 @@ class Unavailable(Exception):
class SVGRasterizer(object):
def __init__(self):
from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt():
raise Unavailable('Not OK to use Qt')
from calibre.gui2 import must_use_qt
must_use_qt()
@classmethod
def config(cls, cfg):

View File

@ -146,9 +146,8 @@ class PDFWriter(QObject):
return self.current_section
def __init__(self, opts, log, cover_data=None, toc=None):
from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt():
raise Exception('Not OK to use Qt')
from calibre.gui2 import must_use_qt
must_use_qt()
QObject.__init__(self)
self.logger = self.log = log

View File

@ -36,9 +36,8 @@ def get_custom_size(opts):
return custom_size
def get_pdf_printer(opts, for_comic=False, output_file_name=None): # {{{
from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt():
raise Exception('Not OK to use Qt')
from calibre.gui2 import must_use_qt
must_use_qt()
printer = QPrinter(QPrinter.HighResolution)
custom_size = get_custom_size(opts)
@ -137,10 +136,9 @@ class Page(QWebPage): # {{{
class PDFWriter(QObject): # {{{
def __init__(self, opts, log, cover_data=None, toc=None):
from calibre.gui2 import is_ok_to_use_qt
from calibre.gui2 import must_use_qt
from calibre.utils.podofo import get_podofo
if not is_ok_to_use_qt():
raise Exception('Not OK to use Qt')
must_use_qt()
QObject.__init__(self)
self.logger = self.log = log

View File

@ -1039,12 +1039,16 @@ def open_local_file(path):
open_url(url)
def must_use_qt():
''' This function should be called if you want to use Qt for some non-GUI
task like rendering HTML/SVG or using a headless browser. It will raise a
RuntimeError if using Qt is not possible, which will happen if the current
thread is not the main GUI thread. On linux, it uses a special QPA headless
plugin, so that the X server does not need to be running. '''
global gui_thread, _store_app
if (islinux or isbsd) and ':' not in os.environ.get('DISPLAY', ''):
raise RuntimeError('X server required. If you are running on a'
' headless machine, use xvfb')
if _store_app is None and QApplication.instance() is None:
_store_app = QApplication([])
if islinux or isbsd:
args = sys.argv[:1] + ['-platformpluginpath', sys.extensions_location, '-platform', 'headless']
_store_app = QApplication(args)
if gui_thread is None:
gui_thread = QThread.currentThread()
if gui_thread is not QThread.currentThread():

View File

@ -94,7 +94,8 @@ class FontKeyChooser(QDialog, Ui_Dialog):
if __name__ == '__main__':
from calibre.gui2 import is_ok_to_use_qt
is_ok_to_use_qt()
from PyQt5.Qt import QApplication
app = QApplication([])
d = FontKeyChooser()
d.exec_()
del app

View File

@ -197,8 +197,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.recipe_model.do_refresh()
self.count_label.setText(
# NOTE: Number of news sources
_('%s news sources') %
self.recipe_model.showing_count)
_('%s news sources') % self.recipe_model.showing_count)
self.schedule_widgets = []
for key in reversed(self.SCHEDULE_TYPES):
@ -583,8 +582,9 @@ class Scheduler(QObject):
break
if __name__ == '__main__':
from calibre.gui2 import is_ok_to_use_qt
is_ok_to_use_qt()
from PyQt5.Qt import QApplication
app = QApplication([])
d = SchedulerDialog(RecipeModel())
d.exec_()
del app

View File

@ -440,9 +440,10 @@ class %(classname)s(%(base_class)s):
ResizableDialog.reject(self)
if __name__ == '__main__':
from calibre.gui2 import is_ok_to_use_qt
is_ok_to_use_qt()
from PyQt5.Qt import QApplication
app = QApplication([])
from calibre.web.feeds.recipes.model import RecipeModel
d=UserProfiles(None, RecipeModel())
d.exec_()
del app

View File

@ -270,11 +270,10 @@ class ShortcutConfig(QWidget):
if __name__ == '__main__':
from calibre.gui2 import is_ok_to_use_qt
app = QApplication([])
from calibre.gui2.viewer.keys import SHORTCUTS
is_ok_to_use_qt()
model = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
conf = ShortcutConfig(model)
conf.resize(400, 500)
conf.show()
QApplication.instance().exec_()
app.exec_()

View File

@ -22,54 +22,14 @@ import time, BaseHTTPServer, os, sys, re, SocketServer
from threading import Lock
from SimpleHTTPServer import SimpleHTTPRequestHandler
# Compiler {{{
try:
from PyQt5.Qt import QJSEngine, QJSValue
class Compiler(QJSEngine):
'''
You can use this class in any thread, but make sure you instantiate it in
the main thread. Alternatively, construct a QCoreApplication in the main
thread, after which you can instantiate this class and use it in any
thread.
'''
def __init__(self):
from PyQt5.Qt import QCoreApplication
if QCoreApplication.instance() is None:
self.__app_ = QCoreApplication([])
QJSEngine.__init__(self)
res = self.evaluate(CS_JS, 'coffee-script.js')
if res.isError():
raise Exception('Failed to run the coffee script compiler: %s'%
res.toString())
self.lock = Lock()
def __call__(self, raw, filename=None):
with self.lock:
if not isinstance(raw, unicode):
raw = raw.decode('utf-8')
if not filename:
filename = '<string>'
go = self.globalObject()
go.setProperty('coffee_src', QJSValue(raw))
res = self.evaluate('this.CoffeeScript.compile(this.coffee_src)',
filename)
if res.isError():
return '', [res.toString()]
return res.toString(), []
except ImportError:
pass
def do_compile(raw, filename=None):
from calibre.gui2 import must_use_qt
must_use_qt()
from PyQt5.Qt import QWebPage, QApplication
import json
app = QApplication([])
app = QApplication.instance()
class C(QWebPage):
@ -95,19 +55,13 @@ def do_compile(raw, filename=None):
# }}}
def compile_coffeescript(raw, filename=None):
from calibre.gui2 import is_ok_to_use_qt
from calibre.utils.ipc.simple_worker import fork_job, WorkerError
try:
if is_ok_to_use_qt():
raise NameError('The WebKit based compiler is much faster than QJSEngine')
return Compiler()(raw, filename)
except NameError:
from calibre.utils.ipc.simple_worker import fork_job, WorkerError
try:
return fork_job('calibre.utils.serve_coffee', 'do_compile',
args=(raw,), no_output=True)['result']
except WorkerError as err:
print (err.orig_tb)
raise
return fork_job('calibre.utils.serve_coffee', 'do_compile',
args=(raw,), no_output=True)['result']
except WorkerError as err:
print (err.orig_tb)
raise
def check_coffeescript(filename):
with open(filename, 'rb') as f:
@ -369,10 +323,8 @@ def main():
cc.add_argument('--highlight', default=False, action='store_true',
help='Syntax highlight the output (requires Pygments)')
cs.add_argument('--port', type=int, default=8000, help=
'The port on which to serve. Default: %default')
cs.add_argument('--host', default='0.0.0.0', help=
'The IP address on which to listen. Default is to listen on all'
cs.add_argument('--port', type=int, default=8000, help='The port on which to serve. Default: %default')
cs.add_argument('--host', default='0.0.0.0', help='The IP address on which to listen. Default is to listen on all'
' IPv4 addresses (0.0.0.0)')
args = parser.parse_args()
if args.which == 'compile':