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()