Mercurial > hg-stable
comparison mercurial/debugcommands.py @ 36557:72e487851a53
debugcommands: add debugwireproto command
We currently don't have a low-level mechanism for sending
arbitrary wire protocol commands. Having a generic and robust
mechanism for sending wire protocol commands, examining wire
data, etc would make it vastly easier to test the wire protocol
and debug server operation. This is a problem I've wanted a
solution for numerous times, especially recently as I've been
hacking on a new version of the wire protocol.
This commit establishes a `hg debugwireproto` command for sending
data to a peer.
The command invents a mini language for specifying actions to take.
This will enable a lot of flexibility for issuing commands and testing
variations for how commands are sent.
Right now, we only support low-level raw sends and receives. These
are probably the least valuable commands to intended users of this
command. But they are the most useful commands to implement to
bootstrap the feature (I've chosen to reimplement test-ssh-proto.t
using this command to prove its usefulness).
My eventual goal of `hg debugwireproto` is to allow calling wire
protocol commands with a human-friendly interface. Essentially,
people can type in a command name and arguments and
`hg debugwireproto` will figure out how to send that on the wire.
I'd love to eventually be able to save the server's raw response
to a file. This would allow us to e.g. call "getbundle" wire
protocol commands easily.
test-ssh-proto.t has been updated to use the new command in lieu
of piping directly to a server process. As part of the transition,
test behavior improved. Before, we piped all request data to the
server at once. Now, we have explicit control over the ordering of
operations. e.g. we can send one command, receive its response,
then send another command. This will allow us to more robustly
test race conditions, buffering behavior, etc.
There were some subtle changes in test behavior. For example,
previous behavior would often send trailing newlines to the server.
The new mechanism doesn't treat literal newlines specially and
requires newlines be escaped in the payload.
Because the new logging code is very low level, it is easy to
introduce race conditions in tests. For example, the number of bytes
returned by a read() may vary depending on load. This is why tests
make heavy use of "readline" for consuming data: the result of
that operation should be deterministic and not subject to race
conditions. There are still some uses of "readavailable." However,
those are only for reading from stderr. I was able to reproduce
timing issues with my system under load when using "readavailable"
globally. But if I "readline" to grab stdout, "readavailable"
appears to work deterministically for stderr. I think this is
because the server writes to stderr first. As long as the OS
delivers writes to pipes in the same order they were made, this
should work. If there are timing issues, we can introduce a
mechanism to readline from stderr.
Differential Revision: https://phab.mercurial-scm.org/D2392
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Thu, 01 Mar 2018 08:24:54 -0800 |
parents | 44dc34b8d17b |
children | bde0bd50f368 |
comparison
equal
deleted
inserted
replaced
36556:44dc34b8d17b | 36557:72e487851a53 |
---|---|
15 import os | 15 import os |
16 import random | 16 import random |
17 import socket | 17 import socket |
18 import ssl | 18 import ssl |
19 import string | 19 import string |
20 import subprocess | |
20 import sys | 21 import sys |
21 import tempfile | 22 import tempfile |
22 import time | 23 import time |
23 | 24 |
24 from .i18n import _ | 25 from .i18n import _ |
63 revsetlang, | 64 revsetlang, |
64 scmutil, | 65 scmutil, |
65 setdiscovery, | 66 setdiscovery, |
66 simplemerge, | 67 simplemerge, |
67 smartset, | 68 smartset, |
69 sshpeer, | |
68 sslutil, | 70 sslutil, |
69 streamclone, | 71 streamclone, |
70 templater, | 72 templater, |
71 treediscovery, | 73 treediscovery, |
72 upgrade, | 74 upgrade, |
2527 res1 = repo.debugwireargs(*vals, **args) | 2529 res1 = repo.debugwireargs(*vals, **args) |
2528 res2 = repo.debugwireargs(*vals, **args) | 2530 res2 = repo.debugwireargs(*vals, **args) |
2529 ui.write("%s\n" % res1) | 2531 ui.write("%s\n" % res1) |
2530 if res1 != res2: | 2532 if res1 != res2: |
2531 ui.warn("%s\n" % res2) | 2533 ui.warn("%s\n" % res2) |
2534 | |
2535 def _parsewirelangblocks(fh): | |
2536 activeaction = None | |
2537 blocklines = [] | |
2538 | |
2539 for line in fh: | |
2540 line = line.rstrip() | |
2541 if not line: | |
2542 continue | |
2543 | |
2544 if line.startswith(b'#'): | |
2545 continue | |
2546 | |
2547 if not line.startswith(' '): | |
2548 # New block. Flush previous one. | |
2549 if activeaction: | |
2550 yield activeaction, blocklines | |
2551 | |
2552 activeaction = line | |
2553 blocklines = [] | |
2554 continue | |
2555 | |
2556 # Else we start with an indent. | |
2557 | |
2558 if not activeaction: | |
2559 raise error.Abort(_('indented line outside of block')) | |
2560 | |
2561 blocklines.append(line) | |
2562 | |
2563 # Flush last block. | |
2564 if activeaction: | |
2565 yield activeaction, blocklines | |
2566 | |
2567 @command('debugwireproto', | |
2568 [ | |
2569 ('', 'localssh', False, _('start an SSH server for this repo')), | |
2570 ('', 'peer', '', _('construct a specific version of the peer')), | |
2571 ] + cmdutil.remoteopts, | |
2572 _('[REPO]'), | |
2573 optionalrepo=True) | |
2574 def debugwireproto(ui, repo, **opts): | |
2575 """send wire protocol commands to a server | |
2576 | |
2577 This command can be used to issue wire protocol commands to remote | |
2578 peers and to debug the raw data being exchanged. | |
2579 | |
2580 ``--localssh`` will start an SSH server against the current repository | |
2581 and connect to that. By default, the connection will perform a handshake | |
2582 and establish an appropriate peer instance. | |
2583 | |
2584 ``--peer`` can be used to bypass the handshake protocol and construct a | |
2585 peer instance using the specified class type. Valid values are ``raw``, | |
2586 ``ssh1``, and ``ssh2``. ``raw`` instances only allow sending raw data | |
2587 payloads and don't support higher-level command actions. | |
2588 | |
2589 Commands are issued via a mini language which is specified via stdin. | |
2590 The language consists of individual actions to perform. An action is | |
2591 defined by a block. A block is defined as a line with no leading | |
2592 space followed by 0 or more lines with leading space. Blocks are | |
2593 effectively a high-level command with additional metadata. | |
2594 | |
2595 Lines beginning with ``#`` are ignored. | |
2596 | |
2597 The following sections denote available actions. | |
2598 | |
2599 raw | |
2600 --- | |
2601 | |
2602 Send raw data to the server. | |
2603 | |
2604 The block payload contains the raw data to send as one atomic send | |
2605 operation. The data may not actually be delivered in a single system | |
2606 call: it depends on the abilities of the transport being used. | |
2607 | |
2608 Each line in the block is de-indented and concatenated. Then, that | |
2609 value is evaluated as a Python b'' literal. This allows the use of | |
2610 backslash escaping, etc. | |
2611 | |
2612 raw+ | |
2613 ---- | |
2614 | |
2615 Behaves like ``raw`` except flushes output afterwards. | |
2616 | |
2617 close | |
2618 ----- | |
2619 | |
2620 Close the connection to the server. | |
2621 | |
2622 flush | |
2623 ----- | |
2624 | |
2625 Flush data written to the server. | |
2626 | |
2627 readavailable | |
2628 ------------- | |
2629 | |
2630 Read all available data from the server. | |
2631 | |
2632 If the connection to the server encompasses multiple pipes, we poll both | |
2633 pipes and read available data. | |
2634 | |
2635 readline | |
2636 -------- | |
2637 | |
2638 Read a line of output from the server. If there are multiple output | |
2639 pipes, reads only the main pipe. | |
2640 """ | |
2641 opts = pycompat.byteskwargs(opts) | |
2642 | |
2643 if opts['localssh'] and not repo: | |
2644 raise error.Abort(_('--localssh requires a repository')) | |
2645 | |
2646 if opts['peer'] and opts['peer'] not in ('raw', 'ssh1', 'ssh2'): | |
2647 raise error.Abort(_('invalid value for --peer'), | |
2648 hint=_('valid values are "raw", "ssh1", and "ssh2"')) | |
2649 | |
2650 if ui.interactive(): | |
2651 ui.write(_('(waiting for commands on stdin)\n')) | |
2652 | |
2653 blocks = list(_parsewirelangblocks(ui.fin)) | |
2654 | |
2655 proc = None | |
2656 | |
2657 if opts['localssh']: | |
2658 # We start the SSH server in its own process so there is process | |
2659 # separation. This prevents a whole class of potential bugs around | |
2660 # shared state from interfering with server operation. | |
2661 args = util.hgcmd() + [ | |
2662 '-R', repo.root, | |
2663 'debugserve', '--sshstdio', | |
2664 ] | |
2665 proc = subprocess.Popen(args, stdin=subprocess.PIPE, | |
2666 stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
2667 bufsize=0) | |
2668 | |
2669 stdin = proc.stdin | |
2670 stdout = proc.stdout | |
2671 stderr = proc.stderr | |
2672 | |
2673 # We turn the pipes into observers so we can log I/O. | |
2674 if ui.verbose or opts['peer'] == 'raw': | |
2675 stdin = util.makeloggingfileobject(ui, proc.stdin, b'i', | |
2676 logdata=True) | |
2677 stdout = util.makeloggingfileobject(ui, proc.stdout, b'o', | |
2678 logdata=True) | |
2679 stderr = util.makeloggingfileobject(ui, proc.stderr, b'e', | |
2680 logdata=True) | |
2681 | |
2682 # --localssh also implies the peer connection settings. | |
2683 | |
2684 url = 'ssh://localserver' | |
2685 | |
2686 if opts['peer'] == 'ssh1': | |
2687 ui.write(_('creating ssh peer for wire protocol version 1\n')) | |
2688 peer = sshpeer.sshv1peer(ui, url, proc, stdin, stdout, stderr, | |
2689 None) | |
2690 elif opts['peer'] == 'ssh2': | |
2691 ui.write(_('creating ssh peer for wire protocol version 2\n')) | |
2692 peer = sshpeer.sshv2peer(ui, url, proc, stdin, stdout, stderr, | |
2693 None) | |
2694 elif opts['peer'] == 'raw': | |
2695 ui.write(_('using raw connection to peer\n')) | |
2696 peer = None | |
2697 else: | |
2698 ui.write(_('creating ssh peer from handshake results\n')) | |
2699 peer = sshpeer.makepeer(ui, url, proc, stdin, stdout, stderr) | |
2700 | |
2701 else: | |
2702 raise error.Abort(_('only --localssh is currently supported')) | |
2703 | |
2704 # Now perform actions based on the parsed wire language instructions. | |
2705 for action, lines in blocks: | |
2706 if action in ('raw', 'raw+'): | |
2707 # Concatenate the data together. | |
2708 data = ''.join(l.lstrip() for l in lines) | |
2709 data = util.unescapestr(data) | |
2710 stdin.write(data) | |
2711 | |
2712 if action == 'raw+': | |
2713 stdin.flush() | |
2714 elif action == 'flush': | |
2715 stdin.flush() | |
2716 elif action == 'close': | |
2717 peer.close() | |
2718 elif action == 'readavailable': | |
2719 fds = util.poll([stdout.fileno(), stderr.fileno()]) | |
2720 | |
2721 if stdout.fileno() in fds: | |
2722 util.readpipe(stdout) | |
2723 if stderr.fileno() in fds: | |
2724 util.readpipe(stderr) | |
2725 elif action == 'readline': | |
2726 stdout.readline() | |
2727 else: | |
2728 raise error.Abort(_('unknown action: %s') % action) | |
2729 | |
2730 if peer: | |
2731 peer.close() | |
2732 | |
2733 if proc: | |
2734 proc.kill() |