%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/srv/ |
| Current File : //usr/lib/calibre/calibre/srv/auto_reload.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import errno
import os
import signal
import socket
import ssl
import subprocess
import sys
import time
from threading import Lock, Thread
from calibre.constants import islinux, ismacos, iswindows
from calibre.srv.http_response import create_http_handler
from calibre.srv.loop import ServerLoop
from calibre.srv.opts import Options
from calibre.srv.standalone import create_option_parser
from calibre.srv.utils import create_sock_pair
from calibre.srv.web_socket import DummyHandler
from calibre.utils.monotonic import monotonic
from polyglot.builtins import error_message, itervalues, native_string_type
from polyglot.queue import Empty, Queue
MAX_RETRIES = 10
class NoAutoReload(EnvironmentError):
pass
# Filesystem watcher {{{
class WatcherBase:
EXTENSIONS_TO_WATCH = frozenset('py pyj svg'.split())
BOUNCE_INTERVAL = 2 # seconds
def __init__(self, worker, log):
self.worker, self.log = worker, log
fpath = os.path.abspath(__file__)
d = os.path.dirname
self.base = d(d(d(d(fpath))))
self.last_restart_time = monotonic()
def handle_modified(self, modified):
if modified:
if monotonic() - self.last_restart_time > self.BOUNCE_INTERVAL:
modified = {os.path.relpath(x, self.base) if x.startswith(self.base) else x for x in modified if x}
changed = os.pathsep.join(sorted(modified))
self.log('')
self.log.warn('Restarting server because of changed files:', changed)
self.log('')
self.worker.restart()
self.last_restart_time = monotonic()
def force_restart(self):
self.worker.restart(forced=True)
self.last_restart_time = monotonic()
def file_is_watched(self, fname):
return fname and fname.rpartition('.')[-1] in self.EXTENSIONS_TO_WATCH
if islinux:
import select
from calibre.utils.inotify import INotifyTreeWatcher
class Watcher(WatcherBase):
def __init__(self, root_dirs, worker, log):
WatcherBase.__init__(self, worker, log)
self.client_sock, self.srv_sock = create_sock_pair()
self.fd_map = {}
for d in frozenset(root_dirs):
w = INotifyTreeWatcher(d, self.ignore_event)
self.fd_map[w._inotify_fd] = w
def loop(self):
while True:
r = select.select([self.srv_sock] + list(self.fd_map), [], [])[0]
modified = set()
for fd in r:
if fd is self.srv_sock:
self.srv_sock.recv(1000)
self.force_restart()
continue
w = self.fd_map[fd]
modified |= w()
self.handle_modified(modified)
def ignore_event(self, path, name):
return not self.file_is_watched(name)
def wakeup(self):
self.client_sock.sendall(b'w')
elif iswindows:
from calibre.srv.utils import HandleInterrupt
from calibre_extensions import winutil
class TreeWatcher(Thread):
def __init__(self, path_to_watch, modified_queue):
Thread.__init__(self, name='TreeWatcher', daemon=True)
self.modified_queue = modified_queue
self.path_to_watch = path_to_watch
def run(self):
dir_handle = winutil.create_file(
self.path_to_watch,
winutil.FILE_LIST_DIRECTORY,
winutil.FILE_SHARE_READ,
winutil.OPEN_EXISTING,
winutil.FILE_FLAG_BACKUP_SEMANTICS,
)
try:
buffer = b'0' * 8192
while True:
try:
results = winutil.read_directory_changes(
dir_handle, buffer,
True, # Watch sub-directories as well
winutil.FILE_NOTIFY_CHANGE_FILE_NAME |
winutil.FILE_NOTIFY_CHANGE_DIR_NAME |
winutil.FILE_NOTIFY_CHANGE_ATTRIBUTES |
winutil.FILE_NOTIFY_CHANGE_SIZE |
winutil.FILE_NOTIFY_CHANGE_LAST_WRITE |
winutil.FILE_NOTIFY_CHANGE_SECURITY,
)
for action, filename in results:
if self.file_is_watched(filename):
self.modified_queue.put(os.path.join(self.path_to_watch, filename))
except OverflowError:
pass # the buffer overflowed, there are unknown changes
except Exception:
import traceback
traceback.print_exc()
class Watcher(WatcherBase):
def __init__(self, root_dirs, worker, log):
WatcherBase.__init__(self, worker, log)
self.watchers = []
self.modified_queue = Queue()
for d in frozenset(root_dirs):
self.watchers.append(TreeWatcher(d, self.modified_queue))
def wakeup(self):
self.modified_queue.put(True)
def loop(self):
for w in self.watchers:
w.start()
with HandleInterrupt(lambda : self.modified_queue.put(None)):
while True:
path = self.modified_queue.get()
if path is None:
break
if path is True:
self.force_restart()
else:
self.handle_modified({path})
elif ismacos:
from fsevents import Observer, Stream
class Watcher(WatcherBase):
def __init__(self, root_dirs, worker, log):
WatcherBase.__init__(self, worker, log)
self.stream = Stream(self.notify, *(x.encode('utf-8') for x in root_dirs), file_events=True)
self.wait_queue = Queue()
def wakeup(self):
self.wait_queue.put(True)
def loop(self):
observer = Observer()
observer.schedule(self.stream)
observer.daemon = True
observer.start()
try:
while True:
try:
# Cannot use blocking get() as it is not interrupted by
# Ctrl-C
if self.wait_queue.get(10000) is True:
self.force_restart()
except Empty:
pass
finally:
observer.unschedule(self.stream)
observer.stop()
def notify(self, ev):
name = ev.name
if isinstance(name, bytes):
name = name.decode('utf-8')
if self.file_is_watched(name):
self.handle_modified({name})
else:
Watcher = None
def find_dirs_to_watch(fpath, dirs, add_default_dirs):
dirs = {os.path.abspath(x) for x in dirs}
def add(x):
if os.path.isdir(x):
dirs.add(x)
if add_default_dirs:
d = os.path.dirname
srv = d(fpath)
add(srv)
base = d(d(d(srv)))
add(os.path.join(base, 'resources', 'server'))
add(os.path.join(base, 'src', 'calibre', 'db'))
add(os.path.join(base, 'src', 'pyj'))
add(os.path.join(base, 'imgsrc', 'srv'))
return dirs
# }}}
def join_process(p, timeout=5):
t = Thread(target=p.wait, name='JoinProcess')
t.daemon = True
t.start()
t.join(timeout)
return p.poll()
class Worker:
def __init__(self, cmd, log, server, timeout=5):
self.cmd = cmd
self.log = log
self.server = server
self.p = None
self.wakeup = None
self.timeout = timeout
cmd = self.cmd
if 'calibre-debug' in cmd[0].lower():
try:
idx = cmd.index('--')
except ValueError:
cmd = ['srv']
else:
cmd = ['srv'] + cmd[idx+1:]
opts = create_option_parser().parse_args(cmd)[0]
self.port = opts.port
self.uses_ssl = bool(opts.ssl_certfile and opts.ssl_keyfile)
self.connection_timeout = opts.timeout
self.retry_count = 0
t = Thread(name='PingThread', target=self.ping_thread)
t.daemon = True
t.start()
def ping_thread(self):
while True:
self.server.ping()
time.sleep(30)
def __enter__(self):
self.restart()
return self
def __exit__(self, *args):
if self.p and self.p.poll() is None:
# SIGINT will already have been sent to the child process
self.clean_kill(send_signal=False)
def clean_kill(self, send_signal=True):
if self.p is not None:
if send_signal:
self.p.send_signal(getattr(signal, 'CTRL_BREAK_EVENT', signal.SIGINT))
if join_process(self.p) is None:
self.p.kill()
self.p.wait()
self.log('Killed server process %d with return code: %d' % (self.p.pid, self.p.returncode))
self.p = None
def restart(self, forced=False):
from calibre.utils.rapydscript import CompileFailure, compile_srv
self.clean_kill()
if forced:
self.retry_count += 1
else:
self.retry_count = 0
try:
compile_srv()
except OSError as e:
# Happens if the editor deletes and replaces a file being edited
if e.errno != errno.ENOENT or not getattr(e, 'filename', False):
raise
st = monotonic()
while not os.path.exists(e.filename) and monotonic() - st < 3:
time.sleep(0.01)
compile_srv()
except CompileFailure as e:
self.log.error(error_message(e))
time.sleep(0.1 * self.retry_count)
if self.retry_count < MAX_RETRIES and self.wakeup is not None:
self.wakeup() # Force a restart
return
self.retry_count = 0
self.p = subprocess.Popen(self.cmd, creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0))
self.wait_for_listen()
self.server.notify_reload()
def wait_for_listen(self):
st = monotonic()
while monotonic() - st < 5:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
try:
if self.uses_ssl:
s = ssl.wrap_socket(s)
s.connect(('localhost', self.port))
return
except OSError:
time.sleep(0.01)
finally:
s.close()
self.log.error('Restarted server did not start listening on:', self.port)
# WebSocket reload notifier {{{
class ReloadHandler(DummyHandler):
def __init__(self, *args, **kw):
DummyHandler.__init__(self, *args, **kw)
self.connections = {}
self.conn_lock = Lock()
def handle_websocket_upgrade(self, connection_id, connection_ref, inheaders):
with self.conn_lock:
self.connections[connection_id] = connection_ref
def handle_websocket_close(self, connection_id):
with self.conn_lock:
self.connections.pop(connection_id, None)
def notify_reload(self):
with self.conn_lock:
for connref in itervalues(self.connections):
conn = connref()
if conn is not None and conn.ready:
conn.send_websocket_message('reload')
def ping(self):
with self.conn_lock:
for connref in itervalues(self.connections):
conn = connref()
if conn is not None and conn.ready:
conn.send_websocket_message('ping')
class ReloadServer(Thread):
daemon = True
def __init__(self, listen_on):
Thread.__init__(self, name='ReloadServer')
self.reload_handler = ReloadHandler()
self.loop = ServerLoop(
create_http_handler(websocket_handler=self.reload_handler),
opts=Options(shutdown_timeout=0.1, listen_on=(listen_on or '127.0.0.1'), port=0))
self.loop.LISTENING_MSG = None
self.notify_reload = self.reload_handler.notify_reload
self.ping = self.reload_handler.ping
self.start()
def run(self):
try:
self.loop.serve_forever()
except KeyboardInterrupt:
pass
def __enter__(self):
while not self.loop.ready and self.is_alive():
time.sleep(0.01)
self.address = self.loop.bound_address[:2]
os.environ['CALIBRE_AUTORELOAD_PORT'] = native_string_type(self.address[1])
return self
def __exit__(self, *args):
self.loop.stop()
self.join(self.loop.opts.shutdown_timeout)
# }}}
def auto_reload(log, dirs=frozenset(), cmd=None, add_default_dirs=True, listen_on=None):
if Watcher is None:
raise NoAutoReload('Auto-reload is not supported on this operating system')
fpath = os.path.abspath(__file__)
if not os.access(fpath, os.R_OK):
raise NoAutoReload('Auto-reload can only be used when running from source')
if cmd is None:
cmd = list(sys.argv)
cmd.remove('--auto-reload')
if os.path.basename(cmd[0]) == 'run-local':
cmd.insert(1, 'calibre-server')
dirs = find_dirs_to_watch(fpath, dirs, add_default_dirs)
log('Auto-restarting server on changes press Ctrl-C to quit')
log('Watching %d directory trees for changes' % len(dirs))
with ReloadServer(listen_on) as server, Worker(cmd, log, server) as worker:
w = Watcher(dirs, worker, log)
worker.wakeup = w.wakeup
try:
w.loop()
except KeyboardInterrupt:
pass