server.py
851 lines
| 26.8 KiB
| text/x-python
|
PythonLexer
|
r6239 | # server.py - inotify status server | ||
# | ||||
# Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com> | ||||
# Copyright 2007, 2008 Brendan Cully <brendan@kublai.com> | ||||
# | ||||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
# GNU General Public License version 2, incorporated herein by reference. | ||||
|
r6239 | |||
|
r6961 | from mercurial.i18n import _ | ||
|
r9514 | from mercurial import cmdutil, osutil, util | ||
|
r6239 | import common | ||
|
r6997 | import errno, os, select, socket, stat, struct, sys, tempfile, time | ||
|
r6239 | |||
try: | ||||
|
r6994 | import linux as inotify | ||
from linux import watcher | ||||
|
r6239 | except ImportError: | ||
raise | ||||
class AlreadyStartedException(Exception): pass | ||||
def join(a, b): | ||||
if a: | ||||
if a[-1] == '/': | ||||
return a + b | ||||
return a + '/' + b | ||||
return b | ||||
|
r8787 | def split(path): | ||
c = path.rfind('/') | ||||
if c == -1: | ||||
return '', path | ||||
return path[:c], path[c+1:] | ||||
|
r6239 | walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG) | ||
|
r9350 | def walkrepodirs(dirstate, absroot): | ||
|
r6239 | '''Iterate over all subdirectories of this repo. | ||
Exclude the .hg directory, any nested repos, and ignored dirs.''' | ||||
def walkit(dirname, top): | ||||
|
r9350 | fullpath = join(absroot, dirname) | ||
|
r6239 | try: | ||
|
r8321 | for name, kind in osutil.listdir(fullpath): | ||
|
r6239 | if kind == stat.S_IFDIR: | ||
if name == '.hg': | ||||
|
r8323 | if not top: | ||
return | ||||
|
r6239 | else: | ||
d = join(dirname, name) | ||||
|
r9350 | if dirstate._ignore(d): | ||
|
r6239 | continue | ||
|
r8322 | for subdir in walkit(d, False): | ||
yield subdir | ||||
|
r6239 | except OSError, err: | ||
if err.errno not in walk_ignored_errors: | ||||
raise | ||||
|
r8324 | yield fullpath | ||
|
r8322 | |||
return walkit('', True) | ||||
|
r6239 | |||
|
r9350 | def walk(dirstate, absroot, root): | ||
|
r6239 | '''Like os.walk, but only yields regular files.''' | ||
# This function is critical to performance during startup. | ||||
def walkit(root, reporoot): | ||||
files, dirs = [], [] | ||||
try: | ||||
|
r9350 | fullpath = join(absroot, root) | ||
|
r6239 | for name, kind in osutil.listdir(fullpath): | ||
if kind == stat.S_IFDIR: | ||||
if name == '.hg': | ||||
|
r8325 | if not reporoot: | ||
|
r8323 | return | ||
|
r8325 | else: | ||
dirs.append(name) | ||||
|
r8381 | path = join(root, name) | ||
|
r9350 | if dirstate._ignore(path): | ||
|
r8381 | continue | ||
for result in walkit(path, False): | ||||
yield result | ||||
|
r6239 | elif kind in (stat.S_IFREG, stat.S_IFLNK): | ||
|
r8334 | files.append(name) | ||
|
r8324 | yield fullpath, dirs, files | ||
|
r6239 | |||
except OSError, err: | ||||
|
r9116 | if err.errno == errno.ENOTDIR: | ||
# fullpath was a directory, but has since been replaced | ||||
# by a file. | ||||
yield fullpath, dirs, files | ||||
elif err.errno not in walk_ignored_errors: | ||||
|
r6239 | raise | ||
|
r8320 | |||
return walkit(root, root == '') | ||||
|
r6239 | |||
|
r9350 | def _explain_watch_limit(ui, dirstate, rootabs): | ||
|
r6239 | path = '/proc/sys/fs/inotify/max_user_watches' | ||
try: | ||||
limit = int(file(path).read()) | ||||
except IOError, err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
raise util.Abort(_('this system does not seem to ' | ||||
'support inotify')) | ||||
ui.warn(_('*** the current per-user limit on the number ' | ||||
'of inotify watches is %s\n') % limit) | ||||
ui.warn(_('*** this limit is too low to watch every ' | ||||
'directory in this repository\n')) | ||||
ui.warn(_('*** counting directories: ')) | ||||
|
r9350 | ndirs = len(list(walkrepodirs(dirstate, rootabs))) | ||
|
r6239 | ui.warn(_('found %d\n') % ndirs) | ||
newlimit = min(limit, 1024) | ||||
while newlimit < ((limit + ndirs) * 1.1): | ||||
newlimit *= 2 | ||||
ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') % | ||||
(limit, newlimit)) | ||||
ui.warn(_('*** echo %d > %s\n') % (newlimit, path)) | ||||
raise util.Abort(_('cannot watch %s until inotify watch limit is raised') | ||||
|
r9350 | % rootabs) | ||
|
r6239 | |||
|
r8610 | class pollable(object): | ||
""" | ||||
Interface to support polling. | ||||
The file descriptor returned by fileno() is registered to a polling | ||||
object. | ||||
Usage: | ||||
Every tick, check if an event has happened since the last tick: | ||||
* If yes, call handle_events | ||||
* If no, call handle_timeout | ||||
""" | ||||
|
r6239 | poll_events = select.POLLIN | ||
|
r8792 | instances = {} | ||
poll = select.poll() | ||||
|
r8610 | def fileno(self): | ||
raise NotImplementedError | ||||
def handle_events(self, events): | ||||
raise NotImplementedError | ||||
def handle_timeout(self): | ||||
raise NotImplementedError | ||||
def shutdown(self): | ||||
raise NotImplementedError | ||||
|
r8792 | def register(self, timeout): | ||
fd = self.fileno() | ||||
pollable.poll.register(fd, pollable.poll_events) | ||||
pollable.instances[fd] = self | ||||
self.registered = True | ||||
self.timeout = timeout | ||||
def unregister(self): | ||||
pollable.poll.unregister(self) | ||||
self.registered = False | ||||
|
r8793 | @classmethod | ||
def run(cls): | ||||
while True: | ||||
timeout = None | ||||
timeobj = None | ||||
for obj in cls.instances.itervalues(): | ||||
if obj.timeout is not None and (timeout is None or obj.timeout < timeout): | ||||
timeout, timeobj = obj.timeout, obj | ||||
try: | ||||
events = cls.poll.poll(timeout) | ||||
except select.error, err: | ||||
if err[0] == errno.EINTR: | ||||
continue | ||||
raise | ||||
if events: | ||||
by_fd = {} | ||||
for fd, event in events: | ||||
by_fd.setdefault(fd, []).append(event) | ||||
for fd, events in by_fd.iteritems(): | ||||
cls.instances[fd].handle_pollevents(events) | ||||
elif timeobj: | ||||
timeobj.handle_timeout() | ||||
|
r8791 | def eventaction(code): | ||
""" | ||||
Decorator to help handle events in repowatcher | ||||
""" | ||||
def decorator(f): | ||||
def wrapper(self, wpath): | ||||
if code == 'm' and wpath in self.lastevent and \ | ||||
self.lastevent[wpath] in 'cm': | ||||
return | ||||
self.lastevent[wpath] = code | ||||
self.timeout = 250 | ||||
f(self, wpath) | ||||
wrapper.func_name = f.func_name | ||||
return wrapper | ||||
return decorator | ||||
|
r9115 | class directory(object): | ||
""" | ||||
Representing a directory | ||||
* path is the relative path from repo root to this directory | ||||
* files is a dict listing the files in this directory | ||||
- keys are file names | ||||
- values are file status | ||||
* dirs is a dict listing the subdirectories | ||||
- key are subdirectories names | ||||
- values are directory objects | ||||
""" | ||||
def __init__(self, relpath=''): | ||||
self.path = relpath | ||||
self.files = {} | ||||
self.dirs = {} | ||||
def dir(self, relpath): | ||||
""" | ||||
Returns the directory contained at the relative path relpath. | ||||
Creates the intermediate directories if necessary. | ||||
""" | ||||
if not relpath: | ||||
return self | ||||
l = relpath.split('/') | ||||
ret = self | ||||
while l: | ||||
next = l.pop(0) | ||||
try: | ||||
ret = ret.dirs[next] | ||||
except KeyError: | ||||
d = directory(join(ret.path, next)) | ||||
ret.dirs[next] = d | ||||
ret = d | ||||
return ret | ||||
def walk(self, states): | ||||
""" | ||||
yield (filename, status) pairs for items in the trees | ||||
that have status in states. | ||||
filenames are relative to the repo root | ||||
""" | ||||
for file, st in self.files.iteritems(): | ||||
if st in states: | ||||
yield join(self.path, file), st | ||||
for dir in self.dirs.itervalues(): | ||||
for e in dir.walk(states): | ||||
yield e | ||||
def lookup(self, states, path): | ||||
""" | ||||
yield root-relative filenames that match path, and whose | ||||
status are in states: | ||||
* if path is a file, yield path | ||||
* if path is a directory, yield directory files | ||||
* if path is not tracked, yield nothing | ||||
""" | ||||
if path[-1] == '/': | ||||
path = path[:-1] | ||||
paths = path.split('/') | ||||
# we need to check separately for last node | ||||
last = paths.pop() | ||||
tree = self | ||||
try: | ||||
for dir in paths: | ||||
tree = tree.dirs[dir] | ||||
except KeyError: | ||||
# path is not tracked | ||||
return | ||||
try: | ||||
# if path is a directory, walk it | ||||
for file, st in tree.dirs[last].walk(states): | ||||
yield file | ||||
except KeyError: | ||||
try: | ||||
if tree.files[last] in states: | ||||
# path is a file | ||||
yield path | ||||
except KeyError: | ||||
# path is not tracked | ||||
pass | ||||
|
r8610 | class repowatcher(pollable): | ||
""" | ||||
Watches inotify events | ||||
""" | ||||
|
r6239 | statuskeys = 'almr!?' | ||
|
r8383 | mask = ( | ||
inotify.IN_ATTRIB | | ||||
inotify.IN_CREATE | | ||||
inotify.IN_DELETE | | ||||
inotify.IN_DELETE_SELF | | ||||
inotify.IN_MODIFY | | ||||
inotify.IN_MOVED_FROM | | ||||
inotify.IN_MOVED_TO | | ||||
inotify.IN_MOVE_SELF | | ||||
inotify.IN_ONLYDIR | | ||||
inotify.IN_UNMOUNT | | ||||
0) | ||||
|
r6239 | |||
|
r9350 | def __init__(self, ui, dirstate, root): | ||
|
r6239 | self.ui = ui | ||
|
r9350 | self.dirstate = dirstate | ||
self.wprefix = join(root, '') | ||||
|
r9349 | self.prefixlen = len(self.wprefix) | ||
|
r6239 | try: | ||
|
r8385 | self.watcher = watcher.watcher() | ||
|
r6239 | except OSError, err: | ||
raise util.Abort(_('inotify service not available: %s') % | ||||
err.strerror) | ||||
|
r8385 | self.threshold = watcher.threshold(self.watcher) | ||
|
r6239 | self.fileno = self.watcher.fileno | ||
|
r9115 | self.tree = directory() | ||
|
r6239 | self.statcache = {} | ||
|
r9115 | self.statustrees = dict([(s, directory()) for s in self.statuskeys]) | ||
|
r6239 | |||
self.last_event = None | ||||
|
r8605 | self.lastevent = {} | ||
|
r6239 | |||
|
r8792 | self.register(timeout=None) | ||
|
r6239 | self.ds_info = self.dirstate_info() | ||
|
r8604 | self.handle_timeout() | ||
|
r6239 | self.scan() | ||
def event_time(self): | ||||
last = self.last_event | ||||
now = time.time() | ||||
self.last_event = now | ||||
if last is None: | ||||
return 'start' | ||||
delta = now - last | ||||
if delta < 5: | ||||
return '+%.3f' % delta | ||||
if delta < 50: | ||||
return '+%.2f' % delta | ||||
return '+%.1f' % delta | ||||
def dirstate_info(self): | ||||
try: | ||||
|
r9350 | st = os.lstat(self.wprefix + '.hg/dirstate') | ||
|
r6239 | return st.st_mtime, st.st_ino | ||
except OSError, err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
return 0, 0 | ||||
def add_watch(self, path, mask): | ||||
if not path: | ||||
return | ||||
if self.watcher.path(path) is None: | ||||
if self.ui.debugflag: | ||||
|
r9349 | self.ui.note(_('watching %r\n') % path[self.prefixlen:]) | ||
|
r6239 | try: | ||
self.watcher.add(path, mask) | ||||
except OSError, err: | ||||
if err.errno in (errno.ENOENT, errno.ENOTDIR): | ||||
return | ||||
if err.errno != errno.ENOSPC: | ||||
raise | ||||
|
r9350 | _explain_watch_limit(self.ui, self.dirstate, self.wprefix) | ||
|
r6239 | |||
def setup(self): | ||||
|
r9349 | self.ui.note(_('watching directories under %r\n') % self.wprefix) | ||
|
r9350 | self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE) | ||
|
r6239 | self.check_dirstate() | ||
def filestatus(self, fn, st): | ||||
try: | ||||
|
r9350 | type_, mode, size, time = self.dirstate._map[fn][:4] | ||
|
r6239 | except KeyError: | ||
type_ = '?' | ||||
if type_ == 'n': | ||||
st_mode, st_size, st_mtime = st | ||||
|
r7082 | if size == -1: | ||
return 'l' | ||||
|
r6239 | if size and (size != st_size or (mode ^ st_mode) & 0100): | ||
return 'm' | ||||
if time != int(st_mtime): | ||||
return 'l' | ||||
return 'n' | ||||
|
r9350 | if type_ == '?' and self.dirstate._ignore(fn): | ||
|
r6239 | return 'i' | ||
return type_ | ||||
|
r8599 | def updatefile(self, wfn, osstat): | ||
''' | ||||
update the file entry of an existing file. | ||||
osstat: (mode, size, time) tuple, as returned by os.lstat(wfn) | ||||
''' | ||||
self._updatestatus(wfn, self.filestatus(wfn, osstat)) | ||||
def deletefile(self, wfn, oldstatus): | ||||
''' | ||||
update the entry of a file which has been deleted. | ||||
oldstatus: char in statuskeys, status of the file before deletion | ||||
''' | ||||
if oldstatus == 'r': | ||||
newstatus = 'r' | ||||
elif oldstatus in 'almn': | ||||
newstatus = '!' | ||||
else: | ||||
newstatus = None | ||||
self.statcache.pop(wfn, None) | ||||
self._updatestatus(wfn, newstatus) | ||||
def _updatestatus(self, wfn, newstatus): | ||||
|
r8382 | ''' | ||
|
r9115 | Update the stored status of a file. | ||
|
r8382 | |||
|
r8599 | newstatus: - char in (statuskeys + 'ni'), new status to apply. | ||
- or None, to stop tracking wfn | ||||
|
r8382 | ''' | ||
|
r8787 | root, fn = split(wfn) | ||
|
r9115 | d = self.tree.dir(root) | ||
|
r8599 | |||
|
r9115 | oldstatus = d.files.get(fn) | ||
|
r8599 | # oldstatus can be either: | ||
# - None : fn is new | ||||
# - a char in statuskeys: fn is a (tracked) file | ||||
|
r8382 | if self.ui.debugflag and oldstatus != newstatus: | ||
|
r9115 | self.ui.note(_('status: %r %s -> %s\n') % | ||
|
r8382 | (wfn, oldstatus, newstatus)) | ||
|
r9115 | |||
if oldstatus and oldstatus in self.statuskeys \ | ||||
and oldstatus != newstatus: | ||||
del self.statustrees[oldstatus].dir(root).files[fn] | ||||
|
r9348 | |||
if newstatus in (None, 'i'): | ||||
d.files.pop(fn, None) | ||||
elif oldstatus != newstatus: | ||||
|
r9115 | d.files[fn] = newstatus | ||
|
r9348 | if newstatus != 'n': | ||
self.statustrees[newstatus].dir(root).files[fn] = newstatus | ||||
|
r7892 | |||
|
r6239 | |||
def check_deleted(self, key): | ||||
# Files that had been deleted but were present in the dirstate | ||||
# may have vanished from the dirstate; we must clean them up. | ||||
nuke = [] | ||||
|
r9115 | for wfn, ignore in self.statustrees[key].walk(key): | ||
|
r9350 | if wfn not in self.dirstate: | ||
|
r6239 | nuke.append(wfn) | ||
for wfn in nuke: | ||||
|
r8787 | root, fn = split(wfn) | ||
|
r9115 | del self.statustrees[key].dir(root).files[fn] | ||
del self.tree.dir(root).files[fn] | ||||
|
r6287 | |||
|
r6239 | def scan(self, topdir=''): | ||
|
r9350 | ds = self.dirstate._map.copy() | ||
|
r9349 | self.add_watch(join(self.wprefix, topdir), self.mask) | ||
|
r9350 | for root, dirs, files in walk(self.dirstate, self.wprefix, topdir): | ||
|
r6239 | for d in dirs: | ||
self.add_watch(join(root, d), self.mask) | ||||
|
r9349 | wroot = root[self.prefixlen:] | ||
|
r8334 | for fn in files: | ||
|
r6239 | wfn = join(wroot, fn) | ||
|
r8599 | self.updatefile(wfn, self.getstat(wfn)) | ||
|
r6239 | ds.pop(wfn, None) | ||
wtopdir = topdir | ||||
if wtopdir and wtopdir[-1] != '/': | ||||
wtopdir += '/' | ||||
for wfn, state in ds.iteritems(): | ||||
if not wfn.startswith(wtopdir): | ||||
continue | ||||
|
r7302 | try: | ||
st = self.stat(wfn) | ||||
except OSError: | ||||
status = state[0] | ||||
|
r8599 | self.deletefile(wfn, status) | ||
|
r6239 | else: | ||
|
r8599 | self.updatefile(wfn, st) | ||
|
r6239 | self.check_deleted('!') | ||
self.check_deleted('r') | ||||
def check_dirstate(self): | ||||
ds_info = self.dirstate_info() | ||||
if ds_info == self.ds_info: | ||||
return | ||||
self.ds_info = ds_info | ||||
if not self.ui.debugflag: | ||||
self.last_event = None | ||||
self.ui.note(_('%s dirstate reload\n') % self.event_time()) | ||||
|
r9350 | self.dirstate.invalidate() | ||
|
r8604 | self.handle_timeout() | ||
|
r6239 | self.scan() | ||
self.ui.note(_('%s end dirstate reload\n') % self.event_time()) | ||||
def update_hgignore(self): | ||||
# An update of the ignore file can potentially change the | ||||
# states of all unknown and ignored files. | ||||
# XXX If the user has other ignore files outside the repo, or | ||||
# changes their list of ignore files at run time, we'll | ||||
# potentially never see changes to them. We could get the | ||||
# client to report to us what ignore data they're using. | ||||
# But it's easier to do nothing than to open that can of | ||||
# worms. | ||||
|
r9350 | if '_ignore' in self.dirstate.__dict__: | ||
delattr(self.dirstate, '_ignore') | ||||
|
r6961 | self.ui.note(_('rescanning due to .hgignore change\n')) | ||
|
r8604 | self.handle_timeout() | ||
|
r6239 | self.scan() | ||
|
r6287 | |||
|
r6239 | def getstat(self, wpath): | ||
try: | ||||
return self.statcache[wpath] | ||||
except KeyError: | ||||
try: | ||||
return self.stat(wpath) | ||||
except OSError, err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
|
r6287 | |||
|
r6239 | def stat(self, wpath): | ||
try: | ||||
st = os.lstat(join(self.wprefix, wpath)) | ||||
ret = st.st_mode, st.st_size, st.st_mtime | ||||
self.statcache[wpath] = ret | ||||
return ret | ||||
|
r7280 | except OSError: | ||
|
r6239 | self.statcache.pop(wpath, None) | ||
raise | ||||
|
r6287 | |||
|
r8606 | @eventaction('c') | ||
|
r6239 | def created(self, wpath): | ||
if wpath == '.hgignore': | ||||
self.update_hgignore() | ||||
try: | ||||
st = self.stat(wpath) | ||||
if stat.S_ISREG(st[0]): | ||||
|
r8599 | self.updatefile(wpath, st) | ||
|
r7280 | except OSError: | ||
|
r6239 | pass | ||
|
r8606 | @eventaction('m') | ||
|
r6239 | def modified(self, wpath): | ||
if wpath == '.hgignore': | ||||
self.update_hgignore() | ||||
try: | ||||
st = self.stat(wpath) | ||||
if stat.S_ISREG(st[0]): | ||||
|
r9350 | if self.dirstate[wpath] in 'lmn': | ||
|
r8599 | self.updatefile(wpath, st) | ||
|
r6239 | except OSError: | ||
pass | ||||
|
r8606 | @eventaction('d') | ||
|
r6239 | def deleted(self, wpath): | ||
if wpath == '.hgignore': | ||||
self.update_hgignore() | ||||
elif wpath.startswith('.hg/'): | ||||
if wpath == '.hg/wlock': | ||||
self.check_dirstate() | ||||
return | ||||
|
r9350 | self.deletefile(wpath, self.dirstate[wpath]) | ||
|
r6287 | |||
|
r6239 | def process_create(self, wpath, evt): | ||
if self.ui.debugflag: | ||||
self.ui.note(_('%s event: created %s\n') % | ||||
(self.event_time(), wpath)) | ||||
if evt.mask & inotify.IN_ISDIR: | ||||
self.scan(wpath) | ||||
else: | ||||
|
r8606 | self.created(wpath) | ||
|
r6239 | |||
def process_delete(self, wpath, evt): | ||||
if self.ui.debugflag: | ||||
|
r6961 | self.ui.note(_('%s event: deleted %s\n') % | ||
|
r6239 | (self.event_time(), wpath)) | ||
if evt.mask & inotify.IN_ISDIR: | ||||
|
r9115 | tree = self.tree.dir(wpath) | ||
todelete = [wfn for wfn, ignore in tree.walk('?')] | ||||
for fn in todelete: | ||||
self.deletefile(fn, '?') | ||||
|
r6239 | self.scan(wpath) | ||
|
r8600 | else: | ||
|
r8606 | self.deleted(wpath) | ||
|
r6239 | |||
def process_modify(self, wpath, evt): | ||||
if self.ui.debugflag: | ||||
self.ui.note(_('%s event: modified %s\n') % | ||||
(self.event_time(), wpath)) | ||||
if not (evt.mask & inotify.IN_ISDIR): | ||||
|
r8606 | self.modified(wpath) | ||
|
r6239 | |||
def process_unmount(self, evt): | ||||
self.ui.warn(_('filesystem containing %s was unmounted\n') % | ||||
evt.fullpath) | ||||
sys.exit(0) | ||||
|
r8609 | def handle_pollevents(self, events): | ||
|
r6239 | if self.ui.debugflag: | ||
|
r6961 | self.ui.note(_('%s readable: %d bytes\n') % | ||
|
r6239 | (self.event_time(), self.threshold.readable())) | ||
if not self.threshold(): | ||||
if self.registered: | ||||
if self.ui.debugflag: | ||||
|
r6961 | self.ui.note(_('%s below threshold - unhooking\n') % | ||
|
r6239 | (self.event_time())) | ||
|
r8792 | self.unregister() | ||
|
r6239 | self.timeout = 250 | ||
else: | ||||
self.read_events() | ||||
def read_events(self, bufsize=None): | ||||
events = self.watcher.read(bufsize) | ||||
if self.ui.debugflag: | ||||
|
r6961 | self.ui.note(_('%s reading %d events\n') % | ||
|
r6239 | (self.event_time(), len(events))) | ||
for evt in events: | ||||
|
r8953 | assert evt.fullpath.startswith(self.wprefix) | ||
|
r9349 | wpath = evt.fullpath[self.prefixlen:] | ||
|
r8953 | |||
|
r9117 | # paths have been normalized, wpath never ends with a '/' | ||
if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR: | ||||
# ignore subdirectories of .hg/ (merge, patches...) | ||||
continue | ||||
|
r6239 | if evt.mask & inotify.IN_UNMOUNT: | ||
self.process_unmount(wpath, evt) | ||||
elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB): | ||||
self.process_modify(wpath, evt) | ||||
elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF | | ||||
inotify.IN_MOVED_FROM): | ||||
self.process_delete(wpath, evt) | ||||
elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO): | ||||
self.process_create(wpath, evt) | ||||
|
r8605 | self.lastevent.clear() | ||
|
r6239 | def handle_timeout(self): | ||
if not self.registered: | ||||
if self.ui.debugflag: | ||||
|
r6961 | self.ui.note(_('%s hooking back up with %d bytes readable\n') % | ||
|
r6239 | (self.event_time(), self.threshold.readable())) | ||
self.read_events(0) | ||||
|
r8792 | self.register(timeout=None) | ||
|
r6239 | |||
self.timeout = None | ||||
def shutdown(self): | ||||
self.watcher.close() | ||||
|
r8555 | def debug(self): | ||
""" | ||||
Returns a sorted list of relatives paths currently watched, | ||||
for debugging purposes. | ||||
""" | ||||
|
r9349 | return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher) | ||
|
r8555 | |||
|
r8610 | class server(pollable): | ||
""" | ||||
Listens for client queries on unix socket inotify.sock | ||||
""" | ||||
|
r9350 | def __init__(self, ui, root, repowatcher, timeout): | ||
|
r6239 | self.ui = ui | ||
|
r8335 | self.repowatcher = repowatcher | ||
|
r6239 | self.sock = socket.socket(socket.AF_UNIX) | ||
|
r9350 | self.sockpath = join(root, '.hg/inotify.sock') | ||
|
r6997 | self.realsockpath = None | ||
|
r6239 | try: | ||
self.sock.bind(self.sockpath) | ||||
except socket.error, err: | ||||
if err[0] == errno.EADDRINUSE: | ||||
|
r6997 | raise AlreadyStartedException(_('could not start server: %s') | ||
|
r6239 | % err[1]) | ||
|
r6997 | if err[0] == "AF_UNIX path too long": | ||
tempdir = tempfile.mkdtemp(prefix="hg-inotify-") | ||||
self.realsockpath = os.path.join(tempdir, "inotify.sock") | ||||
try: | ||||
self.sock.bind(self.realsockpath) | ||||
os.symlink(self.realsockpath, self.sockpath) | ||||
except (OSError, socket.error), inst: | ||||
try: | ||||
os.unlink(self.realsockpath) | ||||
except: | ||||
pass | ||||
os.rmdir(tempdir) | ||||
if inst.errno == errno.EEXIST: | ||||
raise AlreadyStartedException(_('could not start server: %s') | ||||
% inst.strerror) | ||||
raise | ||||
else: | ||||
raise | ||||
|
r6239 | self.sock.listen(5) | ||
self.fileno = self.sock.fileno | ||||
|
r8792 | self.register(timeout=timeout) | ||
|
r6239 | |||
def handle_timeout(self): | ||||
pass | ||||
|
r8554 | def answer_stat_query(self, cs): | ||
|
r6239 | names = cs.read().split('\0') | ||
|
r6287 | |||
|
r6239 | states = names.pop() | ||
self.ui.note(_('answering query for %r\n') % states) | ||||
|
r8335 | if self.repowatcher.timeout: | ||
|
r6239 | # We got a query while a rescan is pending. Make sure we | ||
# rescan before responding, or we could give back a wrong | ||||
# answer. | ||||
|
r8335 | self.repowatcher.handle_timeout() | ||
|
r6239 | |||
if not names: | ||||
def genresult(states, tree): | ||||
|
r9115 | for fn, state in tree.walk(states): | ||
|
r6239 | yield fn | ||
else: | ||||
def genresult(states, tree): | ||||
for fn in names: | ||||
|
r9115 | for f in tree.lookup(states, fn): | ||
yield f | ||||
|
r6239 | |||
|
r8554 | return ['\0'.join(r) for r in [ | ||
|
r8335 | genresult('l', self.repowatcher.statustrees['l']), | ||
genresult('m', self.repowatcher.statustrees['m']), | ||||
genresult('a', self.repowatcher.statustrees['a']), | ||||
genresult('r', self.repowatcher.statustrees['r']), | ||||
genresult('!', self.repowatcher.statustrees['!']), | ||||
'?' in states | ||||
and genresult('?', self.repowatcher.statustrees['?']) | ||||
or [], | ||||
|
r6239 | [], | ||
|
r8335 | 'c' in states and genresult('n', self.repowatcher.tree) or [], | ||
|
r6239 | ]] | ||
|
r8555 | def answer_dbug_query(self): | ||
return ['\0'.join(self.repowatcher.debug())] | ||||
|
r8609 | def handle_pollevents(self, events): | ||
for e in events: | ||||
self.handle_pollevent() | ||||
|
r8608 | def handle_pollevent(self): | ||
|
r8554 | sock, addr = self.sock.accept() | ||
cs = common.recvcs(sock) | ||||
version = ord(cs.read(1)) | ||||
if version != common.version: | ||||
self.ui.warn(_('received query from incompatible client ' | ||||
'version %d\n') % version) | ||||
|
r8952 | try: | ||
# try to send back our version to the client | ||||
# this way, the client too is informed of the mismatch | ||||
sock.sendall(chr(common.version)) | ||||
except: | ||||
pass | ||||
|
r8554 | return | ||
type = cs.read(4) | ||||
if type == 'STAT': | ||||
results = self.answer_stat_query(cs) | ||||
|
r8555 | elif type == 'DBUG': | ||
results = self.answer_dbug_query() | ||||
|
r8554 | else: | ||
self.ui.warn(_('unrecognized query type: %s\n') % type) | ||||
return | ||||
|
r6239 | try: | ||
try: | ||||
|
r8386 | v = chr(common.version) | ||
|
r8553 | sock.sendall(v + type + struct.pack(common.resphdrfmts[type], | ||
*map(len, results))) | ||||
|
r6239 | sock.sendall(''.join(results)) | ||
finally: | ||||
sock.shutdown(socket.SHUT_WR) | ||||
except socket.error, err: | ||||
if err[0] != errno.EPIPE: | ||||
raise | ||||
def shutdown(self): | ||||
self.sock.close() | ||||
try: | ||||
os.unlink(self.sockpath) | ||||
|
r6997 | if self.realsockpath: | ||
os.unlink(self.realsockpath) | ||||
os.rmdir(os.path.dirname(self.realsockpath)) | ||||
|
r6239 | except OSError, err: | ||
if err.errno != errno.ENOENT: | ||||
raise | ||||
|
r8385 | class master(object): | ||
|
r9351 | def __init__(self, ui, dirstate, root, timeout=None): | ||
|
r6239 | self.ui = ui | ||
|
r9351 | self.repowatcher = repowatcher(ui, dirstate, root) | ||
self.server = server(ui, root, self.repowatcher, timeout) | ||||
|
r6239 | |||
def shutdown(self): | ||||
|
r8792 | for obj in pollable.instances.itervalues(): | ||
|
r6239 | obj.shutdown() | ||
def run(self): | ||||
|
r8335 | self.repowatcher.setup() | ||
|
r6239 | self.ui.note(_('finished setup\n')) | ||
if os.getenv('TIME_STARTUP'): | ||||
sys.exit(0) | ||||
|
r8793 | pollable.run() | ||
|
r6239 | |||
|
r9514 | def start(ui, dirstate, root, opts): | ||
timeout = opts.get('timeout') | ||||
if timeout: | ||||
timeout = float(timeout) * 1e3 | ||||
class service(object): | ||||
def init(self): | ||||
|
r7451 | try: | ||
|
r9514 | self.master = master(ui, dirstate, root, timeout) | ||
except AlreadyStartedException, inst: | ||||
raise util.Abort(str(inst)) | ||||
|
r6239 | |||
|
r9514 | def run(self): | ||
try: | ||||
self.master.run() | ||||
finally: | ||||
self.master.shutdown() | ||||
|
r6239 | |||
|
r9514 | runargs = None | ||
if 'inserve' not in sys.argv: | ||||
runargs = [sys.argv[0], 'inserve', '-R', root] | ||||
|
r6239 | |||
|
r9514 | service = service() | ||
logfile = ui.config('inotify', 'log') | ||||
cmdutil.service(opts, initfn=service.init, runfn=service.run, | ||||
logfile=logfile, runargs=runargs) | ||||