mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Server loop: Add support for simple plugins whose lifecycle is tied to the loop's life cycle
This commit is contained in:
parent
f2ad28b964
commit
b95bcfbbe3
@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import ssl, socket, select, os, traceback
|
import ssl, socket, select, os, traceback, time
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from Queue import Empty, Full
|
from Queue import Empty, Full
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@ -14,7 +14,7 @@ from functools import partial
|
|||||||
from calibre import as_unicode
|
from calibre import as_unicode
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
from calibre.srv.errors import JobQueueFull
|
from calibre.srv.errors import JobQueueFull
|
||||||
from calibre.srv.pool import ThreadPool
|
from calibre.srv.pool import ThreadPool, PluginPool
|
||||||
from calibre.srv.opts import Options
|
from calibre.srv.opts import Options
|
||||||
from calibre.srv.utils import (
|
from calibre.srv.utils import (
|
||||||
socket_errors_socket_closed, socket_errors_nonblocking, HandleInterrupt,
|
socket_errors_socket_closed, socket_errors_nonblocking, HandleInterrupt,
|
||||||
@ -275,6 +275,7 @@ class ServerLoop(object):
|
|||||||
self,
|
self,
|
||||||
handler,
|
handler,
|
||||||
opts=None,
|
opts=None,
|
||||||
|
plugins=(),
|
||||||
# A calibre logging object. If None, a default log that logs to
|
# A calibre logging object. If None, a default log that logs to
|
||||||
# stdout is used
|
# stdout is used
|
||||||
log=None
|
log=None
|
||||||
@ -307,6 +308,7 @@ class ServerLoop(object):
|
|||||||
|
|
||||||
self.create_control_connection()
|
self.create_control_connection()
|
||||||
self.pool = ThreadPool(self.log, self.job_completed, count=self.opts.worker_count)
|
self.pool = ThreadPool(self.log, self.job_completed, count=self.opts.worker_count)
|
||||||
|
self.plugin_pool = PluginPool(self, plugins)
|
||||||
|
|
||||||
def create_control_connection(self):
|
def create_control_connection(self):
|
||||||
self.control_in, self.control_out = create_sock_pair()
|
self.control_in, self.control_out = create_sock_pair()
|
||||||
@ -369,6 +371,7 @@ class ServerLoop(object):
|
|||||||
self.tdir = tdir
|
self.tdir = tdir
|
||||||
self.ready = True
|
self.ready = True
|
||||||
self.log('calibre server listening on', ba)
|
self.log('calibre server listening on', ba)
|
||||||
|
self.plugin_pool.start()
|
||||||
|
|
||||||
while self.ready:
|
while self.ready:
|
||||||
try:
|
try:
|
||||||
@ -560,7 +563,9 @@ class ServerLoop(object):
|
|||||||
pass
|
pass
|
||||||
for s, conn in tuple(self.connection_map.iteritems()):
|
for s, conn in tuple(self.connection_map.iteritems()):
|
||||||
self.close(s, conn)
|
self.close(s, conn)
|
||||||
self.pool.stop(self.opts.shutdown_timeout)
|
end = time.time() + self.opts.shutdown_timeout
|
||||||
|
for pool in (self.plugin_pool, self.pool):
|
||||||
|
pool.stop(max(0, end - time.time()))
|
||||||
|
|
||||||
class EchoLine(Connection): # {{{
|
class EchoLine(Connection): # {{{
|
||||||
|
|
||||||
|
@ -81,3 +81,37 @@ class ThreadPool(object):
|
|||||||
@property
|
@property
|
||||||
def idle(self):
|
def idle(self):
|
||||||
return sum(int(not w.working) for w in self.workers)
|
return sum(int(not w.working) for w in self.workers)
|
||||||
|
|
||||||
|
class PluginPool(object):
|
||||||
|
|
||||||
|
def __init__(self, loop, plugins):
|
||||||
|
self.workers = []
|
||||||
|
self.loop = loop
|
||||||
|
for plugin in plugins:
|
||||||
|
w = Thread(target=self.run_plugin, args=(plugin,), name=plugin.__class__.__name__)
|
||||||
|
w.daemon = True
|
||||||
|
w.plugin = plugin
|
||||||
|
self.workers.append(w)
|
||||||
|
|
||||||
|
def run_plugin(self, plugin):
|
||||||
|
try:
|
||||||
|
plugin.start(self.loop)
|
||||||
|
except Exception:
|
||||||
|
self.loop.log.exception('Failed to start plugin: %s', plugin.__class__.__name__)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
for w in self.workers:
|
||||||
|
w.start()
|
||||||
|
|
||||||
|
def stop(self, shutdown_timeout):
|
||||||
|
end = time.time() + shutdown_timeout
|
||||||
|
for w in self.workers:
|
||||||
|
if w.is_alive():
|
||||||
|
w.plugin.stop()
|
||||||
|
for w in self.workers:
|
||||||
|
left = end - time.time()
|
||||||
|
if left > 0:
|
||||||
|
w.join(left)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
self.workers = [w for w in self.workers if w.is_alive()]
|
||||||
|
@ -23,7 +23,7 @@ class TestServer(Thread):
|
|||||||
|
|
||||||
daemon = True
|
daemon = True
|
||||||
|
|
||||||
def __init__(self, handler, **kwargs):
|
def __init__(self, handler, plugins=(), **kwargs):
|
||||||
Thread.__init__(self, name='ServerMain')
|
Thread.__init__(self, name='ServerMain')
|
||||||
from calibre.srv.opts import Options
|
from calibre.srv.opts import Options
|
||||||
from calibre.srv.loop import ServerLoop
|
from calibre.srv.loop import ServerLoop
|
||||||
@ -34,6 +34,7 @@ class TestServer(Thread):
|
|||||||
self.loop = ServerLoop(
|
self.loop = ServerLoop(
|
||||||
create_http_handler(handler),
|
create_http_handler(handler),
|
||||||
opts=Options(**kwargs),
|
opts=Options(**kwargs),
|
||||||
|
plugins=plugins,
|
||||||
log=ServerLog(level=ServerLog.WARN),
|
log=ServerLog(level=ServerLog.WARN),
|
||||||
)
|
)
|
||||||
self.log = self.loop.log
|
self.log = self.loop.log
|
||||||
|
@ -9,6 +9,7 @@ __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import httplib, ssl, os, socket, time
|
import httplib, ssl, os, socket, time
|
||||||
from unittest import skipIf
|
from unittest import skipIf
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
from threading import Event
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.utils.certgen import create_server_cert
|
from calibre.utils.certgen import create_server_cert
|
||||||
@ -50,6 +51,28 @@ class LoopTest(BaseTest):
|
|||||||
self.assertLessEqual(log_size(), 100)
|
self.assertLessEqual(log_size(), 100)
|
||||||
self.ae(history(), {1,2,3,4,5})
|
self.ae(history(), {1,2,3,4,5})
|
||||||
|
|
||||||
|
def test_plugins(self):
|
||||||
|
'Test plugin semantics'
|
||||||
|
class Plugin(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.running = Event()
|
||||||
|
self.event = Event()
|
||||||
|
self.port = None
|
||||||
|
def start(self, loop):
|
||||||
|
self.running.set()
|
||||||
|
self.port = loop.bound_address[1]
|
||||||
|
self.event.wait()
|
||||||
|
self.running.clear()
|
||||||
|
def stop(self):
|
||||||
|
self.event.set()
|
||||||
|
|
||||||
|
plugin = Plugin()
|
||||||
|
with TestServer(lambda data:'xxx', plugins=(plugin,)) as server:
|
||||||
|
self.assertTrue(plugin.running.wait(0.2))
|
||||||
|
self.ae(plugin.port, server.address[1])
|
||||||
|
self.assertTrue(plugin.event.wait(0.2))
|
||||||
|
self.assertFalse(plugin.running.is_set())
|
||||||
|
|
||||||
def test_workers(self):
|
def test_workers(self):
|
||||||
' Test worker semantics '
|
' Test worker semantics '
|
||||||
with TestServer(lambda data:(data.path[0] + data.read()), worker_count=3) as server:
|
with TestServer(lambda data:(data.path[0] + data.read()), worker_count=3) as server:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user