##// END OF EJS Templates
fsmonitor: refactor watchmanclient.client to accept ui and repo path...
Augie Fackler -
r42877:ab190032 default
parent child Browse files
Show More
@@ -1,832 +1,832 b''
1 1 # __init__.py - fsmonitor initialization and overrides
2 2 #
3 3 # Copyright 2013-2016 Facebook, Inc.
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 '''Faster status operations with the Watchman file monitor (EXPERIMENTAL)
9 9
10 10 Integrates the file-watching program Watchman with Mercurial to produce faster
11 11 status results.
12 12
13 13 On a particular Linux system, for a real-world repository with over 400,000
14 14 files hosted on ext4, vanilla `hg status` takes 1.3 seconds. On the same
15 15 system, with fsmonitor it takes about 0.3 seconds.
16 16
17 17 fsmonitor requires no configuration -- it will tell Watchman about your
18 18 repository as necessary. You'll need to install Watchman from
19 19 https://facebook.github.io/watchman/ and make sure it is in your PATH.
20 20
21 21 fsmonitor is incompatible with the largefiles and eol extensions, and
22 22 will disable itself if any of those are active.
23 23
24 24 The following configuration options exist:
25 25
26 26 ::
27 27
28 28 [fsmonitor]
29 29 mode = {off, on, paranoid}
30 30
31 31 When `mode = off`, fsmonitor will disable itself (similar to not loading the
32 32 extension at all). When `mode = on`, fsmonitor will be enabled (the default).
33 33 When `mode = paranoid`, fsmonitor will query both Watchman and the filesystem,
34 34 and ensure that the results are consistent.
35 35
36 36 ::
37 37
38 38 [fsmonitor]
39 39 timeout = (float)
40 40
41 41 A value, in seconds, that determines how long fsmonitor will wait for Watchman
42 42 to return results. Defaults to `2.0`.
43 43
44 44 ::
45 45
46 46 [fsmonitor]
47 47 blacklistusers = (list of userids)
48 48
49 49 A list of usernames for which fsmonitor will disable itself altogether.
50 50
51 51 ::
52 52
53 53 [fsmonitor]
54 54 walk_on_invalidate = (boolean)
55 55
56 56 Whether or not to walk the whole repo ourselves when our cached state has been
57 57 invalidated, for example when Watchman has been restarted or .hgignore rules
58 58 have been changed. Walking the repo in that case can result in competing for
59 59 I/O with Watchman. For large repos it is recommended to set this value to
60 60 false. You may wish to set this to true if you have a very fast filesystem
61 61 that can outpace the IPC overhead of getting the result data for the full repo
62 62 from Watchman. Defaults to false.
63 63
64 64 ::
65 65
66 66 [fsmonitor]
67 67 warn_when_unused = (boolean)
68 68
69 69 Whether to print a warning during certain operations when fsmonitor would be
70 70 beneficial to performance but isn't enabled.
71 71
72 72 ::
73 73
74 74 [fsmonitor]
75 75 warn_update_file_count = (integer)
76 76
77 77 If ``warn_when_unused`` is set and fsmonitor isn't enabled, a warning will
78 78 be printed during working directory updates if this many files will be
79 79 created.
80 80 '''
81 81
82 82 # Platforms Supported
83 83 # ===================
84 84 #
85 85 # **Linux:** *Stable*. Watchman and fsmonitor are both known to work reliably,
86 86 # even under severe loads.
87 87 #
88 88 # **Mac OS X:** *Stable*. The Mercurial test suite passes with fsmonitor
89 89 # turned on, on case-insensitive HFS+. There has been a reasonable amount of
90 90 # user testing under normal loads.
91 91 #
92 92 # **Solaris, BSD:** *Alpha*. watchman and fsmonitor are believed to work, but
93 93 # very little testing has been done.
94 94 #
95 95 # **Windows:** *Alpha*. Not in a release version of watchman or fsmonitor yet.
96 96 #
97 97 # Known Issues
98 98 # ============
99 99 #
100 100 # * fsmonitor will disable itself if any of the following extensions are
101 101 # enabled: largefiles, inotify, eol; or if the repository has subrepos.
102 102 # * fsmonitor will produce incorrect results if nested repos that are not
103 103 # subrepos exist. *Workaround*: add nested repo paths to your `.hgignore`.
104 104 #
105 105 # The issues related to nested repos and subrepos are probably not fundamental
106 106 # ones. Patches to fix them are welcome.
107 107
108 108 from __future__ import absolute_import
109 109
110 110 import codecs
111 111 import hashlib
112 112 import os
113 113 import stat
114 114 import sys
115 115 import weakref
116 116
117 117 from mercurial.i18n import _
118 118 from mercurial.node import (
119 119 hex,
120 120 )
121 121
122 122 from mercurial import (
123 123 context,
124 124 encoding,
125 125 error,
126 126 extensions,
127 127 localrepo,
128 128 merge,
129 129 pathutil,
130 130 pycompat,
131 131 registrar,
132 132 scmutil,
133 133 util,
134 134 )
135 135 from mercurial import match as matchmod
136 136
137 137 from . import (
138 138 pywatchman,
139 139 state,
140 140 watchmanclient,
141 141 )
142 142
143 143 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
144 144 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
145 145 # be specifying the version(s) of Mercurial they are tested with, or
146 146 # leave the attribute unspecified.
147 147 testedwith = 'ships-with-hg-core'
148 148
149 149 configtable = {}
150 150 configitem = registrar.configitem(configtable)
151 151
152 152 configitem('fsmonitor', 'mode',
153 153 default='on',
154 154 )
155 155 configitem('fsmonitor', 'walk_on_invalidate',
156 156 default=False,
157 157 )
158 158 configitem('fsmonitor', 'timeout',
159 159 default='2',
160 160 )
161 161 configitem('fsmonitor', 'blacklistusers',
162 162 default=list,
163 163 )
164 164 configitem('fsmonitor', 'watchman_exe',
165 165 default='watchman',
166 166 )
167 167 configitem('fsmonitor', 'verbose',
168 168 default=True,
169 169 )
170 170 configitem('experimental', 'fsmonitor.transaction_notify',
171 171 default=False,
172 172 )
173 173
174 174 # This extension is incompatible with the following blacklisted extensions
175 175 # and will disable itself when encountering one of these:
176 176 _blacklist = ['largefiles', 'eol']
177 177
178 178 def _handleunavailable(ui, state, ex):
179 179 """Exception handler for Watchman interaction exceptions"""
180 180 if isinstance(ex, watchmanclient.Unavailable):
181 181 # experimental config: fsmonitor.verbose
182 182 if ex.warn and ui.configbool('fsmonitor', 'verbose'):
183 183 if 'illegal_fstypes' not in str(ex):
184 184 ui.warn(str(ex) + '\n')
185 185 if ex.invalidate:
186 186 state.invalidate()
187 187 # experimental config: fsmonitor.verbose
188 188 if ui.configbool('fsmonitor', 'verbose'):
189 189 ui.log('fsmonitor', 'Watchman unavailable: %s\n', ex.msg)
190 190 else:
191 191 ui.log('fsmonitor', 'Watchman exception: %s\n', ex)
192 192
193 193 def _hashignore(ignore):
194 194 """Calculate hash for ignore patterns and filenames
195 195
196 196 If this information changes between Mercurial invocations, we can't
197 197 rely on Watchman information anymore and have to re-scan the working
198 198 copy.
199 199
200 200 """
201 201 sha1 = hashlib.sha1()
202 202 sha1.update(repr(ignore))
203 203 return sha1.hexdigest()
204 204
205 205 _watchmanencoding = pywatchman.encoding.get_local_encoding()
206 206 _fsencoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
207 207 _fixencoding = codecs.lookup(_watchmanencoding) != codecs.lookup(_fsencoding)
208 208
209 209 def _watchmantofsencoding(path):
210 210 """Fix path to match watchman and local filesystem encoding
211 211
212 212 watchman's paths encoding can differ from filesystem encoding. For example,
213 213 on Windows, it's always utf-8.
214 214 """
215 215 try:
216 216 decoded = path.decode(_watchmanencoding)
217 217 except UnicodeDecodeError as e:
218 218 raise error.Abort(str(e), hint='watchman encoding error')
219 219
220 220 try:
221 221 encoded = decoded.encode(_fsencoding, 'strict')
222 222 except UnicodeEncodeError as e:
223 223 raise error.Abort(str(e))
224 224
225 225 return encoded
226 226
227 227 def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True):
228 228 '''Replacement for dirstate.walk, hooking into Watchman.
229 229
230 230 Whenever full is False, ignored is False, and the Watchman client is
231 231 available, use Watchman combined with saved state to possibly return only a
232 232 subset of files.'''
233 233 def bail(reason):
234 234 self._ui.debug('fsmonitor: fallback to core status, %s\n' % reason)
235 235 return orig(match, subrepos, unknown, ignored, full=True)
236 236
237 237 if full:
238 238 return bail('full rewalk requested')
239 239 if ignored:
240 240 return bail('listing ignored files')
241 241 if not self._watchmanclient.available():
242 242 return bail('client unavailable')
243 243 state = self._fsmonitorstate
244 244 clock, ignorehash, notefiles = state.get()
245 245 if not clock:
246 246 if state.walk_on_invalidate:
247 247 return bail('no clock')
248 248 # Initial NULL clock value, see
249 249 # https://facebook.github.io/watchman/docs/clockspec.html
250 250 clock = 'c:0:0'
251 251 notefiles = []
252 252
253 253 ignore = self._ignore
254 254 dirignore = self._dirignore
255 255 if unknown:
256 256 if _hashignore(ignore) != ignorehash and clock != 'c:0:0':
257 257 # ignore list changed -- can't rely on Watchman state any more
258 258 if state.walk_on_invalidate:
259 259 return bail('ignore rules changed')
260 260 notefiles = []
261 261 clock = 'c:0:0'
262 262 else:
263 263 # always ignore
264 264 ignore = util.always
265 265 dirignore = util.always
266 266
267 267 matchfn = match.matchfn
268 268 matchalways = match.always()
269 269 dmap = self._map
270 270 if util.safehasattr(dmap, '_map'):
271 271 # for better performance, directly access the inner dirstate map if the
272 272 # standard dirstate implementation is in use.
273 273 dmap = dmap._map
274 274 nonnormalset = self._map.nonnormalset
275 275
276 276 copymap = self._map.copymap
277 277 getkind = stat.S_IFMT
278 278 dirkind = stat.S_IFDIR
279 279 regkind = stat.S_IFREG
280 280 lnkkind = stat.S_IFLNK
281 281 join = self._join
282 282 normcase = util.normcase
283 283 fresh_instance = False
284 284
285 285 exact = skipstep3 = False
286 286 if match.isexact(): # match.exact
287 287 exact = True
288 288 dirignore = util.always # skip step 2
289 289 elif match.prefix(): # match.match, no patterns
290 290 skipstep3 = True
291 291
292 292 if not exact and self._checkcase:
293 293 # note that even though we could receive directory entries, we're only
294 294 # interested in checking if a file with the same name exists. So only
295 295 # normalize files if possible.
296 296 normalize = self._normalizefile
297 297 skipstep3 = False
298 298 else:
299 299 normalize = None
300 300
301 301 # step 1: find all explicit files
302 302 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
303 303
304 304 skipstep3 = skipstep3 and not (work or dirsnotfound)
305 305 work = [d for d in work if not dirignore(d[0])]
306 306
307 307 if not work and (exact or skipstep3):
308 308 for s in subrepos:
309 309 del results[s]
310 310 del results['.hg']
311 311 return results
312 312
313 313 # step 2: query Watchman
314 314 try:
315 315 # Use the user-configured timeout for the query.
316 316 # Add a little slack over the top of the user query to allow for
317 317 # overheads while transferring the data
318 318 self._watchmanclient.settimeout(state.timeout + 0.1)
319 319 result = self._watchmanclient.command('query', {
320 320 'fields': ['mode', 'mtime', 'size', 'exists', 'name'],
321 321 'since': clock,
322 322 'expression': [
323 323 'not', [
324 324 'anyof', ['dirname', '.hg'],
325 325 ['name', '.hg', 'wholename']
326 326 ]
327 327 ],
328 328 'sync_timeout': int(state.timeout * 1000),
329 329 'empty_on_fresh_instance': state.walk_on_invalidate,
330 330 })
331 331 except Exception as ex:
332 332 _handleunavailable(self._ui, state, ex)
333 333 self._watchmanclient.clearconnection()
334 334 return bail('exception during run')
335 335 else:
336 336 # We need to propagate the last observed clock up so that we
337 337 # can use it for our next query
338 338 state.setlastclock(result['clock'])
339 339 if result['is_fresh_instance']:
340 340 if state.walk_on_invalidate:
341 341 state.invalidate()
342 342 return bail('fresh instance')
343 343 fresh_instance = True
344 344 # Ignore any prior noteable files from the state info
345 345 notefiles = []
346 346
347 347 # for file paths which require normalization and we encounter a case
348 348 # collision, we store our own foldmap
349 349 if normalize:
350 350 foldmap = dict((normcase(k), k) for k in results)
351 351
352 352 switch_slashes = pycompat.ossep == '\\'
353 353 # The order of the results is, strictly speaking, undefined.
354 354 # For case changes on a case insensitive filesystem we may receive
355 355 # two entries, one with exists=True and another with exists=False.
356 356 # The exists=True entries in the same response should be interpreted
357 357 # as being happens-after the exists=False entries due to the way that
358 358 # Watchman tracks files. We use this property to reconcile deletes
359 359 # for name case changes.
360 360 for entry in result['files']:
361 361 fname = entry['name']
362 362 if _fixencoding:
363 363 fname = _watchmantofsencoding(fname)
364 364 if switch_slashes:
365 365 fname = fname.replace('\\', '/')
366 366 if normalize:
367 367 normed = normcase(fname)
368 368 fname = normalize(fname, True, True)
369 369 foldmap[normed] = fname
370 370 fmode = entry['mode']
371 371 fexists = entry['exists']
372 372 kind = getkind(fmode)
373 373
374 374 if '/.hg/' in fname or fname.endswith('/.hg'):
375 375 return bail('nested-repo-detected')
376 376
377 377 if not fexists:
378 378 # if marked as deleted and we don't already have a change
379 379 # record, mark it as deleted. If we already have an entry
380 380 # for fname then it was either part of walkexplicit or was
381 381 # an earlier result that was a case change
382 382 if fname not in results and fname in dmap and (
383 383 matchalways or matchfn(fname)):
384 384 results[fname] = None
385 385 elif kind == dirkind:
386 386 if fname in dmap and (matchalways or matchfn(fname)):
387 387 results[fname] = None
388 388 elif kind == regkind or kind == lnkkind:
389 389 if fname in dmap:
390 390 if matchalways or matchfn(fname):
391 391 results[fname] = entry
392 392 elif (matchalways or matchfn(fname)) and not ignore(fname):
393 393 results[fname] = entry
394 394 elif fname in dmap and (matchalways or matchfn(fname)):
395 395 results[fname] = None
396 396
397 397 # step 3: query notable files we don't already know about
398 398 # XXX try not to iterate over the entire dmap
399 399 if normalize:
400 400 # any notable files that have changed case will already be handled
401 401 # above, so just check membership in the foldmap
402 402 notefiles = set((normalize(f, True, True) for f in notefiles
403 403 if normcase(f) not in foldmap))
404 404 visit = set((f for f in notefiles if (f not in results and matchfn(f)
405 405 and (f in dmap or not ignore(f)))))
406 406
407 407 if not fresh_instance:
408 408 if matchalways:
409 409 visit.update(f for f in nonnormalset if f not in results)
410 410 visit.update(f for f in copymap if f not in results)
411 411 else:
412 412 visit.update(f for f in nonnormalset
413 413 if f not in results and matchfn(f))
414 414 visit.update(f for f in copymap
415 415 if f not in results and matchfn(f))
416 416 else:
417 417 if matchalways:
418 418 visit.update(f for f, st in dmap.iteritems() if f not in results)
419 419 visit.update(f for f in copymap if f not in results)
420 420 else:
421 421 visit.update(f for f, st in dmap.iteritems()
422 422 if f not in results and matchfn(f))
423 423 visit.update(f for f in copymap
424 424 if f not in results and matchfn(f))
425 425
426 426 audit = pathutil.pathauditor(self._root, cached=True).check
427 427 auditpass = [f for f in visit if audit(f)]
428 428 auditpass.sort()
429 429 auditfail = visit.difference(auditpass)
430 430 for f in auditfail:
431 431 results[f] = None
432 432
433 433 nf = iter(auditpass).next
434 434 for st in util.statfiles([join(f) for f in auditpass]):
435 435 f = nf()
436 436 if st or f in dmap:
437 437 results[f] = st
438 438
439 439 for s in subrepos:
440 440 del results[s]
441 441 del results['.hg']
442 442 return results
443 443
444 444 def overridestatus(
445 445 orig, self, node1='.', node2=None, match=None, ignored=False,
446 446 clean=False, unknown=False, listsubrepos=False):
447 447 listignored = ignored
448 448 listclean = clean
449 449 listunknown = unknown
450 450
451 451 def _cmpsets(l1, l2):
452 452 try:
453 453 if 'FSMONITOR_LOG_FILE' in encoding.environ:
454 454 fn = encoding.environ['FSMONITOR_LOG_FILE']
455 455 f = open(fn, 'wb')
456 456 else:
457 457 fn = 'fsmonitorfail.log'
458 458 f = self.vfs.open(fn, 'wb')
459 459 except (IOError, OSError):
460 460 self.ui.warn(_('warning: unable to write to %s\n') % fn)
461 461 return
462 462
463 463 try:
464 464 for i, (s1, s2) in enumerate(zip(l1, l2)):
465 465 if set(s1) != set(s2):
466 466 f.write('sets at position %d are unequal\n' % i)
467 467 f.write('watchman returned: %s\n' % s1)
468 468 f.write('stat returned: %s\n' % s2)
469 469 finally:
470 470 f.close()
471 471
472 472 if isinstance(node1, context.changectx):
473 473 ctx1 = node1
474 474 else:
475 475 ctx1 = self[node1]
476 476 if isinstance(node2, context.changectx):
477 477 ctx2 = node2
478 478 else:
479 479 ctx2 = self[node2]
480 480
481 481 working = ctx2.rev() is None
482 482 parentworking = working and ctx1 == self['.']
483 483 match = match or matchmod.always()
484 484
485 485 # Maybe we can use this opportunity to update Watchman's state.
486 486 # Mercurial uses workingcommitctx and/or memctx to represent the part of
487 487 # the workingctx that is to be committed. So don't update the state in
488 488 # that case.
489 489 # HG_PENDING is set in the environment when the dirstate is being updated
490 490 # in the middle of a transaction; we must not update our state in that
491 491 # case, or we risk forgetting about changes in the working copy.
492 492 updatestate = (parentworking and match.always() and
493 493 not isinstance(ctx2, (context.workingcommitctx,
494 494 context.memctx)) and
495 495 'HG_PENDING' not in encoding.environ)
496 496
497 497 try:
498 498 if self._fsmonitorstate.walk_on_invalidate:
499 499 # Use a short timeout to query the current clock. If that
500 500 # takes too long then we assume that the service will be slow
501 501 # to answer our query.
502 502 # walk_on_invalidate indicates that we prefer to walk the
503 503 # tree ourselves because we can ignore portions that Watchman
504 504 # cannot and we tend to be faster in the warmer buffer cache
505 505 # cases.
506 506 self._watchmanclient.settimeout(0.1)
507 507 else:
508 508 # Give Watchman more time to potentially complete its walk
509 509 # and return the initial clock. In this mode we assume that
510 510 # the filesystem will be slower than parsing a potentially
511 511 # very large Watchman result set.
512 512 self._watchmanclient.settimeout(
513 513 self._fsmonitorstate.timeout + 0.1)
514 514 startclock = self._watchmanclient.getcurrentclock()
515 515 except Exception as ex:
516 516 self._watchmanclient.clearconnection()
517 517 _handleunavailable(self.ui, self._fsmonitorstate, ex)
518 518 # boo, Watchman failed. bail
519 519 return orig(node1, node2, match, listignored, listclean,
520 520 listunknown, listsubrepos)
521 521
522 522 if updatestate:
523 523 # We need info about unknown files. This may make things slower the
524 524 # first time, but whatever.
525 525 stateunknown = True
526 526 else:
527 527 stateunknown = listunknown
528 528
529 529 if updatestate:
530 530 ps = poststatus(startclock)
531 531 self.addpostdsstatus(ps)
532 532
533 533 r = orig(node1, node2, match, listignored, listclean, stateunknown,
534 534 listsubrepos)
535 535 modified, added, removed, deleted, unknown, ignored, clean = r
536 536
537 537 if not listunknown:
538 538 unknown = []
539 539
540 540 # don't do paranoid checks if we're not going to query Watchman anyway
541 541 full = listclean or match.traversedir is not None
542 542 if self._fsmonitorstate.mode == 'paranoid' and not full:
543 543 # run status again and fall back to the old walk this time
544 544 self.dirstate._fsmonitordisable = True
545 545
546 546 # shut the UI up
547 547 quiet = self.ui.quiet
548 548 self.ui.quiet = True
549 549 fout, ferr = self.ui.fout, self.ui.ferr
550 550 self.ui.fout = self.ui.ferr = open(os.devnull, 'wb')
551 551
552 552 try:
553 553 rv2 = orig(
554 554 node1, node2, match, listignored, listclean, listunknown,
555 555 listsubrepos)
556 556 finally:
557 557 self.dirstate._fsmonitordisable = False
558 558 self.ui.quiet = quiet
559 559 self.ui.fout, self.ui.ferr = fout, ferr
560 560
561 561 # clean isn't tested since it's set to True above
562 562 with self.wlock():
563 563 _cmpsets(
564 564 [modified, added, removed, deleted, unknown, ignored, clean],
565 565 rv2)
566 566 modified, added, removed, deleted, unknown, ignored, clean = rv2
567 567
568 568 return scmutil.status(
569 569 modified, added, removed, deleted, unknown, ignored, clean)
570 570
571 571 class poststatus(object):
572 572 def __init__(self, startclock):
573 573 self._startclock = startclock
574 574
575 575 def __call__(self, wctx, status):
576 576 clock = wctx.repo()._fsmonitorstate.getlastclock() or self._startclock
577 577 hashignore = _hashignore(wctx.repo().dirstate._ignore)
578 578 notefiles = (status.modified + status.added + status.removed +
579 579 status.deleted + status.unknown)
580 580 wctx.repo()._fsmonitorstate.set(clock, hashignore, notefiles)
581 581
582 582 def makedirstate(repo, dirstate):
583 583 class fsmonitordirstate(dirstate.__class__):
584 584 def _fsmonitorinit(self, repo):
585 585 # _fsmonitordisable is used in paranoid mode
586 586 self._fsmonitordisable = False
587 587 self._fsmonitorstate = repo._fsmonitorstate
588 588 self._watchmanclient = repo._watchmanclient
589 589 self._repo = weakref.proxy(repo)
590 590
591 591 def walk(self, *args, **kwargs):
592 592 orig = super(fsmonitordirstate, self).walk
593 593 if self._fsmonitordisable:
594 594 return orig(*args, **kwargs)
595 595 return overridewalk(orig, self, *args, **kwargs)
596 596
597 597 def rebuild(self, *args, **kwargs):
598 598 self._fsmonitorstate.invalidate()
599 599 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
600 600
601 601 def invalidate(self, *args, **kwargs):
602 602 self._fsmonitorstate.invalidate()
603 603 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
604 604
605 605 dirstate.__class__ = fsmonitordirstate
606 606 dirstate._fsmonitorinit(repo)
607 607
608 608 def wrapdirstate(orig, self):
609 609 ds = orig(self)
610 610 # only override the dirstate when Watchman is available for the repo
611 611 if util.safehasattr(self, '_fsmonitorstate'):
612 612 makedirstate(self, ds)
613 613 return ds
614 614
615 615 def extsetup(ui):
616 616 extensions.wrapfilecache(
617 617 localrepo.localrepository, 'dirstate', wrapdirstate)
618 618 if pycompat.isdarwin:
619 619 # An assist for avoiding the dangling-symlink fsevents bug
620 620 extensions.wrapfunction(os, 'symlink', wrapsymlink)
621 621
622 622 extensions.wrapfunction(merge, 'update', wrapupdate)
623 623
624 624 def wrapsymlink(orig, source, link_name):
625 625 ''' if we create a dangling symlink, also touch the parent dir
626 626 to encourage fsevents notifications to work more correctly '''
627 627 try:
628 628 return orig(source, link_name)
629 629 finally:
630 630 try:
631 631 os.utime(os.path.dirname(link_name), None)
632 632 except OSError:
633 633 pass
634 634
635 635 class state_update(object):
636 636 ''' This context manager is responsible for dispatching the state-enter
637 637 and state-leave signals to the watchman service. The enter and leave
638 638 methods can be invoked manually (for scenarios where context manager
639 639 semantics are not possible). If parameters oldnode and newnode are None,
640 640 they will be populated based on current working copy in enter and
641 641 leave, respectively. Similarly, if the distance is none, it will be
642 642 calculated based on the oldnode and newnode in the leave method.'''
643 643
644 644 def __init__(self, repo, name, oldnode=None, newnode=None, distance=None,
645 645 partial=False):
646 646 self.repo = repo.unfiltered()
647 647 self.name = name
648 648 self.oldnode = oldnode
649 649 self.newnode = newnode
650 650 self.distance = distance
651 651 self.partial = partial
652 652 self._lock = None
653 653 self.need_leave = False
654 654
655 655 def __enter__(self):
656 656 self.enter()
657 657
658 658 def enter(self):
659 659 # Make sure we have a wlock prior to sending notifications to watchman.
660 660 # We don't want to race with other actors. In the update case,
661 661 # merge.update is going to take the wlock almost immediately. We are
662 662 # effectively extending the lock around several short sanity checks.
663 663 if self.oldnode is None:
664 664 self.oldnode = self.repo['.'].node()
665 665
666 666 if self.repo.currentwlock() is None:
667 667 if util.safehasattr(self.repo, 'wlocknostateupdate'):
668 668 self._lock = self.repo.wlocknostateupdate()
669 669 else:
670 670 self._lock = self.repo.wlock()
671 671 self.need_leave = self._state(
672 672 'state-enter',
673 673 hex(self.oldnode))
674 674 return self
675 675
676 676 def __exit__(self, type_, value, tb):
677 677 abort = True if type_ else False
678 678 self.exit(abort=abort)
679 679
680 680 def exit(self, abort=False):
681 681 try:
682 682 if self.need_leave:
683 683 status = 'failed' if abort else 'ok'
684 684 if self.newnode is None:
685 685 self.newnode = self.repo['.'].node()
686 686 if self.distance is None:
687 687 self.distance = calcdistance(
688 688 self.repo, self.oldnode, self.newnode)
689 689 self._state(
690 690 'state-leave',
691 691 hex(self.newnode),
692 692 status=status)
693 693 finally:
694 694 self.need_leave = False
695 695 if self._lock:
696 696 self._lock.release()
697 697
698 698 def _state(self, cmd, commithash, status='ok'):
699 699 if not util.safehasattr(self.repo, '_watchmanclient'):
700 700 return False
701 701 try:
702 702 self.repo._watchmanclient.command(cmd, {
703 703 'name': self.name,
704 704 'metadata': {
705 705 # the target revision
706 706 'rev': commithash,
707 707 # approximate number of commits between current and target
708 708 'distance': self.distance if self.distance else 0,
709 709 # success/failure (only really meaningful for state-leave)
710 710 'status': status,
711 711 # whether the working copy parent is changing
712 712 'partial': self.partial,
713 713 }})
714 714 return True
715 715 except Exception as e:
716 716 # Swallow any errors; fire and forget
717 717 self.repo.ui.log(
718 718 'watchman', 'Exception %s while running %s\n', e, cmd)
719 719 return False
720 720
721 721 # Estimate the distance between two nodes
722 722 def calcdistance(repo, oldnode, newnode):
723 723 anc = repo.changelog.ancestor(oldnode, newnode)
724 724 ancrev = repo[anc].rev()
725 725 distance = (abs(repo[oldnode].rev() - ancrev)
726 726 + abs(repo[newnode].rev() - ancrev))
727 727 return distance
728 728
729 729 # Bracket working copy updates with calls to the watchman state-enter
730 730 # and state-leave commands. This allows clients to perform more intelligent
731 731 # settling during bulk file change scenarios
732 732 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
733 733 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
734 734 mergeancestor=False, labels=None, matcher=None, **kwargs):
735 735
736 736 distance = 0
737 737 partial = True
738 738 oldnode = repo['.'].node()
739 739 newnode = repo[node].node()
740 740 if matcher is None or matcher.always():
741 741 partial = False
742 742 distance = calcdistance(repo.unfiltered(), oldnode, newnode)
743 743
744 744 with state_update(repo, name="hg.update", oldnode=oldnode, newnode=newnode,
745 745 distance=distance, partial=partial):
746 746 return orig(
747 747 repo, node, branchmerge, force, ancestor, mergeancestor,
748 748 labels, matcher, **kwargs)
749 749
750 750 def repo_has_depth_one_nested_repo(repo):
751 751 for f in repo.wvfs.listdir():
752 752 if os.path.isdir(os.path.join(repo.root, f, '.hg')):
753 753 msg = 'fsmonitor: sub-repository %r detected, fsmonitor disabled\n'
754 754 repo.ui.debug(msg % f)
755 755 return True
756 756 return False
757 757
758 758 def reposetup(ui, repo):
759 759 # We don't work with largefiles or inotify
760 760 exts = extensions.enabled()
761 761 for ext in _blacklist:
762 762 if ext in exts:
763 763 ui.warn(_('The fsmonitor extension is incompatible with the %s '
764 764 'extension and has been disabled.\n') % ext)
765 765 return
766 766
767 767 if repo.local():
768 768 # We don't work with subrepos either.
769 769 #
770 770 # if repo[None].substate can cause a dirstate parse, which is too
771 771 # slow. Instead, look for a file called hgsubstate,
772 772 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
773 773 return
774 774
775 775 if repo_has_depth_one_nested_repo(repo):
776 776 return
777 777
778 778 fsmonitorstate = state.state(repo)
779 779 if fsmonitorstate.mode == 'off':
780 780 return
781 781
782 782 try:
783 client = watchmanclient.client(repo)
783 client = watchmanclient.client(repo.ui, repo._root)
784 784 except Exception as ex:
785 785 _handleunavailable(ui, fsmonitorstate, ex)
786 786 return
787 787
788 788 repo._fsmonitorstate = fsmonitorstate
789 789 repo._watchmanclient = client
790 790
791 791 dirstate, cached = localrepo.isfilecached(repo, 'dirstate')
792 792 if cached:
793 793 # at this point since fsmonitorstate wasn't present,
794 794 # repo.dirstate is not a fsmonitordirstate
795 795 makedirstate(repo, dirstate)
796 796
797 797 class fsmonitorrepo(repo.__class__):
798 798 def status(self, *args, **kwargs):
799 799 orig = super(fsmonitorrepo, self).status
800 800 return overridestatus(orig, self, *args, **kwargs)
801 801
802 802 def wlocknostateupdate(self, *args, **kwargs):
803 803 return super(fsmonitorrepo, self).wlock(*args, **kwargs)
804 804
805 805 def wlock(self, *args, **kwargs):
806 806 l = super(fsmonitorrepo, self).wlock(*args, **kwargs)
807 807 if not ui.configbool(
808 808 "experimental", "fsmonitor.transaction_notify"):
809 809 return l
810 810 if l.held != 1:
811 811 return l
812 812 origrelease = l.releasefn
813 813
814 814 def staterelease():
815 815 if origrelease:
816 816 origrelease()
817 817 if l.stateupdate:
818 818 l.stateupdate.exit()
819 819 l.stateupdate = None
820 820
821 821 try:
822 822 l.stateupdate = None
823 823 l.stateupdate = state_update(self, name="hg.transaction")
824 824 l.stateupdate.enter()
825 825 l.releasefn = staterelease
826 826 except Exception as e:
827 827 # Swallow any errors; fire and forget
828 828 self.ui.log(
829 829 'watchman', 'Exception in state update %s\n', e)
830 830 return l
831 831
832 832 repo.__class__ = fsmonitorrepo
@@ -1,111 +1,111 b''
1 1 # watchmanclient.py - Watchman client for the fsmonitor extension
2 2 #
3 3 # Copyright 2013-2016 Facebook, Inc.
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 __future__ import absolute_import
9 9
10 10 import getpass
11 11
12 12 from mercurial import util
13 13
14 14 from . import pywatchman
15 15
16 16 class Unavailable(Exception):
17 17 def __init__(self, msg, warn=True, invalidate=False):
18 18 self.msg = msg
19 19 self.warn = warn
20 20 if self.msg == 'timed out waiting for response':
21 21 self.warn = False
22 22 self.invalidate = invalidate
23 23
24 24 def __str__(self):
25 25 if self.warn:
26 26 return 'warning: Watchman unavailable: %s' % self.msg
27 27 else:
28 28 return 'Watchman unavailable: %s' % self.msg
29 29
30 30 class WatchmanNoRoot(Unavailable):
31 31 def __init__(self, root, msg):
32 32 self.root = root
33 33 super(WatchmanNoRoot, self).__init__(msg)
34 34
35 35 class client(object):
36 def __init__(self, repo, timeout=1.0):
36 def __init__(self, ui, root, timeout=1.0):
37 37 err = None
38 38 if not self._user:
39 39 err = "couldn't get user"
40 40 warn = True
41 if self._user in repo.ui.configlist('fsmonitor', 'blacklistusers'):
41 if self._user in ui.configlist('fsmonitor', 'blacklistusers'):
42 42 err = 'user %s in blacklist' % self._user
43 43 warn = False
44 44
45 45 if err:
46 46 raise Unavailable(err, warn)
47 47
48 48 self._timeout = timeout
49 49 self._watchmanclient = None
50 self._root = repo.root
51 self._ui = repo.ui
50 self._root = root
51 self._ui = ui
52 52 self._firsttime = True
53 53
54 54 def settimeout(self, timeout):
55 55 self._timeout = timeout
56 56 if self._watchmanclient is not None:
57 57 self._watchmanclient.setTimeout(timeout)
58 58
59 59 def getcurrentclock(self):
60 60 result = self.command('clock')
61 61 if not util.safehasattr(result, 'clock'):
62 62 raise Unavailable('clock result is missing clock value',
63 63 invalidate=True)
64 64 return result.clock
65 65
66 66 def clearconnection(self):
67 67 self._watchmanclient = None
68 68
69 69 def available(self):
70 70 return self._watchmanclient is not None or self._firsttime
71 71
72 72 @util.propertycache
73 73 def _user(self):
74 74 try:
75 75 return getpass.getuser()
76 76 except KeyError:
77 77 # couldn't figure out our user
78 78 return None
79 79
80 80 def _command(self, *args):
81 81 watchmanargs = (args[0], self._root) + args[1:]
82 82 try:
83 83 if self._watchmanclient is None:
84 84 self._firsttime = False
85 85 watchman_exe = self._ui.configpath('fsmonitor', 'watchman_exe')
86 86 self._watchmanclient = pywatchman.client(
87 87 timeout=self._timeout,
88 88 useImmutableBser=True,
89 89 watchman_exe=watchman_exe)
90 90 return self._watchmanclient.query(*watchmanargs)
91 91 except pywatchman.CommandError as ex:
92 92 if 'unable to resolve root' in ex.msg:
93 93 raise WatchmanNoRoot(self._root, ex.msg)
94 94 raise Unavailable(ex.msg)
95 95 except pywatchman.WatchmanError as ex:
96 96 raise Unavailable(str(ex))
97 97
98 98 def command(self, *args):
99 99 try:
100 100 try:
101 101 return self._command(*args)
102 102 except WatchmanNoRoot:
103 103 # this 'watch' command can also raise a WatchmanNoRoot if
104 104 # watchman refuses to accept this root
105 105 self._command('watch')
106 106 return self._command(*args)
107 107 except Unavailable:
108 108 # this is in an outer scope to catch Unavailable form any of the
109 109 # above _command calls
110 110 self._watchmanclient = None
111 111 raise
General Comments 0
You need to be logged in to leave comments. Login now