mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement auto-reload on linux
This commit is contained in:
parent
18c9403df4
commit
74c50bf11a
127
src/calibre/srv/auto_reload.py
Normal file
127
src/calibre/srv/auto_reload.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
#!/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 <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import os, sys, subprocess, signal, time
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from calibre.constants import islinux
|
||||||
|
|
||||||
|
class NoAutoReload(EnvironmentError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
EXTENSIONS_TO_WATCH = frozenset('py pyj css js xml'.split())
|
||||||
|
BOUNCE_INTERVAL = 2 # seconds
|
||||||
|
|
||||||
|
if islinux:
|
||||||
|
import select
|
||||||
|
from calibre.utils.inotify import INotifyTreeWatcher
|
||||||
|
|
||||||
|
def ignore_event(path, name):
|
||||||
|
return name and name.rpartition('.')[-1] not in EXTENSIONS_TO_WATCH
|
||||||
|
|
||||||
|
class Watcher(object):
|
||||||
|
|
||||||
|
def __init__(self, root_dirs, server, log):
|
||||||
|
self.server, self.log = server, log
|
||||||
|
self.fd_map = {}
|
||||||
|
for d in frozenset(root_dirs):
|
||||||
|
w = INotifyTreeWatcher(d, ignore_event)
|
||||||
|
self.fd_map[w._inotify_fd] = w
|
||||||
|
self.last_restart_time = time.time()
|
||||||
|
fpath = os.path.abspath(__file__)
|
||||||
|
d = os.path.dirname
|
||||||
|
self.base = d(d(d(d(fpath))))
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
while True:
|
||||||
|
r = select.select(list(self.fd_map.iterkeys()), [], [])[0]
|
||||||
|
modified = set()
|
||||||
|
for fd in r:
|
||||||
|
w = self.fd_map[fd]
|
||||||
|
modified |= w()
|
||||||
|
if modified:
|
||||||
|
if time.time() - self.last_restart_time > 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('Restarting server because of changed files:', changed)
|
||||||
|
self.log('')
|
||||||
|
self.server.restart()
|
||||||
|
self.last_restart_time = time.time()
|
||||||
|
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'))
|
||||||
|
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(object):
|
||||||
|
|
||||||
|
def __init__(self, cmd, timeout=5):
|
||||||
|
self.cmd = cmd
|
||||||
|
self.p = None
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
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.p = None
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
self.clean_kill()
|
||||||
|
self.p = subprocess.Popen(self.cmd, creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0))
|
||||||
|
|
||||||
|
def auto_reload(log, dirs=frozenset(), cmd=None, add_default_dirs=True):
|
||||||
|
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')
|
||||||
|
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 Worker(cmd) as server:
|
||||||
|
w = Watcher(dirs, server, log)
|
||||||
|
try:
|
||||||
|
w.loop()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
326
src/calibre/utils/inotify.py
Normal file
326
src/calibre/utils/inotify.py
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import sys, os, errno, select
|
||||||
|
|
||||||
|
class INotifyError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoSuchDir(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BaseDirChanged(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DirTooLarge(ValueError):
|
||||||
|
|
||||||
|
def __init__(self, bdir):
|
||||||
|
ValueError.__init__(self, 'The directory {0} is too large to monitor. Try increasing the value in /proc/sys/fs/inotify/max_user_watches'.format(bdir))
|
||||||
|
|
||||||
|
_inotify = None
|
||||||
|
|
||||||
|
def load_inotify(): # {{{
|
||||||
|
''' Initialize the inotify ctypes wrapper '''
|
||||||
|
global _inotify
|
||||||
|
if _inotify is None:
|
||||||
|
if hasattr(sys, 'getwindowsversion'):
|
||||||
|
# On windows abort before loading the C library. Windows has
|
||||||
|
# multiple, incompatible C runtimes, and we have no way of knowing
|
||||||
|
# if the one chosen by ctypes is compatible with the currently
|
||||||
|
# loaded one.
|
||||||
|
raise INotifyError('INotify not available on windows')
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
raise INotifyError('INotify not available on OS X')
|
||||||
|
import ctypes
|
||||||
|
if not hasattr(ctypes, 'c_ssize_t'):
|
||||||
|
raise INotifyError('You need python >= 2.7 to use inotify')
|
||||||
|
libc = ctypes.CDLL(None, use_errno=True)
|
||||||
|
for function in ("inotify_add_watch", "inotify_init1", "inotify_rm_watch"):
|
||||||
|
if not hasattr(libc, function):
|
||||||
|
raise INotifyError('libc is too old')
|
||||||
|
# inotify_init1()
|
||||||
|
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, use_errno=True)
|
||||||
|
init1 = prototype(('inotify_init1', libc), ((1, "flags", 0),))
|
||||||
|
|
||||||
|
# inotify_add_watch()
|
||||||
|
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32, use_errno=True)
|
||||||
|
add_watch = prototype(('inotify_add_watch', libc), (
|
||||||
|
(1, "fd"), (1, "pathname"), (1, "mask")), use_errno=True)
|
||||||
|
|
||||||
|
# inotify_rm_watch()
|
||||||
|
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int, use_errno=True)
|
||||||
|
rm_watch = prototype(('inotify_rm_watch', libc), (
|
||||||
|
(1, "fd"), (1, "wd")), use_errno=True)
|
||||||
|
|
||||||
|
# read()
|
||||||
|
prototype = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, use_errno=True)
|
||||||
|
read = prototype(('read', libc), (
|
||||||
|
(1, "fd"), (1, "buf"), (1, "count")), use_errno=True)
|
||||||
|
_inotify = (init1, add_watch, rm_watch, read)
|
||||||
|
return _inotify
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class INotify(object):
|
||||||
|
|
||||||
|
# See <sys/inotify.h> for the flags defined below
|
||||||
|
|
||||||
|
# Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH.
|
||||||
|
ACCESS = 0x00000001 # File was accessed.
|
||||||
|
MODIFY = 0x00000002 # File was modified.
|
||||||
|
ATTRIB = 0x00000004 # Metadata changed.
|
||||||
|
CLOSE_WRITE = 0x00000008 # Writtable file was closed.
|
||||||
|
CLOSE_NOWRITE = 0x00000010 # Unwrittable file closed.
|
||||||
|
OPEN = 0x00000020 # File was opened.
|
||||||
|
MOVED_FROM = 0x00000040 # File was moved from X.
|
||||||
|
MOVED_TO = 0x00000080 # File was moved to Y.
|
||||||
|
CREATE = 0x00000100 # Subfile was created.
|
||||||
|
DELETE = 0x00000200 # Subfile was deleted.
|
||||||
|
DELETE_SELF = 0x00000400 # Self was deleted.
|
||||||
|
MOVE_SELF = 0x00000800 # Self was moved.
|
||||||
|
|
||||||
|
# Events sent by the kernel.
|
||||||
|
UNMOUNT = 0x00002000 # Backing fs was unmounted.
|
||||||
|
Q_OVERFLOW = 0x00004000 # Event queued overflowed.
|
||||||
|
IGNORED = 0x00008000 # File was ignored.
|
||||||
|
|
||||||
|
# Helper events.
|
||||||
|
CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE) # Close.
|
||||||
|
MOVE = (MOVED_FROM | MOVED_TO) # Moves.
|
||||||
|
|
||||||
|
# Special flags.
|
||||||
|
ONLYDIR = 0x01000000 # Only watch the path if it is a directory.
|
||||||
|
DONT_FOLLOW = 0x02000000 # Do not follow a sym link.
|
||||||
|
EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects.
|
||||||
|
MASK_ADD = 0x20000000 # Add to the mask of an already existing watch.
|
||||||
|
ISDIR = 0x40000000 # Event occurred against dir.
|
||||||
|
ONESHOT = 0x80000000 # Only send event once.
|
||||||
|
|
||||||
|
# All events which a program can wait on.
|
||||||
|
ALL_EVENTS = (ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE |
|
||||||
|
OPEN | MOVED_FROM | MOVED_TO | CREATE | DELETE |
|
||||||
|
DELETE_SELF | MOVE_SELF)
|
||||||
|
|
||||||
|
# See <bits/inotify.h>
|
||||||
|
CLOEXEC = 0x80000
|
||||||
|
NONBLOCK = 0x800
|
||||||
|
|
||||||
|
def __init__(self, cloexec=True, nonblock=True):
|
||||||
|
import ctypes, struct
|
||||||
|
self._init1, self._add_watch, self._rm_watch, self._read = load_inotify()
|
||||||
|
flags = 0
|
||||||
|
if cloexec:
|
||||||
|
flags |= self.CLOEXEC
|
||||||
|
if nonblock:
|
||||||
|
flags |= self.NONBLOCK
|
||||||
|
self._inotify_fd = self._init1(flags)
|
||||||
|
if self._inotify_fd == -1:
|
||||||
|
raise INotifyError(os.strerror(ctypes.get_errno()))
|
||||||
|
|
||||||
|
self._buf = ctypes.create_string_buffer(5120)
|
||||||
|
self.fenc = sys.getfilesystemencoding() or 'utf-8'
|
||||||
|
self.hdr = struct.Struct(b'iIII')
|
||||||
|
if self.fenc == 'ascii':
|
||||||
|
self.fenc = 'utf-8'
|
||||||
|
# We keep a reference to os to prevent it from being deleted
|
||||||
|
# during interpreter shutdown, which would lead to errors in the
|
||||||
|
# __del__ method
|
||||||
|
self.os = os
|
||||||
|
|
||||||
|
def handle_error(self):
|
||||||
|
import ctypes
|
||||||
|
eno = ctypes.get_errno()
|
||||||
|
extra = ''
|
||||||
|
if eno == errno.ENOSPC:
|
||||||
|
extra = 'You may need to increase the inotify limits on your system, via /proc/sys/inotify/max_user_*'
|
||||||
|
raise OSError(eno, self.os.strerror(eno) + extra)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
# This method can be called during interpreter shutdown, which means we
|
||||||
|
# must do the absolute minimum here. Note that there could be running
|
||||||
|
# daemon threads that are trying to call other methods on this object.
|
||||||
|
try:
|
||||||
|
self.os.close(self._inotify_fd)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if hasattr(self, '_inotify_fd'):
|
||||||
|
self.os.close(self._inotify_fd)
|
||||||
|
del self.os
|
||||||
|
del self._add_watch
|
||||||
|
del self._rm_watch
|
||||||
|
del self._inotify_fd
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def read(self, get_name=True):
|
||||||
|
import ctypes
|
||||||
|
buf = []
|
||||||
|
while True:
|
||||||
|
num = self._read(self._inotify_fd, self._buf, len(self._buf))
|
||||||
|
if num == 0:
|
||||||
|
break
|
||||||
|
if num < 0:
|
||||||
|
en = ctypes.get_errno()
|
||||||
|
if en == errno.EAGAIN:
|
||||||
|
break # No more data
|
||||||
|
if en == errno.EINTR:
|
||||||
|
continue # Interrupted, try again
|
||||||
|
raise OSError(en, self.os.strerror(en))
|
||||||
|
buf.append(self._buf.raw[:num])
|
||||||
|
raw = b''.join(buf)
|
||||||
|
pos = 0
|
||||||
|
lraw = len(raw)
|
||||||
|
while lraw - pos >= self.hdr.size:
|
||||||
|
wd, mask, cookie, name_len = self.hdr.unpack_from(raw, pos)
|
||||||
|
pos += self.hdr.size
|
||||||
|
name = None
|
||||||
|
if get_name:
|
||||||
|
name = raw[pos:pos+name_len].rstrip(b'\0').decode(self.fenc)
|
||||||
|
pos += name_len
|
||||||
|
self.process_event(wd, mask, cookie, name)
|
||||||
|
|
||||||
|
def process_event(self, *args):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
'Return True iff there are events waiting to be read. Blocks if timeout is None. Polls if timeout is 0.'
|
||||||
|
return len((select.select([self._inotify_fd], [], []) if timeout is None else select.select([self._inotify_fd], [], [], timeout))[0]) > 0
|
||||||
|
|
||||||
|
def realpath(path):
|
||||||
|
return os.path.abspath(os.path.realpath(path))
|
||||||
|
|
||||||
|
class INotifyTreeWatcher(INotify):
|
||||||
|
|
||||||
|
is_dummy = False
|
||||||
|
|
||||||
|
def __init__(self, basedir, ignore_event=None):
|
||||||
|
super(INotifyTreeWatcher, self).__init__()
|
||||||
|
self.basedir = realpath(basedir)
|
||||||
|
self.watch_tree()
|
||||||
|
self.modified = set()
|
||||||
|
self.ignore_event = (lambda path, name: False) if ignore_event is None else ignore_event
|
||||||
|
|
||||||
|
def watch_tree(self):
|
||||||
|
self.watched_dirs = {}
|
||||||
|
self.watched_rmap = {}
|
||||||
|
try:
|
||||||
|
self.add_watches(self.basedir)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ENOSPC:
|
||||||
|
raise DirTooLarge(self.basedir)
|
||||||
|
|
||||||
|
def add_watches(self, base, top_level=True):
|
||||||
|
''' Add watches for this directory and all its descendant directories,
|
||||||
|
recursively. '''
|
||||||
|
base = realpath(base)
|
||||||
|
# There may exist a link which leads to an endless
|
||||||
|
# add_watches loop or to maximum recursion depth exceeded
|
||||||
|
if not top_level and base in self.watched_dirs:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
is_dir = self.add_watch(base)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
# The entry could have been deleted between listdir() and
|
||||||
|
# add_watch().
|
||||||
|
if top_level:
|
||||||
|
raise NoSuchDir('The dir {0} does not exist'.format(base))
|
||||||
|
return
|
||||||
|
if e.errno == errno.EACCES:
|
||||||
|
# We silently ignore entries for which we dont have permission,
|
||||||
|
# unless they are the top level dir
|
||||||
|
if top_level:
|
||||||
|
raise NoSuchDir('You do not have permission to monitor {0}'.format(base))
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
if is_dir:
|
||||||
|
try:
|
||||||
|
files = os.listdir(base)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno in (errno.ENOTDIR, errno.ENOENT):
|
||||||
|
# The dir was deleted/replaced between the add_watch()
|
||||||
|
# and listdir()
|
||||||
|
if top_level:
|
||||||
|
raise NoSuchDir('The dir {0} does not exist'.format(base))
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
for x in files:
|
||||||
|
self.add_watches(os.path.join(base, x), top_level=False)
|
||||||
|
elif top_level:
|
||||||
|
# The top level dir is a file, not good.
|
||||||
|
raise NoSuchDir('The dir {0} does not exist'.format(base))
|
||||||
|
|
||||||
|
def add_watch(self, path):
|
||||||
|
import ctypes
|
||||||
|
bpath = path if isinstance(path, bytes) else path.encode(self.fenc)
|
||||||
|
wd = self._add_watch(self._inotify_fd, ctypes.c_char_p(bpath),
|
||||||
|
# Ignore symlinks and watch only directories
|
||||||
|
self.DONT_FOLLOW | self.ONLYDIR |
|
||||||
|
|
||||||
|
self.MODIFY | self.CREATE | self.DELETE |
|
||||||
|
self.MOVE_SELF | self.MOVED_FROM | self.MOVED_TO |
|
||||||
|
self.ATTRIB | self.DELETE_SELF)
|
||||||
|
if wd == -1:
|
||||||
|
eno = ctypes.get_errno()
|
||||||
|
if eno == errno.ENOTDIR:
|
||||||
|
return False
|
||||||
|
raise OSError(eno, 'Failed to add watch for: {0}: {1}'.format(path, self.os.strerror(eno)))
|
||||||
|
self.watched_dirs[path] = wd
|
||||||
|
self.watched_rmap[wd] = path
|
||||||
|
return True
|
||||||
|
|
||||||
|
def process_event(self, wd, mask, cookie, name):
|
||||||
|
if wd == -1 and (mask & self.Q_OVERFLOW):
|
||||||
|
# We missed some INOTIFY events, so we dont
|
||||||
|
# know the state of any tracked dirs.
|
||||||
|
self.watch_tree()
|
||||||
|
self.modified.add(None)
|
||||||
|
return
|
||||||
|
path = self.watched_rmap.get(wd, None)
|
||||||
|
if path is not None:
|
||||||
|
if not self.ignore_event(path, name):
|
||||||
|
self.modified.add(os.path.join(path, name or ''))
|
||||||
|
if mask & self.CREATE:
|
||||||
|
# A new sub-directory might have been created, monitor it.
|
||||||
|
try:
|
||||||
|
self.add_watch(os.path.join(path, name))
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
# Deleted before add_watch()
|
||||||
|
pass
|
||||||
|
elif e.errno == errno.ENOSPC:
|
||||||
|
raise DirTooLarge(self.basedir)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
if (mask & self.DELETE_SELF or mask & self.MOVE_SELF) and path == self.basedir:
|
||||||
|
raise BaseDirChanged('The directory %s was moved/deleted' % path)
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self.read()
|
||||||
|
ret = self.modified
|
||||||
|
self.modified = set()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
w = INotifyTreeWatcher(sys.argv[-1])
|
||||||
|
w()
|
||||||
|
print ('Monitoring', sys.argv[-1], 'press Ctrl-C to stop')
|
||||||
|
try:
|
||||||
|
while w.wait():
|
||||||
|
modified = w()
|
||||||
|
for path in modified:
|
||||||
|
print (path or sys.argv[-1], 'changed')
|
||||||
|
raise SystemExit('inotify flaked out')
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
Loading…
x
Reference in New Issue
Block a user