##// END OF EJS Templates
posix: move server side of unix domain sockets out of inotify...
Bryan O'Sullivan -
r18097:ae54cff7 default
parent child Browse files
Show More
@@ -1,444 +1,437
1 1 # linuxserver.py - inotify status server for linux
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from mercurial.i18n import _
10 10 from mercurial import osutil, util, error
11 11 import server
12 12 import errno, os, select, stat, sys, time
13 13
14 14 try:
15 15 import linux as inotify
16 16 from linux import watcher
17 17 except ImportError:
18 18 raise
19 19
20 20 def walkrepodirs(dirstate, absroot):
21 21 '''Iterate over all subdirectories of this repo.
22 22 Exclude the .hg directory, any nested repos, and ignored dirs.'''
23 23 def walkit(dirname, top):
24 24 fullpath = server.join(absroot, dirname)
25 25 try:
26 26 for name, kind in osutil.listdir(fullpath):
27 27 if kind == stat.S_IFDIR:
28 28 if name == '.hg':
29 29 if not top:
30 30 return
31 31 else:
32 32 d = server.join(dirname, name)
33 33 if dirstate._ignore(d):
34 34 continue
35 35 for subdir in walkit(d, False):
36 36 yield subdir
37 37 except OSError, err:
38 38 if err.errno not in server.walk_ignored_errors:
39 39 raise
40 40 yield fullpath
41 41
42 42 return walkit('', True)
43 43
44 44 def _explain_watch_limit(ui, dirstate, rootabs):
45 45 path = '/proc/sys/fs/inotify/max_user_watches'
46 46 try:
47 47 limit = int(util.readfile(path))
48 48 except IOError, err:
49 49 if err.errno != errno.ENOENT:
50 50 raise
51 51 raise util.Abort(_('this system does not seem to '
52 52 'support inotify'))
53 53 ui.warn(_('*** the current per-user limit on the number '
54 54 'of inotify watches is %s\n') % limit)
55 55 ui.warn(_('*** this limit is too low to watch every '
56 56 'directory in this repository\n'))
57 57 ui.warn(_('*** counting directories: '))
58 58 ndirs = len(list(walkrepodirs(dirstate, rootabs)))
59 59 ui.warn(_('found %d\n') % ndirs)
60 60 newlimit = min(limit, 1024)
61 61 while newlimit < ((limit + ndirs) * 1.1):
62 62 newlimit *= 2
63 63 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
64 64 (limit, newlimit))
65 65 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
66 66 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
67 67 % rootabs)
68 68
69 69 class pollable(object):
70 70 """
71 71 Interface to support polling.
72 72 The file descriptor returned by fileno() is registered to a polling
73 73 object.
74 74 Usage:
75 75 Every tick, check if an event has happened since the last tick:
76 76 * If yes, call handle_events
77 77 * If no, call handle_timeout
78 78 """
79 79 poll_events = select.POLLIN
80 80 instances = {}
81 81 poll = select.poll()
82 82
83 83 def fileno(self):
84 84 raise NotImplementedError
85 85
86 86 def handle_events(self, events):
87 87 raise NotImplementedError
88 88
89 89 def handle_timeout(self):
90 90 raise NotImplementedError
91 91
92 92 def shutdown(self):
93 93 raise NotImplementedError
94 94
95 95 def register(self, timeout):
96 96 fd = self.fileno()
97 97
98 98 pollable.poll.register(fd, pollable.poll_events)
99 99 pollable.instances[fd] = self
100 100
101 101 self.registered = True
102 102 self.timeout = timeout
103 103
104 104 def unregister(self):
105 105 pollable.poll.unregister(self)
106 106 self.registered = False
107 107
108 108 @classmethod
109 109 def run(cls):
110 110 while True:
111 111 timeout = None
112 112 timeobj = None
113 113 for obj in cls.instances.itervalues():
114 114 if obj.timeout is not None and (timeout is None
115 115 or obj.timeout < timeout):
116 116 timeout, timeobj = obj.timeout, obj
117 117 try:
118 118 events = cls.poll.poll(timeout)
119 119 except select.error, err:
120 120 if err.args[0] == errno.EINTR:
121 121 continue
122 122 raise
123 123 if events:
124 124 by_fd = {}
125 125 for fd, event in events:
126 126 by_fd.setdefault(fd, []).append(event)
127 127
128 128 for fd, events in by_fd.iteritems():
129 129 cls.instances[fd].handle_pollevents(events)
130 130
131 131 elif timeobj:
132 132 timeobj.handle_timeout()
133 133
134 134 def eventaction(code):
135 135 """
136 136 Decorator to help handle events in repowatcher
137 137 """
138 138 def decorator(f):
139 139 def wrapper(self, wpath):
140 140 if code == 'm' and wpath in self.lastevent and \
141 141 self.lastevent[wpath] in 'cm':
142 142 return
143 143 self.lastevent[wpath] = code
144 144 self.timeout = 250
145 145
146 146 f(self, wpath)
147 147
148 148 wrapper.func_name = f.func_name
149 149 return wrapper
150 150 return decorator
151 151
152 152 class repowatcher(server.repowatcher, pollable):
153 153 """
154 154 Watches inotify events
155 155 """
156 156 mask = (
157 157 inotify.IN_ATTRIB |
158 158 inotify.IN_CREATE |
159 159 inotify.IN_DELETE |
160 160 inotify.IN_DELETE_SELF |
161 161 inotify.IN_MODIFY |
162 162 inotify.IN_MOVED_FROM |
163 163 inotify.IN_MOVED_TO |
164 164 inotify.IN_MOVE_SELF |
165 165 inotify.IN_ONLYDIR |
166 166 inotify.IN_UNMOUNT |
167 167 0)
168 168
169 169 def __init__(self, ui, dirstate, root):
170 170 server.repowatcher.__init__(self, ui, dirstate, root)
171 171
172 172 self.lastevent = {}
173 173 self.dirty = False
174 174 try:
175 175 self.watcher = watcher.watcher()
176 176 except OSError, err:
177 177 raise util.Abort(_('inotify service not available: %s') %
178 178 err.strerror)
179 179 self.threshold = watcher.threshold(self.watcher)
180 180 self.fileno = self.watcher.fileno
181 181 self.register(timeout=None)
182 182
183 183 self.handle_timeout()
184 184 self.scan()
185 185
186 186 def event_time(self):
187 187 last = self.last_event
188 188 now = time.time()
189 189 self.last_event = now
190 190
191 191 if last is None:
192 192 return 'start'
193 193 delta = now - last
194 194 if delta < 5:
195 195 return '+%.3f' % delta
196 196 if delta < 50:
197 197 return '+%.2f' % delta
198 198 return '+%.1f' % delta
199 199
200 200 def add_watch(self, path, mask):
201 201 if not path:
202 202 return
203 203 if self.watcher.path(path) is None:
204 204 if self.ui.debugflag:
205 205 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
206 206 try:
207 207 self.watcher.add(path, mask)
208 208 except OSError, err:
209 209 if err.errno in (errno.ENOENT, errno.ENOTDIR):
210 210 return
211 211 if err.errno != errno.ENOSPC:
212 212 raise
213 213 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
214 214
215 215 def setup(self):
216 216 self.ui.note(_('watching directories under %r\n') % self.wprefix)
217 217 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
218 218
219 219 def scan(self, topdir=''):
220 220 ds = self.dirstate._map.copy()
221 221 self.add_watch(server.join(self.wprefix, topdir), self.mask)
222 222 for root, dirs, files in server.walk(self.dirstate, self.wprefix,
223 223 topdir):
224 224 for d in dirs:
225 225 self.add_watch(server.join(root, d), self.mask)
226 226 wroot = root[self.prefixlen:]
227 227 for fn in files:
228 228 wfn = server.join(wroot, fn)
229 229 self.updatefile(wfn, self.getstat(wfn))
230 230 ds.pop(wfn, None)
231 231 wtopdir = topdir
232 232 if wtopdir and wtopdir[-1] != '/':
233 233 wtopdir += '/'
234 234 for wfn, state in ds.iteritems():
235 235 if not wfn.startswith(wtopdir):
236 236 continue
237 237 try:
238 238 st = self.stat(wfn)
239 239 except OSError:
240 240 status = state[0]
241 241 self.deletefile(wfn, status)
242 242 else:
243 243 self.updatefile(wfn, st)
244 244 self.check_deleted('!')
245 245 self.check_deleted('r')
246 246
247 247 @eventaction('c')
248 248 def created(self, wpath):
249 249 if wpath == '.hgignore':
250 250 self.update_hgignore()
251 251 try:
252 252 st = self.stat(wpath)
253 253 if stat.S_ISREG(st[0]) or stat.S_ISLNK(st[0]):
254 254 self.updatefile(wpath, st)
255 255 except OSError:
256 256 pass
257 257
258 258 @eventaction('m')
259 259 def modified(self, wpath):
260 260 if wpath == '.hgignore':
261 261 self.update_hgignore()
262 262 try:
263 263 st = self.stat(wpath)
264 264 if stat.S_ISREG(st[0]):
265 265 if self.dirstate[wpath] in 'lmn':
266 266 self.updatefile(wpath, st)
267 267 except OSError:
268 268 pass
269 269
270 270 @eventaction('d')
271 271 def deleted(self, wpath):
272 272 if wpath == '.hgignore':
273 273 self.update_hgignore()
274 274 elif wpath.startswith('.hg/'):
275 275 return
276 276
277 277 self.deletefile(wpath, self.dirstate[wpath])
278 278
279 279 def process_create(self, wpath, evt):
280 280 if self.ui.debugflag:
281 281 self.ui.note(_('%s event: created %s\n') %
282 282 (self.event_time(), wpath))
283 283
284 284 if evt.mask & inotify.IN_ISDIR:
285 285 self.scan(wpath)
286 286 else:
287 287 self.created(wpath)
288 288
289 289 def process_delete(self, wpath, evt):
290 290 if self.ui.debugflag:
291 291 self.ui.note(_('%s event: deleted %s\n') %
292 292 (self.event_time(), wpath))
293 293
294 294 if evt.mask & inotify.IN_ISDIR:
295 295 tree = self.tree.dir(wpath)
296 296 todelete = [wfn for wfn, ignore in tree.walk('?')]
297 297 for fn in todelete:
298 298 self.deletefile(fn, '?')
299 299 self.scan(wpath)
300 300 else:
301 301 self.deleted(wpath)
302 302
303 303 def process_modify(self, wpath, evt):
304 304 if self.ui.debugflag:
305 305 self.ui.note(_('%s event: modified %s\n') %
306 306 (self.event_time(), wpath))
307 307
308 308 if not (evt.mask & inotify.IN_ISDIR):
309 309 self.modified(wpath)
310 310
311 311 def process_unmount(self, evt):
312 312 self.ui.warn(_('filesystem containing %s was unmounted\n') %
313 313 evt.fullpath)
314 314 sys.exit(0)
315 315
316 316 def handle_pollevents(self, events):
317 317 if self.ui.debugflag:
318 318 self.ui.note(_('%s readable: %d bytes\n') %
319 319 (self.event_time(), self.threshold.readable()))
320 320 if not self.threshold():
321 321 if self.registered:
322 322 if self.ui.debugflag:
323 323 self.ui.note(_('%s below threshold - unhooking\n') %
324 324 (self.event_time()))
325 325 self.unregister()
326 326 self.timeout = 250
327 327 else:
328 328 self.read_events()
329 329
330 330 def read_events(self, bufsize=None):
331 331 events = self.watcher.read(bufsize)
332 332 if self.ui.debugflag:
333 333 self.ui.note(_('%s reading %d events\n') %
334 334 (self.event_time(), len(events)))
335 335 for evt in events:
336 336 if evt.fullpath == self.wprefix[:-1]:
337 337 # events on the root of the repository
338 338 # itself, e.g. permission changes or repository move
339 339 continue
340 340 assert evt.fullpath.startswith(self.wprefix)
341 341 wpath = evt.fullpath[self.prefixlen:]
342 342
343 343 # paths have been normalized, wpath never ends with a '/'
344 344
345 345 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
346 346 # ignore subdirectories of .hg/ (merge, patches...)
347 347 continue
348 348 if wpath == ".hg/wlock":
349 349 if evt.mask & inotify.IN_DELETE:
350 350 self.dirstate.invalidate()
351 351 self.dirty = False
352 352 self.scan()
353 353 elif evt.mask & inotify.IN_CREATE:
354 354 self.dirty = True
355 355 else:
356 356 if self.dirty:
357 357 continue
358 358
359 359 if evt.mask & inotify.IN_UNMOUNT:
360 360 self.process_unmount(wpath, evt)
361 361 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
362 362 self.process_modify(wpath, evt)
363 363 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
364 364 inotify.IN_MOVED_FROM):
365 365 self.process_delete(wpath, evt)
366 366 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
367 367 self.process_create(wpath, evt)
368 368
369 369 self.lastevent.clear()
370 370
371 371 def handle_timeout(self):
372 372 if not self.registered:
373 373 if self.ui.debugflag:
374 374 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
375 375 (self.event_time(), self.threshold.readable()))
376 376 self.read_events(0)
377 377 self.register(timeout=None)
378 378
379 379 self.timeout = None
380 380
381 381 def shutdown(self):
382 382 self.watcher.close()
383 383
384 384 def debug(self):
385 385 """
386 386 Returns a sorted list of relatives paths currently watched,
387 387 for debugging purposes.
388 388 """
389 389 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
390 390
391 391 class socketlistener(server.socketlistener, pollable):
392 392 """
393 393 Listens for client queries on unix socket inotify.sock
394 394 """
395 395 def __init__(self, ui, root, repowatcher, timeout):
396 396 server.socketlistener.__init__(self, ui, root, repowatcher, timeout)
397 397 self.register(timeout=timeout)
398 398
399 399 def handle_timeout(self):
400 400 raise server.TimeoutException
401 401
402 402 def handle_pollevents(self, events):
403 403 for e in events:
404 404 self.accept_connection()
405 405
406 406 def shutdown(self):
407 407 self.sock.close()
408 try:
409 os.unlink(self.sockpath)
410 if self.realsockpath:
411 os.unlink(self.realsockpath)
412 os.rmdir(os.path.dirname(self.realsockpath))
413 except OSError, err:
414 if err.errno != errno.ENOENT:
415 raise
408 self.sock.cleanup()
416 409
417 410 def answer_stat_query(self, cs):
418 411 if self.repowatcher.timeout:
419 412 # We got a query while a rescan is pending. Make sure we
420 413 # rescan before responding, or we could give back a wrong
421 414 # answer.
422 415 self.repowatcher.handle_timeout()
423 416 return server.socketlistener.answer_stat_query(self, cs)
424 417
425 418 class master(object):
426 419 def __init__(self, ui, dirstate, root, timeout=None):
427 420 self.ui = ui
428 421 self.repowatcher = repowatcher(ui, dirstate, root)
429 422 self.socketlistener = socketlistener(ui, root, self.repowatcher,
430 423 timeout)
431 424
432 425 def shutdown(self):
433 426 for obj in pollable.instances.itervalues():
434 427 try:
435 428 obj.shutdown()
436 429 except error.SignalInterrupt:
437 430 pass
438 431
439 432 def run(self):
440 433 self.repowatcher.setup()
441 434 self.ui.note(_('finished setup\n'))
442 435 if os.getenv('TIME_STARTUP'):
443 436 sys.exit(0)
444 437 pollable.run()
@@ -1,491 +1,465
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 from mercurial import cmdutil, osutil, util
9 from mercurial import cmdutil, posix, 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 self.sock = socket.socket(socket.AF_UNIX)
334 self.sockpath = join(root, '.hg/inotify.sock')
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 os.unlink(self.sockpath)
342 333 try:
343 self.sock.bind(self.realsockpath)
344 except socket.error, err:
345 if err.args[0] == errno.EADDRINUSE:
346 raise AlreadyStartedException(_('cannot start: socket is '
347 'already bound'))
348 if err.args[0] == "AF_UNIX path too long":
349 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
350 self.realsockpath = os.path.join(tempdir, "inotify.sock")
351 try:
352 self.sock.bind(self.realsockpath)
353 os.symlink(self.realsockpath, self.sockpath)
354 except (OSError, socket.error), inst:
355 try:
356 os.unlink(self.realsockpath)
357 except OSError:
358 pass
359 os.rmdir(tempdir)
360 if inst.errno == errno.EEXIST:
361 raise AlreadyStartedException(_('cannot start: tried '
362 'linking .hg/inotify.sock to a temporary socket but'
363 ' .hg/inotify.sock already exists'))
364 raise
365 else:
366 raise
367 self.sock.listen(5)
334 self.sock = posix.unixdomainserver(
335 lambda p: os.path.join(root, '.hg', p),
336 'inotify')
337 except (OSError, socket.error), err:
338 if err.errno == errno.EADDRINUSE:
339 raise AlreadyStartedException(_('cannot start: '
340 'socket is already bound'))
341 raise
368 342 self.fileno = self.sock.fileno
369 343
370 344 def answer_stat_query(self, cs):
371 345 names = cs.read().split('\0')
372 346
373 347 states = names.pop()
374 348
375 349 self.ui.note(_('answering query for %r\n') % states)
376 350
377 351 visited = set()
378 352 if not names:
379 353 def genresult(states, tree):
380 354 for fn, state in tree.walk(states):
381 355 yield fn
382 356 else:
383 357 def genresult(states, tree):
384 358 for fn in names:
385 359 for f in tree.lookup(states, fn, visited):
386 360 yield f
387 361
388 362 return ['\0'.join(r) for r in [
389 363 genresult('l', self.repowatcher.statustrees['l']),
390 364 genresult('m', self.repowatcher.statustrees['m']),
391 365 genresult('a', self.repowatcher.statustrees['a']),
392 366 genresult('r', self.repowatcher.statustrees['r']),
393 367 genresult('!', self.repowatcher.statustrees['!']),
394 368 '?' in states
395 369 and genresult('?', self.repowatcher.statustrees['?'])
396 370 or [],
397 371 [],
398 372 'c' in states and genresult('n', self.repowatcher.tree) or [],
399 373 visited
400 374 ]]
401 375
402 376 def answer_dbug_query(self):
403 377 return ['\0'.join(self.repowatcher.debug())]
404 378
405 379 def accept_connection(self):
406 380 sock, addr = self.sock.accept()
407 381
408 382 cs = common.recvcs(sock)
409 383 version = ord(cs.read(1))
410 384
411 385 if version != common.version:
412 386 self.ui.warn(_('received query from incompatible client '
413 387 'version %d\n') % version)
414 388 try:
415 389 # try to send back our version to the client
416 390 # this way, the client too is informed of the mismatch
417 391 sock.sendall(chr(common.version))
418 392 except socket.error:
419 393 pass
420 394 return
421 395
422 396 type = cs.read(4)
423 397
424 398 if type == 'STAT':
425 399 results = self.answer_stat_query(cs)
426 400 elif type == 'DBUG':
427 401 results = self.answer_dbug_query()
428 402 else:
429 403 self.ui.warn(_('unrecognized query type: %s\n') % type)
430 404 return
431 405
432 406 try:
433 407 try:
434 408 v = chr(common.version)
435 409
436 410 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
437 411 *map(len, results)))
438 412 sock.sendall(''.join(results))
439 413 finally:
440 414 sock.shutdown(socket.SHUT_WR)
441 415 except socket.error, err:
442 416 if err.args[0] != errno.EPIPE:
443 417 raise
444 418
445 419 if sys.platform.startswith('linux'):
446 420 import linuxserver as _server
447 421 else:
448 422 raise ImportError
449 423
450 424 master = _server.master
451 425
452 426 def start(ui, dirstate, root, opts):
453 427 timeout = opts.get('idle_timeout')
454 428 if timeout:
455 429 timeout = float(timeout) * 60000
456 430 else:
457 431 timeout = None
458 432
459 433 class service(object):
460 434 def init(self):
461 435 try:
462 436 self.master = master(ui, dirstate, root, timeout)
463 437 except AlreadyStartedException, inst:
464 438 raise util.Abort("inotify-server: %s" % inst)
465 439
466 440 def run(self):
467 441 try:
468 442 try:
469 443 self.master.run()
470 444 except TimeoutException:
471 445 pass
472 446 finally:
473 447 self.master.shutdown()
474 448
475 449 if 'inserve' not in sys.argv:
476 450 runargs = util.hgcmd() + ['inserve', '-R', root]
477 451 else:
478 452 runargs = util.hgcmd() + sys.argv[1:]
479 453
480 454 pidfile = ui.config('inotify', 'pidfile')
481 455 if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
482 456 runargs.append("--pid-file=%s" % pidfile)
483 457
484 458 service = service()
485 459 logfile = ui.config('inotify', 'log')
486 460
487 461 appendpid = ui.configbool('inotify', 'appendpid', False)
488 462
489 463 ui.debug('starting inotify server: %s\n' % ' '.join(runargs))
490 464 cmdutil.service(opts, initfn=service.init, runfn=service.run,
491 465 logfile=logfile, runargs=runargs, appendpid=appendpid)
@@ -1,485 +1,525
1 1 # posix.py - Posix utility function implementations for Mercurial
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 i18n import _
9 9 import encoding
10 import os, sys, errno, stat, getpass, pwd, grp, tempfile, unicodedata
10 import os, sys, errno, stat, getpass, pwd, grp, socket, tempfile, unicodedata
11 11
12 12 posixfile = open
13 13 normpath = os.path.normpath
14 14 samestat = os.path.samestat
15 15 oslink = os.link
16 16 unlink = os.unlink
17 17 rename = os.rename
18 18 expandglobs = False
19 19
20 20 umask = os.umask(0)
21 21 os.umask(umask)
22 22
23 23 def split(p):
24 24 '''Same as os.path.split, but faster'''
25 25 ht = p.rsplit('/', 1)
26 26 if len(ht) == 1:
27 27 return '', p
28 28 nh = ht[0].rstrip('/')
29 29 if nh:
30 30 return nh, ht[1]
31 31 return ht
32 32
33 33 def openhardlinks():
34 34 '''return true if it is safe to hold open file handles to hardlinks'''
35 35 return True
36 36
37 37 def nlinks(name):
38 38 '''return number of hardlinks for the given file'''
39 39 return os.lstat(name).st_nlink
40 40
41 41 def parsepatchoutput(output_line):
42 42 """parses the output produced by patch and returns the filename"""
43 43 pf = output_line[14:]
44 44 if os.sys.platform == 'OpenVMS':
45 45 if pf[0] == '`':
46 46 pf = pf[1:-1] # Remove the quotes
47 47 else:
48 48 if pf.startswith("'") and pf.endswith("'") and " " in pf:
49 49 pf = pf[1:-1] # Remove the quotes
50 50 return pf
51 51
52 52 def sshargs(sshcmd, host, user, port):
53 53 '''Build argument list for ssh'''
54 54 args = user and ("%s@%s" % (user, host)) or host
55 55 return port and ("%s -p %s" % (args, port)) or args
56 56
57 57 def isexec(f):
58 58 """check whether a file is executable"""
59 59 return (os.lstat(f).st_mode & 0100 != 0)
60 60
61 61 def setflags(f, l, x):
62 62 s = os.lstat(f).st_mode
63 63 if l:
64 64 if not stat.S_ISLNK(s):
65 65 # switch file to link
66 66 fp = open(f)
67 67 data = fp.read()
68 68 fp.close()
69 69 os.unlink(f)
70 70 try:
71 71 os.symlink(data, f)
72 72 except OSError:
73 73 # failed to make a link, rewrite file
74 74 fp = open(f, "w")
75 75 fp.write(data)
76 76 fp.close()
77 77 # no chmod needed at this point
78 78 return
79 79 if stat.S_ISLNK(s):
80 80 # switch link to file
81 81 data = os.readlink(f)
82 82 os.unlink(f)
83 83 fp = open(f, "w")
84 84 fp.write(data)
85 85 fp.close()
86 86 s = 0666 & ~umask # avoid restatting for chmod
87 87
88 88 sx = s & 0100
89 89 if x and not sx:
90 90 # Turn on +x for every +r bit when making a file executable
91 91 # and obey umask.
92 92 os.chmod(f, s | (s & 0444) >> 2 & ~umask)
93 93 elif not x and sx:
94 94 # Turn off all +x bits
95 95 os.chmod(f, s & 0666)
96 96
97 97 def copymode(src, dst, mode=None):
98 98 '''Copy the file mode from the file at path src to dst.
99 99 If src doesn't exist, we're using mode instead. If mode is None, we're
100 100 using umask.'''
101 101 try:
102 102 st_mode = os.lstat(src).st_mode & 0777
103 103 except OSError, inst:
104 104 if inst.errno != errno.ENOENT:
105 105 raise
106 106 st_mode = mode
107 107 if st_mode is None:
108 108 st_mode = ~umask
109 109 st_mode &= 0666
110 110 os.chmod(dst, st_mode)
111 111
112 112 def checkexec(path):
113 113 """
114 114 Check whether the given path is on a filesystem with UNIX-like exec flags
115 115
116 116 Requires a directory (like /foo/.hg)
117 117 """
118 118
119 119 # VFAT on some Linux versions can flip mode but it doesn't persist
120 120 # a FS remount. Frequently we can detect it if files are created
121 121 # with exec bit on.
122 122
123 123 try:
124 124 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
125 125 fh, fn = tempfile.mkstemp(dir=path, prefix='hg-checkexec-')
126 126 try:
127 127 os.close(fh)
128 128 m = os.stat(fn).st_mode & 0777
129 129 new_file_has_exec = m & EXECFLAGS
130 130 os.chmod(fn, m ^ EXECFLAGS)
131 131 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m)
132 132 finally:
133 133 os.unlink(fn)
134 134 except (IOError, OSError):
135 135 # we don't care, the user probably won't be able to commit anyway
136 136 return False
137 137 return not (new_file_has_exec or exec_flags_cannot_flip)
138 138
139 139 def checklink(path):
140 140 """check whether the given path is on a symlink-capable filesystem"""
141 141 # mktemp is not racy because symlink creation will fail if the
142 142 # file already exists
143 143 name = tempfile.mktemp(dir=path, prefix='hg-checklink-')
144 144 try:
145 145 os.symlink(".", name)
146 146 os.unlink(name)
147 147 return True
148 148 except (OSError, AttributeError):
149 149 return False
150 150
151 151 def checkosfilename(path):
152 152 '''Check that the base-relative path is a valid filename on this platform.
153 153 Returns None if the path is ok, or a UI string describing the problem.'''
154 154 pass # on posix platforms, every path is ok
155 155
156 156 def setbinary(fd):
157 157 pass
158 158
159 159 def pconvert(path):
160 160 return path
161 161
162 162 def localpath(path):
163 163 return path
164 164
165 165 def samefile(fpath1, fpath2):
166 166 """Returns whether path1 and path2 refer to the same file. This is only
167 167 guaranteed to work for files, not directories."""
168 168 return os.path.samefile(fpath1, fpath2)
169 169
170 170 def samedevice(fpath1, fpath2):
171 171 """Returns whether fpath1 and fpath2 are on the same device. This is only
172 172 guaranteed to work for files, not directories."""
173 173 st1 = os.lstat(fpath1)
174 174 st2 = os.lstat(fpath2)
175 175 return st1.st_dev == st2.st_dev
176 176
177 177 # os.path.normcase is a no-op, which doesn't help us on non-native filesystems
178 178 def normcase(path):
179 179 return path.lower()
180 180
181 181 if sys.platform == 'darwin':
182 182 import fcntl # only needed on darwin, missing on jython
183 183
184 184 def normcase(path):
185 185 try:
186 186 u = path.decode('utf-8')
187 187 except UnicodeDecodeError:
188 188 # percent-encode any characters that don't round-trip
189 189 p2 = path.decode('utf-8', 'ignore').encode('utf-8')
190 190 s = ""
191 191 pos = 0
192 192 for c in path:
193 193 if p2[pos:pos + 1] == c:
194 194 s += c
195 195 pos += 1
196 196 else:
197 197 s += "%%%02X" % ord(c)
198 198 u = s.decode('utf-8')
199 199
200 200 # Decompose then lowercase (HFS+ technote specifies lower)
201 201 return unicodedata.normalize('NFD', u).lower().encode('utf-8')
202 202
203 203 def realpath(path):
204 204 '''
205 205 Returns the true, canonical file system path equivalent to the given
206 206 path.
207 207
208 208 Equivalent means, in this case, resulting in the same, unique
209 209 file system link to the path. Every file system entry, whether a file,
210 210 directory, hard link or symbolic link or special, will have a single
211 211 path preferred by the system, but may allow multiple, differing path
212 212 lookups to point to it.
213 213
214 214 Most regular UNIX file systems only allow a file system entry to be
215 215 looked up by its distinct path. Obviously, this does not apply to case
216 216 insensitive file systems, whether case preserving or not. The most
217 217 complex issue to deal with is file systems transparently reencoding the
218 218 path, such as the non-standard Unicode normalisation required for HFS+
219 219 and HFSX.
220 220 '''
221 221 # Constants copied from /usr/include/sys/fcntl.h
222 222 F_GETPATH = 50
223 223 O_SYMLINK = 0x200000
224 224
225 225 try:
226 226 fd = os.open(path, O_SYMLINK)
227 227 except OSError, err:
228 228 if err.errno == errno.ENOENT:
229 229 return path
230 230 raise
231 231
232 232 try:
233 233 return fcntl.fcntl(fd, F_GETPATH, '\0' * 1024).rstrip('\0')
234 234 finally:
235 235 os.close(fd)
236 236 elif sys.version_info < (2, 4, 2, 'final'):
237 237 # Workaround for http://bugs.python.org/issue1213894 (os.path.realpath
238 238 # didn't resolve symlinks that were the first component of the path.)
239 239 def realpath(path):
240 240 if os.path.isabs(path):
241 241 return os.path.realpath(path)
242 242 else:
243 243 return os.path.realpath('./' + path)
244 244 else:
245 245 # Fallback to the likely inadequate Python builtin function.
246 246 realpath = os.path.realpath
247 247
248 248 if sys.platform == 'cygwin':
249 249 # workaround for cygwin, in which mount point part of path is
250 250 # treated as case sensitive, even though underlying NTFS is case
251 251 # insensitive.
252 252
253 253 # default mount points
254 254 cygwinmountpoints = sorted([
255 255 "/usr/bin",
256 256 "/usr/lib",
257 257 "/cygdrive",
258 258 ], reverse=True)
259 259
260 260 # use upper-ing as normcase as same as NTFS workaround
261 261 def normcase(path):
262 262 pathlen = len(path)
263 263 if (pathlen == 0) or (path[0] != os.sep):
264 264 # treat as relative
265 265 return encoding.upper(path)
266 266
267 267 # to preserve case of mountpoint part
268 268 for mp in cygwinmountpoints:
269 269 if not path.startswith(mp):
270 270 continue
271 271
272 272 mplen = len(mp)
273 273 if mplen == pathlen: # mount point itself
274 274 return mp
275 275 if path[mplen] == os.sep:
276 276 return mp + encoding.upper(path[mplen:])
277 277
278 278 return encoding.upper(path)
279 279
280 280 # Cygwin translates native ACLs to POSIX permissions,
281 281 # but these translations are not supported by native
282 282 # tools, so the exec bit tends to be set erroneously.
283 283 # Therefore, disable executable bit access on Cygwin.
284 284 def checkexec(path):
285 285 return False
286 286
287 287 # Similarly, Cygwin's symlink emulation is likely to create
288 288 # problems when Mercurial is used from both Cygwin and native
289 289 # Windows, with other native tools, or on shared volumes
290 290 def checklink(path):
291 291 return False
292 292
293 293 def shellquote(s):
294 294 if os.sys.platform == 'OpenVMS':
295 295 return '"%s"' % s
296 296 else:
297 297 return "'%s'" % s.replace("'", "'\\''")
298 298
299 299 def quotecommand(cmd):
300 300 return cmd
301 301
302 302 def popen(command, mode='r'):
303 303 return os.popen(command, mode)
304 304
305 305 def testpid(pid):
306 306 '''return False if pid dead, True if running or not sure'''
307 307 if os.sys.platform == 'OpenVMS':
308 308 return True
309 309 try:
310 310 os.kill(pid, 0)
311 311 return True
312 312 except OSError, inst:
313 313 return inst.errno != errno.ESRCH
314 314
315 315 def explainexit(code):
316 316 """return a 2-tuple (desc, code) describing a subprocess status
317 317 (codes from kill are negative - not os.system/wait encoding)"""
318 318 if code >= 0:
319 319 return _("exited with status %d") % code, code
320 320 return _("killed by signal %d") % -code, -code
321 321
322 322 def isowner(st):
323 323 """Return True if the stat object st is from the current user."""
324 324 return st.st_uid == os.getuid()
325 325
326 326 def findexe(command):
327 327 '''Find executable for command searching like which does.
328 328 If command is a basename then PATH is searched for command.
329 329 PATH isn't searched if command is an absolute or relative path.
330 330 If command isn't found None is returned.'''
331 331 if sys.platform == 'OpenVMS':
332 332 return command
333 333
334 334 def findexisting(executable):
335 335 'Will return executable if existing file'
336 336 if os.path.isfile(executable) and os.access(executable, os.X_OK):
337 337 return executable
338 338 return None
339 339
340 340 if os.sep in command:
341 341 return findexisting(command)
342 342
343 343 if sys.platform == 'plan9':
344 344 return findexisting(os.path.join('/bin', command))
345 345
346 346 for path in os.environ.get('PATH', '').split(os.pathsep):
347 347 executable = findexisting(os.path.join(path, command))
348 348 if executable is not None:
349 349 return executable
350 350 return None
351 351
352 352 def setsignalhandler():
353 353 pass
354 354
355 355 _wantedkinds = set([stat.S_IFREG, stat.S_IFLNK])
356 356
357 357 def statfiles(files):
358 358 '''Stat each file in files. Yield each stat, or None if a file does not
359 359 exist or has a type we don't care about.'''
360 360 lstat = os.lstat
361 361 getkind = stat.S_IFMT
362 362 for nf in files:
363 363 try:
364 364 st = lstat(nf)
365 365 if getkind(st.st_mode) not in _wantedkinds:
366 366 st = None
367 367 except OSError, err:
368 368 if err.errno not in (errno.ENOENT, errno.ENOTDIR):
369 369 raise
370 370 st = None
371 371 yield st
372 372
373 373 def getuser():
374 374 '''return name of current user'''
375 375 return getpass.getuser()
376 376
377 377 def username(uid=None):
378 378 """Return the name of the user with the given uid.
379 379
380 380 If uid is None, return the name of the current user."""
381 381
382 382 if uid is None:
383 383 uid = os.getuid()
384 384 try:
385 385 return pwd.getpwuid(uid)[0]
386 386 except KeyError:
387 387 return str(uid)
388 388
389 389 def groupname(gid=None):
390 390 """Return the name of the group with the given gid.
391 391
392 392 If gid is None, return the name of the current group."""
393 393
394 394 if gid is None:
395 395 gid = os.getgid()
396 396 try:
397 397 return grp.getgrgid(gid)[0]
398 398 except KeyError:
399 399 return str(gid)
400 400
401 401 def groupmembers(name):
402 402 """Return the list of members of the group with the given
403 403 name, KeyError if the group does not exist.
404 404 """
405 405 return list(grp.getgrnam(name).gr_mem)
406 406
407 407 def spawndetached(args):
408 408 return os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
409 409 args[0], args)
410 410
411 411 def gethgcmd():
412 412 return sys.argv[:1]
413 413
414 414 def termwidth():
415 415 try:
416 416 import termios, array, fcntl
417 417 for dev in (sys.stderr, sys.stdout, sys.stdin):
418 418 try:
419 419 try:
420 420 fd = dev.fileno()
421 421 except AttributeError:
422 422 continue
423 423 if not os.isatty(fd):
424 424 continue
425 425 try:
426 426 arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
427 427 width = array.array('h', arri)[1]
428 428 if width > 0:
429 429 return width
430 430 except AttributeError:
431 431 pass
432 432 except ValueError:
433 433 pass
434 434 except IOError, e:
435 435 if e[0] == errno.EINVAL:
436 436 pass
437 437 else:
438 438 raise
439 439 except ImportError:
440 440 pass
441 441 return 80
442 442
443 443 def makedir(path, notindexed):
444 444 os.mkdir(path)
445 445
446 446 def unlinkpath(f):
447 447 """unlink and remove the directory if it is empty"""
448 448 os.unlink(f)
449 449 # try removing directories that might now be empty
450 450 try:
451 451 os.removedirs(os.path.dirname(f))
452 452 except OSError:
453 453 pass
454 454
455 455 def lookupreg(key, name=None, scope=None):
456 456 return None
457 457
458 458 def hidewindow():
459 459 """Hide current shell window.
460 460
461 461 Used to hide the window opened when starting asynchronous
462 462 child process under Windows, unneeded on other systems.
463 463 """
464 464 pass
465 465
466 466 class cachestat(object):
467 467 def __init__(self, path):
468 468 self.stat = os.stat(path)
469 469
470 470 def cacheable(self):
471 471 return bool(self.stat.st_ino)
472 472
473 473 __hash__ = object.__hash__
474 474
475 475 def __eq__(self, other):
476 476 try:
477 477 return self.stat == other.stat
478 478 except AttributeError:
479 479 return False
480 480
481 481 def __ne__(self, other):
482 482 return not self == other
483 483
484 484 def executablepath():
485 485 return None # available on Windows only
486
487 class unixdomainserver(socket.socket):
488 def __init__(self, join, subsystem):
489 '''Create a unix domain socket with the given prefix.'''
490 super(unixdomainserver, self).__init__(socket.AF_UNIX)
491 sockname = subsystem + '.sock'
492 self.realpath = self.path = join(sockname)
493 if os.path.islink(self.path):
494 if os.path.exists(self.path):
495 self.realpath = os.readlink(self.path)
496 else:
497 os.unlink(self.path)
498 try:
499 self.bind(self.realpath)
500 except socket.error, err:
501 if err.args[0] == 'AF_UNIX path too long':
502 tmpdir = tempfile.mkdtemp(prefix='hg-%s-' % subsystem)
503 self.realpath = os.path.join(tmpdir, sockname)
504 try:
505 self.bind(self.realpath)
506 os.symlink(self.realpath, self.path)
507 except (OSError, socket.error):
508 self.cleanup()
509 raise
510 else:
511 raise
512 self.listen(5)
513
514 def cleanup(self):
515 def okayifmissing(f, path):
516 try:
517 f(path)
518 except OSError, err:
519 if err.errno != errno.ENOENT:
520 raise
521
522 okayifmissing(os.unlink, self.path)
523 if self.realpath != self.path:
524 okayifmissing(os.unlink, self.realpath)
525 okayifmissing(os.rmdir, os.path.dirname(self.realpath))
General Comments 0
You need to be logged in to leave comments. Login now