From 0aa2c3349ffd8711680492cb68fff5231590dda3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Jun 2015 16:23:56 +0530 Subject: [PATCH] Start work on standalone server --- src/calibre/debug.py | 5 ++ src/calibre/srv/handler.py | 27 ++++++++ src/calibre/srv/opts.py | 43 ++++++++++++- src/calibre/srv/standalone.py | 113 ++++++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/calibre/srv/handler.py create mode 100644 src/calibre/srv/standalone.py diff --git a/src/calibre/debug.py b/src/calibre/debug.py index 1b91b71063..e8bac90dd3 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -81,6 +81,8 @@ Everything after the -- is passed to the script. 'calibre-debug --diff file1 file2')) parser.add_option('--default-programs', default=None, choices=['register', 'unregister'], help=_('(Un)register calibre from Windows Default Programs.') + ' --default-programs=(register|unregister)') + parser.add_option('--new-server', action='store_true', + help='Run the new calibre content server. Any options specified after a -- will be passed to the server.') return parser @@ -278,6 +280,9 @@ def main(args=sys.argv): from calibre.utils.winreg.default_programs import unregister as func print 'Running', func.__name__, '...' func() + elif opts.new_server: + from calibre.srv.standalone import main + main(args) elif len(args) >= 2 and args[1].rpartition('.')[-1] in {'py', 'recipe'}: run_script(args[1], args[2:]) elif len(args) >= 2 and args[1].rpartition('.')[-1] in {'mobi', 'azw', 'azw3', 'docx', 'odt'}: diff --git a/src/calibre/srv/handler.py b/src/calibre/srv/handler.py new file mode 100644 index 0000000000..2c9286648f --- /dev/null +++ b/src/calibre/srv/handler.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2015, Kovid Goyal ' + +from calibre.srv.routes import Router + +class LibraryBroker(object): + + def __init__(self, libraries): + self.libraries = libraries + +class Context(object): + + def __init__(self, libraries): + self.library_broker = LibraryBroker(libraries) + +class Handler(object): + + def __init__(self, libraries, opts): + self.router = Router(ctx=Context(libraries), url_prefix=opts.url_prefix) + self.router.ctx.url_for = self.router.url_for + self.dispatch = self.router.dispatch + diff --git a/src/calibre/srv/opts.py b/src/calibre/srv/opts.py index 462b31ced5..785e0a53bf 100644 --- a/src/calibre/srv/opts.py +++ b/src/calibre/srv/opts.py @@ -9,6 +9,7 @@ __copyright__ = '2015, Kovid Goyal ' from itertools import izip_longest from collections import namedtuple, OrderedDict from operator import attrgetter +from functools import partial Option = namedtuple('Option', 'name default longdoc shortdoc choices') @@ -36,7 +37,7 @@ raw_options = ( 'shutdown_timeout', 5.0, None, - 'Allow socket pre-allocation, for example, with systemd socket activation', + 'Enable/disable socket pre-allocation, for example, with systemd socket activation', 'allow_socket_preallocation', True, None, @@ -66,11 +67,16 @@ raw_options = ( ' example, "127.0.0.1" to only listen for connections from the local machine, or' ' to "::" to listen to all incoming IPv6 and IPv4 connections.', - 'Use zero copy file transfers for increased performance', + 'Enable/disable zero copy file transfers for increased performance', 'use_sendfile', True, 'This will use zero-copy in-kernel transfers when sending files over the network,' ' increasing performance. However, it can cause corrupted file transfers on some' ' broken filesystems. If you experience corrupted file transfers, turn it off.', + + 'Max. log file size (in MB)', + 'max_log_size', 20, + 'The maximum size of log files, generated by the server. When the log becomes larger' + ' than this size, it is automatically rotated.', ) assert len(raw_options) % 4 == 0 @@ -97,3 +103,36 @@ class Options(object): def __init__(self, **kwargs): for opt in options.itervalues(): setattr(self, opt.name, kwargs.get(opt.name, opt.default)) + +def opt_to_cli_help(opt): + ans = opt.shortdoc + if not ans.endswith('.'): + ans += '.' + if opt.longdoc: + ans += '\n\t' + opt.longdoc + return ans + +def boolean_option(add_option, opt): + name = opt.name.replace('_', '-') + help = opt_to_cli_help(opt) + add_option('--enable-' + name, action='store_true', help=help) + add_option('--disable-' + name, action='store_false', help=help) + +def opts_to_parser(usage): + from calibre.utils.config import OptionParser + parser = OptionParser(usage) + for opt in options.itervalues(): + add_option = partial(parser.add_option, dest=opt.name, help=opt_to_cli_help(opt), default=opt.default) + if opt.default is True or opt.default is False: + boolean_option(add_option, opt) + elif opt.choices: + name = '--' + opt.name.replace('_', '-') + add_option(name, choices=opt.choices) + else: + name = '--' + opt.name.replace('_', '-') + otype = 'string' + if isinstance(opt.default, (int, long, float)): + otype = type(opt.default).__name__ + add_option(name, type=otype) + + return parser diff --git a/src/calibre/srv/standalone.py b/src/calibre/srv/standalone.py new file mode 100644 index 0000000000..ae19612380 --- /dev/null +++ b/src/calibre/srv/standalone.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2015, Kovid Goyal ' + +import sys, os + +from calibre import as_unicode +from calibre.constants import plugins, iswindows +from calibre.srv.loop import ServerLoop +from calibre.srv.opts import opts_to_parser +from calibre.srv.http_response import create_http_handler +from calibre.srv.handler import Handler +from calibre.srv.utils import RotatingLog +from calibre.utils.config import prefs +from calibre.db.legacy import LibraryDatabase + +def daemonize(): # {{{ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as e: + raise SystemExit('fork #1 failed: %s' % as_unicode(e)) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError as e: + raise SystemExit('fork #2 failed: %s' % as_unicode(e)) + + # Redirect standard file descriptors. + try: + plugins['speedup'][0].detach(os.devnull) + except AttributeError: # people running from source without updated binaries + si = os.open(os.devnull, os.O_RDONLY) + so = os.open(os.devnull, os.O_WRONLY) + se = os.open(os.devnull, os.O_WRONLY) + os.dup2(si, sys.stdin.fileno()) + os.dup2(so, sys.stdout.fileno()) + os.dup2(se, sys.stderr.fileno()) +# }}} + +class Server(object): + + def __init__(self, libraries, opts): + self.handler = Handler(libraries, opts) + log = None + if opts.log: + log = RotatingLog(opts.log, max_size=opts.max_log_size) + self.loop = ServerLoop(create_http_handler(self.handler.dispatch), opts=opts, log=log) + self.serve_forever = self.loop.serve_forever + + +def create_option_parser(): + parser = opts_to_parser('%prog '+ _( +'''[options] [path to library folder ...] + +Start the calibre content server. The calibre content server +exposes your calibre libraries over the internet. You can specify +the path to the library folders as arguments to %prog. If you do +not specify any paths, the library last opened (if any) in the main calibre +program will be used. +''' + )) + parser.add_option( + '-l', '--library', dest='libraries', action='append', default=[], + help=_('Path to a calibre library folder. Can be specified multiple' + ' times to serve multiple libraries')) + parser.add_option( + '--log', default=None, + help=_('Path to log file for server log')) + parser.add_option( + '--url-prefix', default=None, + help=_('A prefix to prepend to all URLs. Useful if you wish to run this server behind a reverse proxy.')) + parser.add_option('--daemonize', default=False, action='store_true', + help=_('Run process in background as a daemon. No effect on Windows.')) + parser.add_option('--pidfile', default=None, + help=_('Write process PID to the specified file')) + + return parser + +def main(args=sys.argv): + opts, args = create_option_parser().parse_args(args) + libraries = args[1:] + for lib in libraries: + if not lib or not LibraryDatabase.exists_at(lib): + raise SystemExit(_('There is no calibre library at: %s') % lib) + if not libraries: + if not prefs['library_path']: + raise SystemExit(_('You must specify at least one calibre library')) + libraries = [prefs['library_path']] + server = Server(libraries, opts) + if opts.daemonize: + if not opts.log and not iswindows: + raise SystemExit('In order to daemonize you must specify a log file, you can use /dev/stdout to log to screen even as a daemon') + daemonize() + if opts.pidfile: + with lopen(opts.pidfile, 'wb') as f: + f.write(str(os.getpid())) + server.serve_forever()