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