server.py
489 lines
| 15.2 KiB
| text/x-python
|
PythonLexer
|
r9933 | # server.py - common entry point for inotify status server | ||
|
r6239 | # | ||
|
r9933 | # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com> | ||
|
r6239 | # | ||
|
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 | ||
|
r9933 | import errno | ||
import os | ||||
import socket | ||||
import stat | ||||
import struct | ||||
import sys | ||||
import tempfile | ||||
|
r6239 | |||
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 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 | |||
|
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 | ||||
|
r9854 | def walk(self, states, visited=None): | ||
|
r9115 | """ | ||
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(): | ||||
|
r9854 | if visited is not None: | ||
visited.add(dir.path) | ||||
|
r9115 | for e in dir.walk(states): | ||
yield e | ||||
|
r9854 | def lookup(self, states, path, visited): | ||
|
r9115 | """ | ||
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 | ||||
|
r9854 | visited.add(tree.path) | ||
|
r9115 | return | ||
try: | ||||
# if path is a directory, walk it | ||||
|
r9854 | target = tree.dirs[last] | ||
visited.add(target.path) | ||||
for file, st in target.walk(states, visited): | ||||
|
r9115 | yield file | ||
except KeyError: | ||||
try: | ||||
if tree.files[last] in states: | ||||
# path is a file | ||||
|
r9854 | visited.add(tree.path) | ||
|
r9115 | yield path | ||
except KeyError: | ||||
# path is not tracked | ||||
pass | ||||
|
r9933 | class repowatcher(object): | ||
|
r8610 | """ | ||
Watches inotify events | ||||
""" | ||||
|
r6239 | statuskeys = 'almr!?' | ||
|
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 | |||
|
r9115 | self.tree = directory() | ||
|
r6239 | self.statcache = {} | ||
|
r9115 | self.statustrees = dict([(s, directory()) for s in self.statuskeys]) | ||
|
r6239 | |||
|
r9933 | self.ds_info = self.dirstate_info() | ||
|
r6239 | self.last_event = None | ||
|
r9933 | def handle_timeout(self): | ||
pass | ||||
|
r6239 | |||
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 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 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 | ||||
|
r9350 | self.dirstate.invalidate() | ||
|
r8604 | self.handle_timeout() | ||
|
r6239 | self.scan() | ||
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 | |||
|
r9933 | class socketlistener(object): | ||
|
r8610 | """ | ||
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: | ||||
|
r9900 | raise AlreadyStartedException( _('cannot start: socket is ' | ||
'already bound')) | ||||
|
r6997 | if err[0] == "AF_UNIX path too long": | ||
|
r9900 | if os.path.islink(self.sockpath) and \ | ||
not os.path.exists(self.sockpath): | ||||
raise util.Abort('inotify-server: cannot start: ' | ||||
'.hg/inotify.sock is a broken symlink') | ||||
|
r6997 | 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: | ||||
|
r9900 | raise AlreadyStartedException(_('cannot start: tried ' | ||
'linking .hg/inotify.sock to a temporary socket but' | ||||
' .hg/inotify.sock already exists')) | ||||
|
r6997 | raise | ||
else: | ||||
raise | ||||
|
r6239 | self.sock.listen(5) | ||
self.fileno = self.sock.fileno | ||||
|
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) | ||||
|
r9854 | visited = set() | ||
|
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: | ||||
|
r9854 | for f in tree.lookup(states, fn, visited): | ||
|
r9115 | 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 [], | ||
|
r9854 | visited | ||
|
r6239 | ]] | ||
|
r8555 | def answer_dbug_query(self): | ||
return ['\0'.join(self.repowatcher.debug())] | ||||
|
r9933 | def accept_connection(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 | ||||
|
r9933 | if sys.platform == 'linux2': | ||
import linuxserver as _server | ||||
else: | ||||
raise ImportError | ||||
|
r6239 | |||
|
r9933 | master = _server.master | ||
|
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: | ||||
|
r9900 | raise util.Abort("inotify-server: %s" % inst) | ||
|
r6239 | |||
|
r9514 | def run(self): | ||
try: | ||||
self.master.run() | ||||
finally: | ||||
self.master.shutdown() | ||||
|
r6239 | |||
|
r9514 | if 'inserve' not in sys.argv: | ||
runargs = [sys.argv[0], 'inserve', '-R', root] | ||||
|
r9897 | else: | ||
runargs = sys.argv[:] | ||||
pidfile = ui.config('inotify', 'pidfile') | ||||
if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs: | ||||
runargs.append("--pid-file=%s" % pidfile) | ||||
|
r6239 | |||
|
r9514 | service = service() | ||
logfile = ui.config('inotify', 'log') | ||||
|
r10013 | |||
appendpid = ui.configbool('inotify', 'appendpid', False) | ||||
|
r9514 | cmdutil.service(opts, initfn=service.init, runfn=service.run, | ||
|
r10013 | logfile=logfile, runargs=runargs, appendpid=appendpid) | ||