##// END OF EJS Templates
inotify: raise correct error if server is already started in a deep repository...
Nicolas Dumazet -
r12650:fed4bb2c default
parent child Browse files
Show More
@@ -1,489 +1,492
1 1 # server.py - common entry point for inotify status server
2 2 #
3 3 # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial import cmdutil, osutil, util
10 10 import common
11 11
12 12 import errno
13 13 import os
14 14 import socket
15 15 import stat
16 16 import struct
17 17 import sys
18 18 import tempfile
19 19
20 20 class AlreadyStartedException(Exception):
21 21 pass
22 22 class TimeoutException(Exception):
23 23 pass
24 24
25 25 def join(a, b):
26 26 if a:
27 27 if a[-1] == '/':
28 28 return a + b
29 29 return a + '/' + b
30 30 return b
31 31
32 32 def split(path):
33 33 c = path.rfind('/')
34 34 if c == -1:
35 35 return '', path
36 36 return path[:c], path[c + 1:]
37 37
38 38 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
39 39
40 40 def walk(dirstate, absroot, root):
41 41 '''Like os.walk, but only yields regular files.'''
42 42
43 43 # This function is critical to performance during startup.
44 44
45 45 def walkit(root, reporoot):
46 46 files, dirs = [], []
47 47
48 48 try:
49 49 fullpath = join(absroot, root)
50 50 for name, kind in osutil.listdir(fullpath):
51 51 if kind == stat.S_IFDIR:
52 52 if name == '.hg':
53 53 if not reporoot:
54 54 return
55 55 else:
56 56 dirs.append(name)
57 57 path = join(root, name)
58 58 if dirstate._ignore(path):
59 59 continue
60 60 for result in walkit(path, False):
61 61 yield result
62 62 elif kind in (stat.S_IFREG, stat.S_IFLNK):
63 63 files.append(name)
64 64 yield fullpath, dirs, files
65 65
66 66 except OSError, err:
67 67 if err.errno == errno.ENOTDIR:
68 68 # fullpath was a directory, but has since been replaced
69 69 # by a file.
70 70 yield fullpath, dirs, files
71 71 elif err.errno not in walk_ignored_errors:
72 72 raise
73 73
74 74 return walkit(root, root == '')
75 75
76 76 class directory(object):
77 77 """
78 78 Representing a directory
79 79
80 80 * path is the relative path from repo root to this directory
81 81 * files is a dict listing the files in this directory
82 82 - keys are file names
83 83 - values are file status
84 84 * dirs is a dict listing the subdirectories
85 85 - key are subdirectories names
86 86 - values are directory objects
87 87 """
88 88 def __init__(self, relpath=''):
89 89 self.path = relpath
90 90 self.files = {}
91 91 self.dirs = {}
92 92
93 93 def dir(self, relpath):
94 94 """
95 95 Returns the directory contained at the relative path relpath.
96 96 Creates the intermediate directories if necessary.
97 97 """
98 98 if not relpath:
99 99 return self
100 100 l = relpath.split('/')
101 101 ret = self
102 102 while l:
103 103 next = l.pop(0)
104 104 try:
105 105 ret = ret.dirs[next]
106 106 except KeyError:
107 107 d = directory(join(ret.path, next))
108 108 ret.dirs[next] = d
109 109 ret = d
110 110 return ret
111 111
112 112 def walk(self, states, visited=None):
113 113 """
114 114 yield (filename, status) pairs for items in the trees
115 115 that have status in states.
116 116 filenames are relative to the repo root
117 117 """
118 118 for file, st in self.files.iteritems():
119 119 if st in states:
120 120 yield join(self.path, file), st
121 121 for dir in self.dirs.itervalues():
122 122 if visited is not None:
123 123 visited.add(dir.path)
124 124 for e in dir.walk(states):
125 125 yield e
126 126
127 127 def lookup(self, states, path, visited):
128 128 """
129 129 yield root-relative filenames that match path, and whose
130 130 status are in states:
131 131 * if path is a file, yield path
132 132 * if path is a directory, yield directory files
133 133 * if path is not tracked, yield nothing
134 134 """
135 135 if path[-1] == '/':
136 136 path = path[:-1]
137 137
138 138 paths = path.split('/')
139 139
140 140 # we need to check separately for last node
141 141 last = paths.pop()
142 142
143 143 tree = self
144 144 try:
145 145 for dir in paths:
146 146 tree = tree.dirs[dir]
147 147 except KeyError:
148 148 # path is not tracked
149 149 visited.add(tree.path)
150 150 return
151 151
152 152 try:
153 153 # if path is a directory, walk it
154 154 target = tree.dirs[last]
155 155 visited.add(target.path)
156 156 for file, st in target.walk(states, visited):
157 157 yield file
158 158 except KeyError:
159 159 try:
160 160 if tree.files[last] in states:
161 161 # path is a file
162 162 visited.add(tree.path)
163 163 yield path
164 164 except KeyError:
165 165 # path is not tracked
166 166 pass
167 167
168 168 class repowatcher(object):
169 169 """
170 170 Watches inotify events
171 171 """
172 172 statuskeys = 'almr!?'
173 173
174 174 def __init__(self, ui, dirstate, root):
175 175 self.ui = ui
176 176 self.dirstate = dirstate
177 177
178 178 self.wprefix = join(root, '')
179 179 self.prefixlen = len(self.wprefix)
180 180
181 181 self.tree = directory()
182 182 self.statcache = {}
183 183 self.statustrees = dict([(s, directory()) for s in self.statuskeys])
184 184
185 185 self.ds_info = self.dirstate_info()
186 186
187 187 self.last_event = None
188 188
189 189
190 190 def handle_timeout(self):
191 191 pass
192 192
193 193 def dirstate_info(self):
194 194 try:
195 195 st = os.lstat(self.wprefix + '.hg/dirstate')
196 196 return st.st_mtime, st.st_ino
197 197 except OSError, err:
198 198 if err.errno != errno.ENOENT:
199 199 raise
200 200 return 0, 0
201 201
202 202 def filestatus(self, fn, st):
203 203 try:
204 204 type_, mode, size, time = self.dirstate._map[fn][:4]
205 205 except KeyError:
206 206 type_ = '?'
207 207 if type_ == 'n':
208 208 st_mode, st_size, st_mtime = st
209 209 if size == -1:
210 210 return 'l'
211 211 if size and (size != st_size or (mode ^ st_mode) & 0100):
212 212 return 'm'
213 213 if time != int(st_mtime):
214 214 return 'l'
215 215 return 'n'
216 216 if type_ == '?' and self.dirstate._dirignore(fn):
217 217 # we must check not only if the file is ignored, but if any part
218 218 # of its path match an ignore pattern
219 219 return 'i'
220 220 return type_
221 221
222 222 def updatefile(self, wfn, osstat):
223 223 '''
224 224 update the file entry of an existing file.
225 225
226 226 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
227 227 '''
228 228
229 229 self._updatestatus(wfn, self.filestatus(wfn, osstat))
230 230
231 231 def deletefile(self, wfn, oldstatus):
232 232 '''
233 233 update the entry of a file which has been deleted.
234 234
235 235 oldstatus: char in statuskeys, status of the file before deletion
236 236 '''
237 237 if oldstatus == 'r':
238 238 newstatus = 'r'
239 239 elif oldstatus in 'almn':
240 240 newstatus = '!'
241 241 else:
242 242 newstatus = None
243 243
244 244 self.statcache.pop(wfn, None)
245 245 self._updatestatus(wfn, newstatus)
246 246
247 247 def _updatestatus(self, wfn, newstatus):
248 248 '''
249 249 Update the stored status of a file.
250 250
251 251 newstatus: - char in (statuskeys + 'ni'), new status to apply.
252 252 - or None, to stop tracking wfn
253 253 '''
254 254 root, fn = split(wfn)
255 255 d = self.tree.dir(root)
256 256
257 257 oldstatus = d.files.get(fn)
258 258 # oldstatus can be either:
259 259 # - None : fn is new
260 260 # - a char in statuskeys: fn is a (tracked) file
261 261
262 262 if self.ui.debugflag and oldstatus != newstatus:
263 263 self.ui.note(_('status: %r %s -> %s\n') %
264 264 (wfn, oldstatus, newstatus))
265 265
266 266 if oldstatus and oldstatus in self.statuskeys \
267 267 and oldstatus != newstatus:
268 268 del self.statustrees[oldstatus].dir(root).files[fn]
269 269
270 270 if newstatus in (None, 'i'):
271 271 d.files.pop(fn, None)
272 272 elif oldstatus != newstatus:
273 273 d.files[fn] = newstatus
274 274 if newstatus != 'n':
275 275 self.statustrees[newstatus].dir(root).files[fn] = newstatus
276 276
277 277 def check_deleted(self, key):
278 278 # Files that had been deleted but were present in the dirstate
279 279 # may have vanished from the dirstate; we must clean them up.
280 280 nuke = []
281 281 for wfn, ignore in self.statustrees[key].walk(key):
282 282 if wfn not in self.dirstate:
283 283 nuke.append(wfn)
284 284 for wfn in nuke:
285 285 root, fn = split(wfn)
286 286 del self.statustrees[key].dir(root).files[fn]
287 287 del self.tree.dir(root).files[fn]
288 288
289 289 def update_hgignore(self):
290 290 # An update of the ignore file can potentially change the
291 291 # states of all unknown and ignored files.
292 292
293 293 # XXX If the user has other ignore files outside the repo, or
294 294 # changes their list of ignore files at run time, we'll
295 295 # potentially never see changes to them. We could get the
296 296 # client to report to us what ignore data they're using.
297 297 # But it's easier to do nothing than to open that can of
298 298 # worms.
299 299
300 300 if '_ignore' in self.dirstate.__dict__:
301 301 delattr(self.dirstate, '_ignore')
302 302 self.ui.note(_('rescanning due to .hgignore change\n'))
303 303 self.handle_timeout()
304 304 self.scan()
305 305
306 306 def getstat(self, wpath):
307 307 try:
308 308 return self.statcache[wpath]
309 309 except KeyError:
310 310 try:
311 311 return self.stat(wpath)
312 312 except OSError, err:
313 313 if err.errno != errno.ENOENT:
314 314 raise
315 315
316 316 def stat(self, wpath):
317 317 try:
318 318 st = os.lstat(join(self.wprefix, wpath))
319 319 ret = st.st_mode, st.st_size, st.st_mtime
320 320 self.statcache[wpath] = ret
321 321 return ret
322 322 except OSError:
323 323 self.statcache.pop(wpath, None)
324 324 raise
325 325
326 326 class socketlistener(object):
327 327 """
328 328 Listens for client queries on unix socket inotify.sock
329 329 """
330 330 def __init__(self, ui, root, repowatcher, timeout):
331 331 self.ui = ui
332 332 self.repowatcher = repowatcher
333 333 self.sock = socket.socket(socket.AF_UNIX)
334 334 self.sockpath = join(root, '.hg/inotify.sock')
335 self.realsockpath = None
335
336 self.realsockpath = self.sockpath
337 if os.path.islink(self.sockpath):
338 if os.path.exists(self.sockpath):
339 self.realsockpath = os.readlink(self.sockpath)
340 else:
341 raise util.Abort('inotify-server: cannot start: '
342 '.hg/inotify.sock is a broken symlink')
336 343 try:
337 self.sock.bind(self.sockpath)
344 self.sock.bind(self.realsockpath)
338 345 except socket.error, err:
339 346 if err.args[0] == errno.EADDRINUSE:
340 347 raise AlreadyStartedException(_('cannot start: socket is '
341 348 'already bound'))
342 349 if err.args[0] == "AF_UNIX path too long":
343 if os.path.islink(self.sockpath) and \
344 not os.path.exists(self.sockpath):
345 raise util.Abort('inotify-server: cannot start: '
346 '.hg/inotify.sock is a broken symlink')
347 350 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
348 351 self.realsockpath = os.path.join(tempdir, "inotify.sock")
349 352 try:
350 353 self.sock.bind(self.realsockpath)
351 354 os.symlink(self.realsockpath, self.sockpath)
352 355 except (OSError, socket.error), inst:
353 356 try:
354 357 os.unlink(self.realsockpath)
355 358 except:
356 359 pass
357 360 os.rmdir(tempdir)
358 361 if inst.errno == errno.EEXIST:
359 362 raise AlreadyStartedException(_('cannot start: tried '
360 363 'linking .hg/inotify.sock to a temporary socket but'
361 364 ' .hg/inotify.sock already exists'))
362 365 raise
363 366 else:
364 367 raise
365 368 self.sock.listen(5)
366 369 self.fileno = self.sock.fileno
367 370
368 371 def answer_stat_query(self, cs):
369 372 names = cs.read().split('\0')
370 373
371 374 states = names.pop()
372 375
373 376 self.ui.note(_('answering query for %r\n') % states)
374 377
375 378 visited = set()
376 379 if not names:
377 380 def genresult(states, tree):
378 381 for fn, state in tree.walk(states):
379 382 yield fn
380 383 else:
381 384 def genresult(states, tree):
382 385 for fn in names:
383 386 for f in tree.lookup(states, fn, visited):
384 387 yield f
385 388
386 389 return ['\0'.join(r) for r in [
387 390 genresult('l', self.repowatcher.statustrees['l']),
388 391 genresult('m', self.repowatcher.statustrees['m']),
389 392 genresult('a', self.repowatcher.statustrees['a']),
390 393 genresult('r', self.repowatcher.statustrees['r']),
391 394 genresult('!', self.repowatcher.statustrees['!']),
392 395 '?' in states
393 396 and genresult('?', self.repowatcher.statustrees['?'])
394 397 or [],
395 398 [],
396 399 'c' in states and genresult('n', self.repowatcher.tree) or [],
397 400 visited
398 401 ]]
399 402
400 403 def answer_dbug_query(self):
401 404 return ['\0'.join(self.repowatcher.debug())]
402 405
403 406 def accept_connection(self):
404 407 sock, addr = self.sock.accept()
405 408
406 409 cs = common.recvcs(sock)
407 410 version = ord(cs.read(1))
408 411
409 412 if version != common.version:
410 413 self.ui.warn(_('received query from incompatible client '
411 414 'version %d\n') % version)
412 415 try:
413 416 # try to send back our version to the client
414 417 # this way, the client too is informed of the mismatch
415 418 sock.sendall(chr(common.version))
416 419 except:
417 420 pass
418 421 return
419 422
420 423 type = cs.read(4)
421 424
422 425 if type == 'STAT':
423 426 results = self.answer_stat_query(cs)
424 427 elif type == 'DBUG':
425 428 results = self.answer_dbug_query()
426 429 else:
427 430 self.ui.warn(_('unrecognized query type: %s\n') % type)
428 431 return
429 432
430 433 try:
431 434 try:
432 435 v = chr(common.version)
433 436
434 437 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
435 438 *map(len, results)))
436 439 sock.sendall(''.join(results))
437 440 finally:
438 441 sock.shutdown(socket.SHUT_WR)
439 442 except socket.error, err:
440 443 if err.args[0] != errno.EPIPE:
441 444 raise
442 445
443 446 if sys.platform == 'linux2':
444 447 import linuxserver as _server
445 448 else:
446 449 raise ImportError
447 450
448 451 master = _server.master
449 452
450 453 def start(ui, dirstate, root, opts):
451 454 timeout = opts.get('idle_timeout')
452 455 if timeout:
453 456 timeout = float(timeout) * 60000
454 457 else:
455 458 timeout = None
456 459
457 460 class service(object):
458 461 def init(self):
459 462 try:
460 463 self.master = master(ui, dirstate, root, timeout)
461 464 except AlreadyStartedException, inst:
462 465 raise util.Abort("inotify-server: %s" % inst)
463 466
464 467 def run(self):
465 468 try:
466 469 try:
467 470 self.master.run()
468 471 except TimeoutException:
469 472 pass
470 473 finally:
471 474 self.master.shutdown()
472 475
473 476 if 'inserve' not in sys.argv:
474 477 runargs = util.hgcmd() + ['inserve', '-R', root]
475 478 else:
476 479 runargs = util.hgcmd() + sys.argv[1:]
477 480
478 481 pidfile = ui.config('inotify', 'pidfile')
479 482 if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
480 483 runargs.append("--pid-file=%s" % pidfile)
481 484
482 485 service = service()
483 486 logfile = ui.config('inotify', 'log')
484 487
485 488 appendpid = ui.configbool('inotify', 'appendpid', False)
486 489
487 490 ui.debug('starting inotify server: %s\n' % ' '.join(runargs))
488 491 cmdutil.service(opts, initfn=service.init, runfn=service.run,
489 492 logfile=logfile, runargs=runargs, appendpid=appendpid)
@@ -1,29 +1,36
1 1
2 2 $ "$TESTDIR/hghave" inotify || exit 80
3 3 $ echo "[extensions]" >> $HGRCPATH
4 4 $ echo "inotify=" >> $HGRCPATH
5 5 $ p="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
6 6 $ hg init $p
7 7 $ cd $p
8 8
9 9 fail
10 10
11 11 $ ln -sf doesnotexist .hg/inotify.sock
12 12 $ hg st
13 13 abort: inotify-server: cannot start: .hg/inotify.sock is a broken symlink
14 14 inotify-client: could not start inotify server: child process failed to start
15 15 $ hg inserve
16 16 abort: inotify-server: cannot start: .hg/inotify.sock is a broken symlink
17 17 [255]
18 18 $ rm .hg/inotify.sock
19 19
20 20 inserve
21 21
22 22 $ hg inserve -d --pid-file=hg.pid
23 23 $ cat hg.pid >> "$DAEMON_PIDS"
24 24
25 25 status
26 26
27 27 $ hg status
28 28 ? hg.pid
29
30 if we try to start twice the server, make sure we get a correct error
31
32 $ hg inserve -d --pid-file=hg2.pid
33 abort: inotify-server: cannot start: socket is already bound
34 abort: child process failed to start
35 [255]
29 36 $ kill `cat hg.pid`
General Comments 0
You need to be logged in to leave comments. Login now