Mercurial > public > mercurial-scm > hg
comparison mercurial/util.py @ 36524:bfe38f787d5b
util: add a file object proxy that can notify observers
There are various places in Mercurial where we may want to
instrument low-level I/O. The use cases I can think of all
involve development-type activities like monitoring the raw
bytes passing through a file (for testing and debugging),
counting the number of I/O function calls (for performance
monitoring), and changing the behavior of I/O function calls
(e.g. simulating a failure) (to facilitate testing).
This commit invents a mechanism to wrap a file object so we
can observe activity on it. We have similar functionality in
badserverext.py. But that's a test-only extension and is pretty
specific to the HTTP server. I would like a mechanism in core
that is sufficiently generic so it can be used by multiple
consumers, including `hg debug*` commands.
The added code consists of a proxy type for file objects.
It is bound to an "observer," which receives callbacks whenever
I/O methods are called.
We also add an implementation of an observer that logs specific
I/O events. This observer will be used in an upcoming commit
to record low-level wire protocol activity.
A helper function to convert a file object into an observed
file object has also been implemented.
I don't anticipate any critical functionality in core using
these types. So I don't think explicit test coverage is
worth implementing.
Differential Revision: https://phab.mercurial-scm.org/D2462
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Sat, 24 Feb 2018 12:22:20 -0800 |
parents | 1ca4e86c7265 |
children | 3158052720ae |
comparison
equal
deleted
inserted
replaced
36523:e7411fb7ba7f | 36524:bfe38f787d5b |
---|---|
485 stdin=subprocess.PIPE, stdout=subprocess.PIPE, | 485 stdin=subprocess.PIPE, stdout=subprocess.PIPE, |
486 stderr=subprocess.PIPE, | 486 stderr=subprocess.PIPE, |
487 universal_newlines=newlines, | 487 universal_newlines=newlines, |
488 env=env) | 488 env=env) |
489 return p.stdin, p.stdout, p.stderr, p | 489 return p.stdin, p.stdout, p.stderr, p |
490 | |
491 class fileobjectproxy(object): | |
492 """A proxy around file objects that tells a watcher when events occur. | |
493 | |
494 This type is intended to only be used for testing purposes. Think hard | |
495 before using it in important code. | |
496 """ | |
497 __slots__ = ( | |
498 r'_orig', | |
499 r'_observer', | |
500 ) | |
501 | |
502 def __init__(self, fh, observer): | |
503 object.__setattr__(self, '_orig', fh) | |
504 object.__setattr__(self, '_observer', observer) | |
505 | |
506 def __getattribute__(self, name): | |
507 ours = { | |
508 # IOBase | |
509 r'close', | |
510 # closed if a property | |
511 r'fileno', | |
512 r'flush', | |
513 r'isatty', | |
514 r'readable', | |
515 r'readline', | |
516 r'readlines', | |
517 r'seek', | |
518 r'seekable', | |
519 r'tell', | |
520 r'truncate', | |
521 r'writable', | |
522 r'writelines', | |
523 # RawIOBase | |
524 r'read', | |
525 r'readall', | |
526 r'readinto', | |
527 r'write', | |
528 # BufferedIOBase | |
529 # raw is a property | |
530 r'detach', | |
531 # read defined above | |
532 r'read1', | |
533 # readinto defined above | |
534 # write defined above | |
535 } | |
536 | |
537 # We only observe some methods. | |
538 if name in ours: | |
539 return object.__getattribute__(self, name) | |
540 | |
541 return getattr(object.__getattribute__(self, r'_orig'), name) | |
542 | |
543 def __delattr__(self, name): | |
544 return delattr(object.__getattribute__(self, r'_orig'), name) | |
545 | |
546 def __setattr__(self, name, value): | |
547 return setattr(object.__getattribute__(self, r'_orig'), name, value) | |
548 | |
549 def __iter__(self): | |
550 return object.__getattribute__(self, r'_orig').__iter__() | |
551 | |
552 def _observedcall(self, name, *args, **kwargs): | |
553 # Call the original object. | |
554 orig = object.__getattribute__(self, r'_orig') | |
555 res = getattr(orig, name)(*args, **kwargs) | |
556 | |
557 # Call a method on the observer of the same name with arguments | |
558 # so it can react, log, etc. | |
559 observer = object.__getattribute__(self, r'_observer') | |
560 fn = getattr(observer, name, None) | |
561 if fn: | |
562 fn(res, *args, **kwargs) | |
563 | |
564 return res | |
565 | |
566 def close(self, *args, **kwargs): | |
567 return object.__getattribute__(self, r'_observedcall')( | |
568 r'close', *args, **kwargs) | |
569 | |
570 def fileno(self, *args, **kwargs): | |
571 return object.__getattribute__(self, r'_observedcall')( | |
572 r'fileno', *args, **kwargs) | |
573 | |
574 def flush(self, *args, **kwargs): | |
575 return object.__getattribute__(self, r'_observedcall')( | |
576 r'flush', *args, **kwargs) | |
577 | |
578 def isatty(self, *args, **kwargs): | |
579 return object.__getattribute__(self, r'_observedcall')( | |
580 r'isatty', *args, **kwargs) | |
581 | |
582 def readable(self, *args, **kwargs): | |
583 return object.__getattribute__(self, r'_observedcall')( | |
584 r'readable', *args, **kwargs) | |
585 | |
586 def readline(self, *args, **kwargs): | |
587 return object.__getattribute__(self, r'_observedcall')( | |
588 r'readline', *args, **kwargs) | |
589 | |
590 def readlines(self, *args, **kwargs): | |
591 return object.__getattribute__(self, r'_observedcall')( | |
592 r'readlines', *args, **kwargs) | |
593 | |
594 def seek(self, *args, **kwargs): | |
595 return object.__getattribute__(self, r'_observedcall')( | |
596 r'seek', *args, **kwargs) | |
597 | |
598 def seekable(self, *args, **kwargs): | |
599 return object.__getattribute__(self, r'_observedcall')( | |
600 r'seekable', *args, **kwargs) | |
601 | |
602 def tell(self, *args, **kwargs): | |
603 return object.__getattribute__(self, r'_observedcall')( | |
604 r'tell', *args, **kwargs) | |
605 | |
606 def truncate(self, *args, **kwargs): | |
607 return object.__getattribute__(self, r'_observedcall')( | |
608 r'truncate', *args, **kwargs) | |
609 | |
610 def writable(self, *args, **kwargs): | |
611 return object.__getattribute__(self, r'_observedcall')( | |
612 r'writable', *args, **kwargs) | |
613 | |
614 def writelines(self, *args, **kwargs): | |
615 return object.__getattribute__(self, r'_observedcall')( | |
616 r'writelines', *args, **kwargs) | |
617 | |
618 def read(self, *args, **kwargs): | |
619 return object.__getattribute__(self, r'_observedcall')( | |
620 r'read', *args, **kwargs) | |
621 | |
622 def readall(self, *args, **kwargs): | |
623 return object.__getattribute__(self, r'_observedcall')( | |
624 r'readall', *args, **kwargs) | |
625 | |
626 def readinto(self, *args, **kwargs): | |
627 return object.__getattribute__(self, r'_observedcall')( | |
628 r'readinto', *args, **kwargs) | |
629 | |
630 def write(self, *args, **kwargs): | |
631 return object.__getattribute__(self, r'_observedcall')( | |
632 r'write', *args, **kwargs) | |
633 | |
634 def detach(self, *args, **kwargs): | |
635 return object.__getattribute__(self, r'_observedcall')( | |
636 r'detach', *args, **kwargs) | |
637 | |
638 def read1(self, *args, **kwargs): | |
639 return object.__getattribute__(self, r'_observedcall')( | |
640 r'read1', *args, **kwargs) | |
641 | |
642 DATA_ESCAPE_MAP = {pycompat.bytechr(i): br'\x%02x' % i for i in range(256)} | |
643 DATA_ESCAPE_MAP.update({ | |
644 b'\\': b'\\\\', | |
645 b'\r': br'\r', | |
646 b'\n': br'\n', | |
647 }) | |
648 DATA_ESCAPE_RE = remod.compile(br'[\x00-\x08\x0a-\x1f\\\x7f-\xff]') | |
649 | |
650 def escapedata(s): | |
651 return DATA_ESCAPE_RE.sub(lambda m: DATA_ESCAPE_MAP[m.group(0)], s) | |
652 | |
653 class fileobjectobserver(object): | |
654 """Logs file object activity.""" | |
655 def __init__(self, fh, name, reads=True, writes=True, logdata=False): | |
656 self.fh = fh | |
657 self.name = name | |
658 self.logdata = logdata | |
659 self.reads = reads | |
660 self.writes = writes | |
661 | |
662 def _writedata(self, data): | |
663 if not self.logdata: | |
664 self.fh.write('\n') | |
665 return | |
666 | |
667 # Simple case writes all data on a single line. | |
668 if b'\n' not in data: | |
669 self.fh.write(': %s\n' % escapedata(data)) | |
670 return | |
671 | |
672 # Data with newlines is written to multiple lines. | |
673 self.fh.write(':\n') | |
674 lines = data.splitlines(True) | |
675 for line in lines: | |
676 self.fh.write('%s> %s\n' % (self.name, escapedata(line))) | |
677 | |
678 def read(self, res, size=-1): | |
679 if not self.reads: | |
680 return | |
681 | |
682 self.fh.write('%s> read(%d) -> %d' % (self.name, size, len(res))) | |
683 self._writedata(res) | |
684 | |
685 def readline(self, res, limit=-1): | |
686 if not self.reads: | |
687 return | |
688 | |
689 self.fh.write('%s> readline() -> %d' % (self.name, len(res))) | |
690 self._writedata(res) | |
691 | |
692 def write(self, res, data): | |
693 if not self.writes: | |
694 return | |
695 | |
696 self.fh.write('%s> write(%d) -> %r' % (self.name, len(data), res)) | |
697 self._writedata(data) | |
698 | |
699 def flush(self, res): | |
700 if not self.writes: | |
701 return | |
702 | |
703 self.fh.write('%s> flush() -> %r\n' % (self.name, res)) | |
704 | |
705 def makeloggingfileobject(logh, fh, name, reads=True, writes=True, | |
706 logdata=False): | |
707 """Turn a file object into a logging file object.""" | |
708 | |
709 observer = fileobjectobserver(logh, name, reads=reads, writes=writes, | |
710 logdata=logdata) | |
711 return fileobjectproxy(fh, observer) | |
490 | 712 |
491 def version(): | 713 def version(): |
492 """Return version information if available.""" | 714 """Return version information if available.""" |
493 try: | 715 try: |
494 from . import __version__ | 716 from . import __version__ |