D2620: wireproto: define and implement unified framing protocol for SSH

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
2 messages Options
Reply | Threaded
Open this post in threaded view
|

D2620: wireproto: define and implement unified framing protocol for SSH

indygreg (Gregory Szorc)
indygreg created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  THIS COMMIT IS INCOMPLETE AND TESTS STILL FAIL. DO NOT COMMIT.
 
  (This is probably one of the largest patches you'll ever see me write.
  Sorry about that: it is difficult to implement all of this
  functionality across multiple commits.)
 
  The existing HTTP and SSH wire protocols suffer from a host of flaws
  and shortcomings. I've been wanting to rewrite the protocol for a while
  now. Supporting partial clone - which will require new wire protocol
  commands and capabilities - and other advanced server functionality
  will be much easier if we start from a clean slate and don't have
  to be constrained by limitations of the existing wire protocol.
 
  This commit introduces a rewrite of the wire protocol. The current
  implementation is very, very, very far from what I want the final
  implementation to look like. It hasn't begun to tackle things like
  compression, multi part responses, various side-channels, etc.
 
  Anyway, the goal of this initial commit is to create a clean break
  from the existing SSH wire protocol so we have *something* better.
  And that *something* will gradually evolve over several backwards
  incompatible changes.
 
  As the updated internals documentation states, the new protocol
  attempts to be transport agnostic: all that's required to run the
  protocol is a pair of unidirectional, half-duplex pipes. While
  we have only implemented the protocol for SSH, we eventually want
  to "tunnel" this protocol over HTTP. (For HTTP I anticipate
  supporting both exchanging this protocol format via HTTP message
  bodies as well as leveraging the Upgrade header to switch from HTTP
  to the new frame-based protocol - thus effectively making HTTP
  connections behave identically to SSH. But this is for a future
  commit.) One of the differences with this protocol for SSH is
  it doesn't use stderr. This is by design: by limiting ourselves
  to only a pair of pipes, we make it possible to speak this protocol
  across any transport channel that supports ordered message delivery,
  including HTTP.
 
  The new protocol is built on top of "frames." A frame is an atomic
  unit (kind of like a TCP packet). It has a header and data payload.
  The header is currently extremely simple and will evolve significantly
  over time. The header records the size of the payload, the frame
  type, and per-type bit flags to denote frame behavior.
 
  The defined frame types are based on features of the existing
  wire protocol. You have frames to communicate a request to run a
  command: a command frame, argument frame, and command data frame.
  There are also frames to represent the response to that request:
  a raw data frame, an error frame, and frames for sending server
  output. I anticipate that the frame types will evolve heavily over
  time. It is difficult to do anything too radical at this stage because
  we need to concurrently support both the old and new protocols and
  the client and server APIs still need to evolve a ways before we
  can do crazier things.
 
  Because the new protocol aims to be transport agnostic, we have
  generic implementations for the peer and server. The SSH peer and
  server are very thin wrappers around the generic implementation.
 
  The server is implemented as a state machine of sorts. It basically
  sits around, waiting to receive a frame. Once it has received a
  request to run a command, it dispatches to the wire command handler
  and then sends a response. It is a very traditional half-duplex
  request-response protocol, not unlike HTTP/1.1. I anticipate
  evolving the protocol to support full-duplex operations. But that's
  way too complicated for the initial implementation.
 
  The new protocol is fully functional over SSH. Support for speaking
  this protocol over HTTP will be added later.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D2620

AFFECTED FILES
  mercurial/debugcommands.py
  mercurial/help/internals/wireprotocol.txt
  mercurial/sshpeer.py
  mercurial/wireprotoframing.py
  mercurial/wireprotoserver.py
  mercurial/wireprototypes.py
  tests/test-check-interfaces.py
  tests/test-clone.t
  tests/test-ssh-bundle1.t
  tests/test-ssh-proto-unbundle.t
  tests/test-ssh-proto.t
  tests/test-ssh.t

CHANGE DETAILS

diff --git a/tests/test-ssh.t b/tests/test-ssh.t
--- a/tests/test-ssh.t
+++ b/tests/test-ssh.t
@@ -490,14 +490,14 @@
   $ hg pull --debug ssh://user@dummy/remote --config devel.debug.peer-request=yes
   pulling from ssh://user@dummy/remote
   running .* ".*/dummyssh" ['"]user@dummy['"] ('|")hg -R remote serve --stdio('|") (re)
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob) (sshv2 !)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob) (sshv2 !)
   devel-peer-request: hello
   sending hello command
   devel-peer-request: between
   devel-peer-request:   pairs: 81 bytes
   sending between command
   remote: 384 (sshv1 !)
-  protocol upgraded to exp-ssh-v2-0001 (sshv2 !)
+  protocol upgraded to exp-ssh-v2-0002 (sshv2 !)
   remote: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   remote: 1 (sshv1 !)
   query 1; heads
diff --git a/tests/test-ssh-proto.t b/tests/test-ssh-proto.t
--- a/tests/test-ssh-proto.t
+++ b/tests/test-ssh-proto.t
@@ -910,7 +910,7 @@
 
   $ hg debugwireproto --localssh --peer raw << EOF
   > raw
-  >     upgrade 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a proto=irrelevant1%2Cirrelevant2\n
+  >     upgrade * proto=irrelevant1%2Cirrelevant2\n (glob)
   > readline
   > raw
   >     hello\n
@@ -924,7 +924,7 @@
   > EOF
   using raw connection to peer
   i> write(77) -> 77:
-  i>     upgrade 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a proto=irrelevant1%2Cirrelevant2\n
+  i>     upgrade * proto=irrelevant1%2Cirrelevant2\n (glob)
   o> readline() -> 2:
   o>     0\n
   i> write(104) -> 104:
@@ -946,7 +946,7 @@
   $ hg --config experimental.sshpeer.advertise-v2=true --debug debugpeer ssh://user@dummy/server
   running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) (no-windows !)
   running * "*\tests/dummyssh" "user@dummy" "hg -R server serve --stdio" (glob) (windows !)
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob)
   devel-peer-request: hello
   sending hello command
   devel-peer-request: between
@@ -974,7 +974,7 @@
 
   $ hg debugwireproto --localssh --peer raw << EOF
   > raw
-  >     upgrade this-is-some-token proto=exp-ssh-v2-0001\n
+  >     upgrade this-is-some-token proto=exp-ssh-v2-0002\n
   >     hello\n
   >     between\n
   >     pairs 81\n
@@ -985,13 +985,13 @@
   > EOF
   using raw connection to peer
   i> write(153) -> 153:
-  i>     upgrade this-is-some-token proto=exp-ssh-v2-0001\n
+  i>     upgrade this-is-some-token proto=exp-ssh-v2-0002\n
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   o> readline() -> 44:
-  o>     upgraded this-is-some-token exp-ssh-v2-0001\n
+  o>     upgraded this-is-some-token exp-ssh-v2-0002\n
   o> readline() -> 4:
   o>     383\n
   o> readline() -> 384:
@@ -1002,13 +1002,13 @@
   $ hg --config experimental.sshpeer.advertise-v2=true --debug debugpeer ssh://user@dummy/server
   running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) (no-windows !)
   running * "*\tests/dummyssh" "user@dummy" "hg -R server serve --stdio" (glob) (windows !)
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob)
   devel-peer-request: hello
   sending hello command
   devel-peer-request: between
   devel-peer-request:   pairs: 81 bytes
   sending between command
-  protocol upgraded to exp-ssh-v2-0001
+  protocol upgraded to exp-ssh-v2-0002
   remote: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   url: ssh://user@dummy/server
   local: no
@@ -1019,13 +1019,13 @@
   $ hg --config experimental.sshpeer.advertise-v2=true --debug debugcapabilities ssh://user@dummy/server
   running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) (no-windows !)
   running * "*\tests/dummyssh" "user@dummy" "hg -R server serve --stdio" (glob) (windows !)
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob)
   devel-peer-request: hello
   sending hello command
   devel-peer-request: between
   devel-peer-request:   pairs: 81 bytes
   sending between command
-  protocol upgraded to exp-ssh-v2-0001
+  protocol upgraded to exp-ssh-v2-0002
   remote: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   Main capabilities:
     batch
@@ -1069,37 +1069,38 @@
 
   $ hg debugwireproto --localssh --peer raw << EOF
   > raw
-  >      upgrade this-is-some-token proto=exp-ssh-v2-0001\n
+  >      upgrade this-is-some-token proto=exp-ssh-v2-0002\n
   >      hello\n
   >      between\n
   >      pairs 81\n
   >      0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   > readline
   > readline
   > readline
   > raw
-  >      hello\n
-  > readline
-  > readline
+  > # Frame of length 5
+  >     \x05\x00\x00
+  > # 0x01 frame type with 0x01 end of data flag set.
+  >     \x11
+  >     hello
+  > readframe
   > EOF
   using raw connection to peer
   i> write(153) -> 153:
-  i>     upgrade this-is-some-token proto=exp-ssh-v2-0001\n
+  i>     upgrade this-is-some-token proto=exp-ssh-v2-0002\n
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   o> readline() -> 44:
-  o>     upgraded this-is-some-token exp-ssh-v2-0001\n
+  o>     upgraded this-is-some-token exp-ssh-v2-0002\n
   o> readline() -> 4:
   o>     383\n
   o> readline() -> 384:
   o>     capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN\n
-  i> write(6) -> 6:
-  i>     hello\n
-  o> readline() -> 4:
-  o>     366\n
-  o> readline() -> 366:
+  i> write(9) -> 9: \x05\x00\x00\x11hello
+  o> readinto(4) -> 4: n\x01\x00R
+  o> read(366) -> 366:
   o>     capabilities: lookup branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN\n
 
 Multiple upgrades is not allowed
@@ -1115,9 +1116,13 @@
   > readline
   > readline
   > raw
-  >     upgrade another-token proto=irrelevant\n
-  >     hello\n
-  > readline
+  >     \x26\x00\x00
+  > # 0x01 << 4 | 0x01
+  >     \x11
+  >     upgrade another-token proto=irrelevant
+  >     \x05\x00\x00
+  >     \x11
+  >     hello
   > readavailable
   > EOF
   using raw connection to peer
@@ -1127,20 +1132,16 @@
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
-  o> readline() -> 44:
-  o>     upgraded this-is-some-token exp-ssh-v2-0001\n
+  o> readline() -> 2:
+  o>     0\n
   o> readline() -> 4:
-  o>     383\n
+  o>     384\n
   o> readline() -> 384:
   o>     capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN\n
-  i> write(45) -> 45:
-  i>     upgrade another-token proto=irrelevant\n
-  i>     hello\n
-  o> readline() -> 1:
+  i> write(51) -> 51: &\x00\x00\x11upgrade another-token proto=irrelevant\x05\x00\x00\x11hello
+  o> read(-1) -> 3:
+  o>     1\n
   o>     \n
-  e> read(-1) -> 42:
-  e>     cannot upgrade protocols multiple times\n
-  e>     -\n
 
 Malformed upgrade request line (not exactly 3 space delimited tokens)
 
@@ -1227,11 +1228,10 @@
   i> write(44) -> 44:
   i>     upgrade token proto=exp-ssh-v2-0001\n
   i>     invalid\n
-  o> readline() -> 1:
-  o>     \n
-  e> read(-1) -> 46:
-  e>     malformed handshake protocol: missing hello\n
-  e>     -\n
+  o> readline() -> 2:
+  o>     0\n
+  o> read(-1) -> 2:
+  o>     0\n
 
   $ hg debugwireproto --localssh --peer raw << EOF
   > raw
@@ -1246,11 +1246,8 @@
   i>     upgrade token proto=exp-ssh-v2-0001\n
   i>     hello\n
   i>     invalid\n
-  o> readline() -> 1:
-  o>     \n
-  e> read(-1) -> 48:
-  e>     malformed handshake protocol: missing between\n
-  e>     -\n
+  o> readline() -> 2:
+  o>     0\n
 
   $ hg debugwireproto --localssh --peer raw << EOF
   > raw
@@ -1267,11 +1264,8 @@
   i>     hello\n
   i>     between\n
   i>     invalid\n
-  o> readline() -> 1:
-  o>     \n
-  e> read(-1) -> 49:
-  e>     malformed handshake protocol: missing pairs 81\n
-  e>     -\n
+  o> readline() -> 2:
+  o>     0\n
 
 Legacy commands are not exposed to version 2 of protocol
 
@@ -1281,24 +1275,30 @@
   > EOF
   creating ssh peer from handshake results
   sending branches command
-  response:
+  abort: application level error:
+  'command not available: branches'
+  [255]
 
   $ hg --config experimental.sshpeer.advertise-v2=true debugwireproto --localssh << EOF
   > command changegroup
   >     roots 0000000000000000000000000000000000000000
   > EOF
   creating ssh peer from handshake results
   sending changegroup command
-  response:
+  abort: application level error:
+  'command not available: changegroup'
+  [255]
 
   $ hg --config experimental.sshpeer.advertise-v2=true debugwireproto --localssh << EOF
   > command changegroupsubset
   >     bases 0000000000000000000000000000000000000000
   >     heads 0000000000000000000000000000000000000000
   > EOF
   creating ssh peer from handshake results
   sending changegroupsubset command
-  response:
+  abort: application level error:
+  'command not available: changegroupsubset'
+  [255]
 
   $ cd ..
 
@@ -1344,29 +1344,27 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending listkeys command
-  i> write(9) -> 9:
-  i>     listkeys\n
-  i> write(13) -> 13:
-  i>     namespace 10\n
-  i> write(10) -> 10: namespaces
+  i> write(12) -> 12: \x08\x00\x00\x12listkeys
+  i> write(27) -> 27:
+  i>     \x17\x00\x00" \x00\n
+  i>     \x00namespacenamespaces
   i> flush() -> None
-  o> bufferedreadline() -> 3:
-  o>     30\n
-  o> bufferedread(30) -> 30:
+  o> readinto(4) -> 4: \x1e\x00\x00R
+  o> read(30) -> 30:
   o>     bookmarks \n
   o>     namespaces \n
   o>     phases
@@ -1420,28 +1418,25 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending listkeys command
-  i> write(9) -> 9:
-  i>     listkeys\n
-  i> write(12) -> 12:
-  i>     namespace 9\n
-  i> write(9) -> 9: bookmarks
+  i> write(12) -> 12: \x08\x00\x00\x12listkeys
+  i> write(26) -> 26: \x16\x00\x00" \x00 \x00namespacebookmarks
   i> flush() -> None
-  o> bufferedreadline() -> 2:
-  o>     0\n
+  o> readinto(4) -> 4: \x00\x00\x00R
+  o> read(0) -> 0:
   response:
 
 With a single bookmark set
@@ -1482,29 +1477,25 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending listkeys command
-  i> write(9) -> 9:
-  i>     listkeys\n
-  i> write(12) -> 12:
-  i>     namespace 9\n
-  i> write(9) -> 9: bookmarks
+  i> write(12) -> 12: \x08\x00\x00\x12listkeys
+  i> write(26) -> 26: \x16\x00\x00" \x00 \x00namespacebookmarks
   i> flush() -> None
-  o> bufferedreadline() -> 3:
-  o>     46\n
-  o> bufferedread(46) -> 46: bookA 68986213bd4485ea51533535e3fc9e78007a711f
+  o> readinto(4) -> 4: .\x00\x00R
+  o> read(46) -> 46: bookA 68986213bd4485ea51533535e3fc9e78007a711f
   response: bookA 68986213bd4485ea51533535e3fc9e78007a711f
 
 With multiple bookmarks set
@@ -1547,29 +1538,25 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending listkeys command
-  i> write(9) -> 9:
-  i>     listkeys\n
-  i> write(12) -> 12:
-  i>     namespace 9\n
-  i> write(9) -> 9: bookmarks
+  i> write(12) -> 12: \x08\x00\x00\x12listkeys
+  i> write(26) -> 26: \x16\x00\x00" \x00 \x00namespacebookmarks
   i> flush() -> None
-  o> bufferedreadline() -> 3:
-  o>     93\n
-  o> bufferedread(93) -> 93:
+  o> readinto(4) -> 4: ]\x00\x00R
+  o> read(93) -> 93:
   o>     bookA 68986213bd4485ea51533535e3fc9e78007a711f\n
   o>     bookB 1880f3755e2e52e3199e0ee5638128b08642f34d
   response: bookA 68986213bd4485ea51533535e3fc9e78007a711f\nbookB 1880f3755e2e52e3199e0ee5638128b08642f34d
@@ -1623,37 +1610,28 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending pushkey command
-  i> write(8) -> 8:
-  i>     pushkey\n
-  i> write(6) -> 6:
-  i>     key 6\n
-  i> write(6) -> 6: remote
-  i> write(12) -> 12:
-  i>     namespace 9\n
-  i> write(9) -> 9: bookmarks
-  i> write(7) -> 7:
-  i>     new 40\n
-  i> write(40) -> 40: 68986213bd4485ea51533535e3fc9e78007a711f
-  i> write(6) -> 6:
-  i>     old 0\n
+  i> write(11) -> 11: \x07\x00\x00\x12pushkey
+  i> write(17) -> 17: \r\x00\x00 \x03\x00\x06\x00keyremote
+  i> write(26) -> 26: \x16\x00\x00 \x00 \x00namespacebookmarks
+  i> write(51) -> 51: /\x00\x00 \x03\x00(\x00new68986213bd4485ea51533535e3fc9e78007a711f
+  i> write(11) -> 11: \x07\x00\x00"\x03\x00\x00\x00old
   i> flush() -> None
-  o> bufferedreadline() -> 2:
-  o>     2\n
-  o> bufferedread(2) -> 2:
+  o> readinto(4) -> 4: \x02\x00\x00R
+  o> read(2) -> 2:
   o>     1\n
   response: 1\n
 
@@ -1706,29 +1684,25 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending listkeys command
-  i> write(9) -> 9:
-  i>     listkeys\n
-  i> write(12) -> 12:
-  i>     namespace 6\n
-  i> write(6) -> 6: phases
+  i> write(12) -> 12: \x08\x00\x00\x12listkeys
+  i> write(23) -> 23: \x13\x00\x00" \x00\x06\x00namespacephases
   i> flush() -> None
-  o> bufferedreadline() -> 3:
-  o>     15\n
-  o> bufferedread(15) -> 15: publishing True
+  o> readinto(4) -> 4: \x0f\x00\x00R
+  o> read(15) -> 15: publishing True
   response: publishing True
 
 Create some commits
@@ -1788,29 +1762,25 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending listkeys command
-  i> write(9) -> 9:
-  i>     listkeys\n
-  i> write(12) -> 12:
-  i>     namespace 6\n
-  i> write(6) -> 6: phases
+  i> write(12) -> 12: \x08\x00\x00\x12listkeys
+  i> write(23) -> 23: \x13\x00\x00" \x00\x06\x00namespacephases
   i> flush() -> None
-  o> bufferedreadline() -> 4:
-  o>     101\n
-  o> bufferedread(101) -> 101:
+  o> readinto(4) -> 4: e\x00\x00R
+  o> read(101) -> 101:
   o>     20b8a89289d80036e6c4e87c2083e3bea1586637 1\n
   o>     c4750011d906c18ea2f0527419cbc1a544435150 1\n
   o>     publishing True
@@ -1856,29 +1826,25 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending listkeys command
-  i> write(9) -> 9:
-  i>     listkeys\n
-  i> write(12) -> 12:
-  i>     namespace 6\n
-  i> write(6) -> 6: phases
+  i> write(12) -> 12: \x08\x00\x00\x12listkeys
+  i> write(23) -> 23: \x13\x00\x00" \x00\x06\x00namespacephases
   i> flush() -> None
-  o> bufferedreadline() -> 3:
-  o>     58\n
-  o> bufferedread(58) -> 58:
+  o> readinto(4) -> 4: :\x00\x00R
+  o> read(58) -> 58:
   o>     c4750011d906c18ea2f0527419cbc1a544435150 1\n
   o>     publishing True
   response: c4750011d906c18ea2f0527419cbc1a544435150 1\npublishing True
@@ -1921,29 +1887,25 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending listkeys command
-  i> write(9) -> 9:
-  i>     listkeys\n
-  i> write(12) -> 12:
-  i>     namespace 6\n
-  i> write(6) -> 6: phases
+  i> write(12) -> 12: \x08\x00\x00\x12listkeys
+  i> write(23) -> 23: \x13\x00\x00" \x00\x06\x00namespacephases
   i> flush() -> None
-  o> bufferedreadline() -> 3:
-  o>     15\n
-  o> bufferedread(15) -> 15: publishing True
+  o> readinto(4) -> 4: \x0f\x00\x00R
+  o> read(15) -> 15: publishing True
   response: publishing True
 
 Setting public phase via pushkey
@@ -1998,38 +1960,28 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending pushkey command
-  i> write(8) -> 8:
-  i>     pushkey\n
-  i> write(7) -> 7:
-  i>     key 40\n
-  i> write(40) -> 40: 7127240a084fd9dc86fe8d1f98e26229161ec82b
-  i> write(12) -> 12:
-  i>     namespace 6\n
-  i> write(6) -> 6: phases
-  i> write(6) -> 6:
-  i>     new 1\n
-  i> write(1) -> 1: 0
-  i> write(6) -> 6:
-  i>     old 1\n
-  i> write(1) -> 1: 1
+  i> write(11) -> 11: \x07\x00\x00\x12pushkey
+  i> write(51) -> 51: /\x00\x00 \x03\x00(\x00key7127240a084fd9dc86fe8d1f98e26229161ec82b
+  i> write(23) -> 23: \x13\x00\x00 \x00\x06\x00namespacephases
+  i> write(12) -> 12: \x08\x00\x00 \x03\x00\x01\x00new0
+  i> write(12) -> 12: \x08\x00\x00"\x03\x00\x01\x00old1
   i> flush() -> None
-  o> bufferedreadline() -> 2:
-  o>     2\n
-  o> bufferedread(2) -> 2:
+  o> readinto(4) -> 4: \x02\x00\x00R
+  o> read(2) -> 2:
   o>     1\n
   response: 1\n
 
@@ -2104,31 +2056,25 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending batch with 3 sub-commands
-  i> write(6) -> 6:
-  i>     batch\n
-  i> write(4) -> 4:
-  i>     * 0\n
-  i> write(8) -> 8:
-  i>     cmds 61\n
-  i> write(61) -> 61: heads ;listkeys namespace=bookmarks;listkeys namespace=phases
+  i> write(9) -> 9: \x05\x00\x00\x12batch
+  i> write(73) -> 73: E\x00\x00"\x04\x00=\x00cmdsheads ;listkeys namespace=bookmarks;listkeys namespace=phases
   i> flush() -> None
-  o> bufferedreadline() -> 4:
-  o>     278\n
-  o> bufferedread(278) -> 278:
+  o> readinto(4) -> 4: \x16\x01\x00R
+  o> read(278) -> 278:
   o>     bfebe6bd38eebc6f8202e419c1171268987ea6a6 4ee3fcef1c800fa2bf23e20af7c83ff111d9c7ab\n
   o>     ;bookA 4ee3fcef1c800fa2bf23e20af7c83ff111d9c7ab\n
   o>     bookB bfebe6bd38eebc6f8202e419c1171268987ea6a6;4ee3fcef1c800fa2bf23e20af7c83ff111d9c7ab 1\n
diff --git a/tests/test-ssh-proto-unbundle.t b/tests/test-ssh-proto-unbundle.t
--- a/tests/test-ssh-proto-unbundle.t
+++ b/tests/test-ssh-proto-unbundle.t
@@ -100,52 +100,73 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
-  result: 0
-  remote output:
-  e> read(-1) -> 115:
-  e>     abort: incompatible Mercurial client; bundle2 required\n
-  e>     (see https://www.mercurial-scm.org/wiki/IncompatibleClient)\n
+  ** unknown exception encountered, please report by visiting
+  ** https://mercurial-scm.org/wiki/BugTracker
+  ** Python 2.7.14 (default, Nov 23 2017, 16:06:54) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.38)]
+  ** Mercurial Distributed SCM (version 4.5+785-8bb225342dee+20180302)
+  ** Extensions loaded:
+  Traceback (most recent call last):
+    File "/Users/gps/src/hg/hg", line 41, in <module>
+      dispatch.run()
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 88, in run
+      status = (dispatch(req) or 0) & 255
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 183, in dispatch
+      ret = _runcatch(req)
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 324, in _runcatch
+      return _callcatch(ui, _runcatchfunc)
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 332, in _callcatch
+      return scmutil.callcatch(ui, func)
+    File "/Users/gps/src/hg/mercurial/scmutil.py", line 154, in callcatch
+      return func()
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 314, in _runcatchfunc
+      return _dispatch(req)
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 917, in _dispatch
+      cmdpats, cmdoptions)
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 674, in runcommand
+      ret = _runcommand(ui, options, cmd, d)
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 925, in _runcommand
+      return cmdfunc()
+    File "/Users/gps/src/hg/mercurial/dispatch.py", line 914, in <lambda>
+      d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
+    File "/Users/gps/src/hg/mercurial/util.py", line 1497, in check
+      return func(*args, **kwargs)
+    File "/Users/gps/src/hg/mercurial/debugcommands.py", line 2800, in debugwireproto
+      **pycompat.strkwargs(args))
+    File "/Users/gps/src/hg/mercurial/wireprotoframing.py", line 654, in _callpush
+      error, response, output = readoutputandsimpleresponse(self._readpipe)
+    File "/Users/gps/src/hg/mercurial/wireprotoframing.py", line 146, in readoutputandsimpleresponse
+      return err, response, output.getvalue()
+  AttributeError: 'NoneType' object has no attribute 'getvalue'
 
   $ cd ..
 
@@ -273,57 +294,55 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \x9a\x00\x00@
+  o> read(154) -> 154:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     ui.write 1 line\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: ui.write 1 line
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 196:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     ui.write 1 line\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
+  e> read(-1) -> 42:
   e>     abort: pretxnchangegroup.fail hook failed\n
 
 And a variation that writes multiple lines using ui.write
@@ -400,58 +419,57 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \xb0\x00\x00@
+  o> read(176) -> 176:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     ui.write 2 lines 1\n
+  o>     ui.write 2 lines 2\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: ui.write 2 lines 1
+  remote: ui.write 2 lines 2
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 218:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     ui.write 2 lines 1\n
-  e>     ui.write 2 lines 2\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
+  e> read(-1) -> 42:
   e>     abort: pretxnchangegroup.fail hook failed\n
 
 And a variation that does a ui.flush() after writing output
@@ -527,57 +545,55 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \xa0\x00\x00@
+  o> read(160) -> 160:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     ui.write 1 line flush\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: ui.write 1 line flush
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 202:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     ui.write 1 line flush\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
+  e> read(-1) -> 42:
   e>     abort: pretxnchangegroup.fail hook failed\n
 
 Multiple writes + flush
@@ -654,58 +670,57 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \xa4\x00\x00@
+  o> read(164) -> 164:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     ui.write 1st\n
+  o>     ui.write 2nd\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: ui.write 1st
+  remote: ui.write 2nd
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 206:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     ui.write 1st\n
-  e>     ui.write 2nd\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
+  e> read(-1) -> 42:
   e>     abort: pretxnchangegroup.fail hook failed\n
 
 ui.write() + ui.write_err() output is captured
@@ -784,60 +799,61 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \xbe\x00\x00@
+  o> read(190) -> 190:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     ui.write 1\n
+  o>     ui.write_err 1\n
+  o>     ui.write 2\n
+  o>     ui.write_err 2\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: ui.write 1
+  remote: ui.write_err 1
+  remote: ui.write 2
+  remote: ui.write_err 2
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 232:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     ui.write 1\n
-  e>     ui.write_err 1\n
-  e>     ui.write 2\n
-  e>     ui.write_err 2\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
+  e> read(-1) -> 42:
   e>     abort: pretxnchangegroup.fail hook failed\n
 
 print() output is captured
@@ -913,57 +929,54 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \x8a\x00\x00@
+  o> read(138) -> 138:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 193:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
+  e> read(-1) -> 55:
   e>     printed line\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
   e>     abort: pretxnchangegroup.fail hook failed\n
 
 Mixed print() and ui.write() are both captured
@@ -1042,60 +1055,59 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \xa0\x00\x00@
+  o> read(160) -> 160:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     ui.write 1\n
+  o>     ui.write 2\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: ui.write 1
+  remote: ui.write 2
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 218:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     ui.write 1\n
-  e>     ui.write 2\n
+  e> read(-1) -> 58:
   e>     print 1\n
   e>     print 2\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
   e>     abort: pretxnchangegroup.fail hook failed\n
 
 print() to stdout and stderr both get captured
@@ -1174,60 +1186,57 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \x8a\x00\x00@
+  o> read(138) -> 138:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 216:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
+  e> read(-1) -> 78:
   e>     stderr 1\n
   e>     stderr 2\n
   e>     stdout 1\n
   e>     stdout 2\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
   e>     abort: pretxnchangegroup.fail hook failed\n
 
 Shell hook writing to stdout has output captured
@@ -1310,58 +1319,57 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \x9c\x00\x00@
+  o> read(156) -> 156:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     stdout 1\n
+  o>     stdout 2\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: stdout 1
+  remote: stdout 2
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 212:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     stdout 1\n
-  e>     stdout 2\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
+  e> read(-1) -> 56:
   e>     abort: pretxnchangegroup.fail hook exited with status 1\n
 
 Shell hook writing to stderr has output captured
@@ -1439,58 +1447,57 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \x9c\x00\x00@
+  o> read(156) -> 156:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     stderr 1\n
+  o>     stderr 2\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: stderr 1
+  remote: stderr 2
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 212:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     stderr 1\n
-  e>     stderr 2\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
+  e> read(-1) -> 56:
   e>     abort: pretxnchangegroup.fail hook exited with status 1\n
 
 Shell hook writing to stdout and stderr has output captured
@@ -1572,60 +1579,61 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \xae\x00\x00@
+  o> read(174) -> 174:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     stdout 1\n
+  o>     stderr 1\n
+  o>     stdout 2\n
+  o>     stderr 2\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: stdout 1
+  remote: stderr 1
+  remote: stdout 2
+  remote: stderr 2
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 230:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     stdout 1\n
-  e>     stderr 1\n
-  e>     stdout 2\n
-  e>     stderr 2\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
+  e> read(-1) -> 56:
   e>     abort: pretxnchangegroup.fail hook exited with status 1\n
 
 Shell and Python hooks writing to stdout and stderr have output captured
@@ -1717,64 +1725,65 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \xc6\x00\x00@
+  o> read(198) -> 198:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     shell stdout 1\n
+  o>     shell stderr 1\n
+  o>     shell stdout 2\n
+  o>     shell stderr 2\n
+  o>     transaction abort!\n
+  o>     rollback completed\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 0
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: shell stdout 1
+  remote: shell stderr 1
+  remote: shell stdout 2
+  remote: shell stderr 2
+  remote: transaction abort!
+  remote: rollback completed
   result: 0
   remote output:
-  e> read(-1) -> 273:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     shell stdout 1\n
-  e>     shell stderr 1\n
-  e>     shell stdout 2\n
-  e>     shell stderr 2\n
+  e> read(-1) -> 75:
   e>     stderr 1\n
   e>     stderr 2\n
   e>     stdout 1\n
   e>     stdout 2\n
-  e>     transaction abort!\n
-  e>     rollback completed\n
   e>     abort: pretxnchangegroup.b hook failed\n
 
   $ cd ..
@@ -1846,54 +1855,48 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: d\x00\x00@
+  o> read(100) -> 100:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 1
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
   result: 1
   remote output:
-  e> read(-1) -> 100:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
 
   $ cd ..
 
@@ -1980,55 +1983,53 @@
   testing ssh2
   creating ssh peer from handshake results
   i> write(171) -> 171:
-  i>     upgrade * proto=exp-ssh-v2-0001\n (glob)
+  i>     upgrade * proto=exp-ssh-v2-0002\n (glob)
   i>     hello\n
   i>     between\n
   i>     pairs 81\n
   i>     0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
   i> flush() -> None
   o> readline() -> 62:
-  o>     upgraded * exp-ssh-v2-0001\n (glob)
+  o>     upgraded * exp-ssh-v2-0002\n (glob)
   o> readline() -> 4:
   o>     383\n
   o> read(383) -> 383: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   o> read(1) -> 1:
   o>     \n
   sending unbundle command
-  i> write(9) -> 9:
-  i>     unbundle\n
-  i> write(9) -> 9:
-  i>     heads 10\n
-  i> write(10) -> 10: 666f726365
-  i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  i> write(4) -> 4:
-  i>     426\n
-  i> write(426) -> 426:
-  i>     HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
+  i> write(12) -> 12: \x08\x00\x00\x16unbundle
+  i> write(23) -> 23:
+  i>     \x13\x00\x00"\x05\x00\n
+  i>     \x00heads666f726365
+  i> write(430) -> 430:
+  i>     \xaa\x01\x002HG10UN\x00\x00\x00\x9eh\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>cba485ca3678256e044428f70f58291196f6e9de\n
   i>     test\n
   i>     0 0\n
   i>     foo\n
   i>     \n
   i>     initial\x00\x00\x00\x00\x00\x00\x00\x8d\xcb\xa4\x85\xca6x%n\x04D(\xf7\x0fX)\x11\x96\xf6\xe9\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-foo\x00362fef284ce2ca02aecc8de6d5e8a1c3af0556fe\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x07foo\x00\x00\x00b6/\xef(L\xe2\xca\x02\xae\xcc\x8d\xe6\xd5\xe8\xa1\xc3\xaf\x05V\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x98b\x13\xbdD\x85\xeaQS55\xe3\xfc\x9ex\x00zq\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x020\n
   i>     \x00\x00\x00\x00\x00\x00\x00\x00
-  i> write(2) -> 2:
-  i>     0\n
   i> flush() -> None
-  o> readline() -> 2:
-  o>     0\n
-  o> readline() -> 2:
-  o>     1\n
+  o> readinto(4) -> 4: \x98\x00\x00@
+  o> read(152) -> 152:
+  o>     adding changesets\n
+  o>     adding manifests\n
+  o>     adding file changes\n
+  o>     added 1 changesets with 1 changes to 1 files\n
+  o>     ui.write 1\n
+  o>     ui.write_err 1\n
+  o>     ui.write 2\n
+  o>     ui.write_err 2\n
+  o> readinto(4) -> 4: \x01\x00\x00R
   o> read(1) -> 1: 1
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: ui.write 1
+  remote: ui.write_err 1
+  remote: ui.write 2
+  remote: ui.write_err 2
   result: 1
   remote output:
-  e> read(-1) -> 152:
-  e>     adding changesets\n
-  e>     adding manifests\n
-  e>     adding file changes\n
-  e>     added 1 changesets with 1 changes to 1 files\n
-  e>     ui.write 1\n
-  e>     ui.write_err 1\n
-  e>     ui.write 2\n
-  e>     ui.write_err 2\n
diff --git a/tests/test-ssh-bundle1.t b/tests/test-ssh-bundle1.t
--- a/tests/test-ssh-bundle1.t
+++ b/tests/test-ssh-bundle1.t
@@ -475,10 +475,10 @@
   $ hg pull --debug ssh://user@dummy/remote
   pulling from ssh://user@dummy/remote
   running .* ".*/dummyssh" ['"]user@dummy['"] ('|")hg -R remote serve --stdio('|") (re)
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob) (sshv2 !)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob) (sshv2 !)
   sending hello command
   sending between command
-  protocol upgraded to exp-ssh-v2-0001 (sshv2 !)
+  protocol upgraded to exp-ssh-v2-0002 (sshv2 !)
   remote: 384 (sshv1 !)
   remote: capabilities: lookup branchmap pushkey known getbundle unbundlehash batch changegroupsubset streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
   remote: 1 (sshv1 !)
diff --git a/tests/test-clone.t b/tests/test-clone.t
--- a/tests/test-clone.t
+++ b/tests/test-clone.t
@@ -1152,38 +1152,38 @@
 #if windows
   $ hg clone "ssh://%26touch%20owned%20/" --debug
   running sh -c "read l; read l; read l" "&touch owned " "hg -R . serve --stdio"
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob) (sshv2 !)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob) (sshv2 !)
   sending hello command
   sending between command
   abort: no suitable response from remote hg!
   [255]
   $ hg clone "ssh://example.com:%26touch%20owned%20/" --debug
   running sh -c "read l; read l; read l" -p "&touch owned " example.com "hg -R . serve --stdio"
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob) (sshv2 !)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob) (sshv2 !)
   sending hello command
   sending between command
   abort: no suitable response from remote hg!
   [255]
 #else
   $ hg clone "ssh://%3btouch%20owned%20/" --debug
   running sh -c "read l; read l; read l" ';touch owned ' 'hg -R . serve --stdio'
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob) (sshv2 !)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob) (sshv2 !)
   sending hello command
   sending between command
   abort: no suitable response from remote hg!
   [255]
   $ hg clone "ssh://example.com:%3btouch%20owned%20/" --debug
   running sh -c "read l; read l; read l" -p ';touch owned ' example.com 'hg -R . serve --stdio'
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob) (sshv2 !)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob) (sshv2 !)
   sending hello command
   sending between command
   abort: no suitable response from remote hg!
   [255]
 #endif
 
   $ hg clone "ssh://v-alid.example.com/" --debug
   running sh -c "read l; read l; read l" v-alid\.example\.com ['"]hg -R \. serve --stdio['"] (re)
-  sending upgrade request: * proto=exp-ssh-v2-0001 (glob) (sshv2 !)
+  sending upgrade request: * proto=exp-ssh-v2-0002 (glob) (sshv2 !)
   sending hello command
   sending between command
   abort: no suitable response from remote hg!
diff --git a/tests/test-check-interfaces.py b/tests/test-check-interfaces.py
--- a/tests/test-check-interfaces.py
+++ b/tests/test-check-interfaces.py
@@ -72,7 +72,7 @@
     checkobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(),
                                   dummypipe(), None, None))
     checkobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(),
-                                  dummypipe(), None, None))
+                                  dummypipe(), None))
     checkobject(bundlerepo.bundlepeer(dummyrepo()))
     checkobject(statichttprepo.statichttppeer(dummyrepo()))
     checkobject(unionrepo.unionpeer(dummyrepo()))
diff --git a/mercurial/wireprototypes.py b/mercurial/wireprototypes.py
--- a/mercurial/wireprototypes.py
+++ b/mercurial/wireprototypes.py
@@ -11,7 +11,7 @@
 SSHV1 = 'ssh-v1'
 # This is advertised over the wire. Incremental the counter at the end
 # to reflect BC breakages.
-SSHV2 = 'exp-ssh-v2-0001'
+SSHV2 = 'exp-ssh-v2-0002'
 
 # All available wire protocol transports.
 TRANSPORTS = {
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -19,6 +19,7 @@
     pycompat,
     util,
     wireproto,
+    wireprotoframing,
     wireprototypes,
 )
 
@@ -389,13 +390,17 @@
     def addcapabilities(self, repo, caps):
         return caps
 
-class sshv2protocolhandler(sshv1protocolhandler):
+class sshv2protocolhandler(wireprotoframing.framedprotocolhandler):
     """Protocol handler for version 2 of the SSH protocol."""
 
     @property
     def name(self):
         return wireprototypes.SSHV2
 
+    def client(self):
+        client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
+        return 'remote:ssh:%s' % client
+
 def _runsshserver(ui, repo, fin, fout, ev):
     # This function operates like a state machine of sorts. The following
     # states are defined:
@@ -445,9 +450,8 @@
     #    Protocol upgrade to version 2 complete. Server can now speak protocol
     #    version 2.
     #
-    # protov2-serving -> protov1-serving
-    #    Ths happens by default since protocol version 2 is the same as
-    #    version 1 except for the handshake.
+    # protov2-serving -> shutdown
+    #    When the server wants to shut down.
 
     state = 'protov1-serving'
     proto = sshv1protocolhandler(ui, fin, fout)
@@ -505,9 +509,10 @@
                 raise error.ProgrammingError('unhandled response type from '
                                              'wire protocol command: %s' % rsp)
 
-        # For now, protocol version 2 serving just goes back to version 1.
         elif state == 'protov2-serving':
-            state = 'protov1-serving'
+            wireprotoframing.runserver(ui, repo, fin, fout,
+                                       sshv2protocolhandler)
+            state = 'shutdown'
             continue
 
         elif state == 'upgrade-initial':
@@ -596,7 +601,6 @@
             fout.write(b'%d\n%s\n' % (len(rsp), rsp))
             fout.flush()
 
-            proto = sshv2protocolhandler(ui, fin, fout)
             protoswitched = True
 
             state = 'protov2-serving'
diff --git a/mercurial/wireprotoframing.py b/mercurial/wireprotoframing.py
new file mode 100644
--- /dev/null
+++ b/mercurial/wireprotoframing.py
@@ -0,0 +1,670 @@
+# wireprotoframing.py - unified framing protocol for wire protocol
+#
+# Copyright 2018 Gregory Szorc <[hidden email]>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+# This file contains functionality to support the unified frame-based wire
+# protocol. For details about the protocol, see
+# `hg help internals.wireprotocol`.
+
+from __future__ import absolute_import
+
+import contextlib
+import struct
+
+from .i18n import _
+from . import (
+    error,
+    pycompat,
+    util,
+    wireproto,
+    wireprototypes,
+)
+
+FRAME_HEADER_SIZE = 4
+
+FRAME_TYPE_COMMAND_NAME = 0x01
+FRAME_TYPE_COMMAND_ARGUMENT = 0x02
+FRAME_TYPE_COMMAND_DATA = 0x03
+FRAME_TYPE_OUTPUT = 0x04
+FRAME_TYPE_RESPONSE_BYTES = 0x05
+FRAME_TYPE_ERROR = 0x06
+
+FLAG_COMMAND_NAME_EOS = 0x01
+FLAG_COMMAND_NAME_HAVE_ARGS = 0x02
+FLAG_COMMAND_NAME_HAVE_DATA = 0x04
+
+FLAG_COMMAND_ARGUMENT_CONTINUATION = 0x01
+FLAG_COMMAND_ARGUMENT_EOA = 0x02
+
+FLAG_COMMAND_DATA_CONTINUATION = 0x01
+FLAG_COMMAND_DATA_EOS = 0x02
+
+FLAG_RESPONSE_CONTINUATION = 0x01
+FLAG_RESPONSE_EOS = 0x02
+
+FLAG_ERROR_PROTOCOL = 0x01
+FLAG_ERROR_APPLICATION = 0x02
+
+ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH')
+
+def readframe(fh):
+    """Read a frame from the unified framing protocol."""
+    header = bytearray(FRAME_HEADER_SIZE)
+
+    readcount = fh.readinto(header)
+
+    if readcount == 0:
+        return None
+
+    if readcount != FRAME_HEADER_SIZE:
+        raise error.Abort(_('received incomplete frame: got %d bytes: %s') %
+                          (readcount, header))
+
+    # 24 bits payload length
+    # 4 bits frame type
+    # 4 bits frame flags
+    # ... payload
+    framelength = header[0] + 256 * header[1] + 16384 * header[2]
+    typeflags = header[3]
+
+    frametype = (typeflags & 0xf0) >> 4
+    frameflags = typeflags & 0x0f
+
+    payload = fh.read(framelength)
+    if len(payload) != framelength:
+        raise error.Abort(_('frame length error: expected %d; got %d') %
+                          (framelength, len(payload)))
+
+    return frametype, frameflags, payload
+
+def readresponsetogenerator(fh):
+    """Read a command response into a generator of payload chunks."""
+    while True:
+        frame = readframe(fh)
+
+        if not frame:
+            raise error.Abort(_('unexpected end to response stream'))
+
+        frametype, frameflags, payload = frame
+
+        if frametype == FRAME_TYPE_RESPONSE_BYTES:
+            yield payload
+
+            if frameflags & FLAG_RESPONSE_EOS:
+                return
+
+        elif frametype == FRAME_TYPE_ERROR:
+            if frameflags & FLAG_ERROR_PROTOCOL:
+                msg = _('protocol level error:')
+            elif frameflags & FLAG_ERROR_APPLICATION:
+                msg = _('application level error:')
+            else:
+                msg = _('unknown error:')
+
+            raise error.ResponseError(msg, payload)
+
+        else:
+            raise error.Abort(_('frame not handled: %d') % frametype)
+
+def readoutputandsimpleresponse(fh):
+    """Read frames constituting a simple response to a command.
+
+    We expect to see 0 or more side-channel frames containing server output
+    followed by either a bytes response or error frame, which signals the
+    end of the exchange.
+    """
+    err = None
+    response = None
+    output = None
+
+    while True:
+        frame = readframe(fh)
+
+        if not frame:
+            raise error.Abort(_('unexpected end to simple response'))
+
+        frametype, frameflags, payload = frame
+
+        if frametype == FRAME_TYPE_RESPONSE_BYTES:
+            if not frameflags & FLAG_RESPONSE_EOS:
+                raise error.ResponseError(_('response spanned multiple frames'))
+
+            response = payload
+            break
+        elif frametype == FRAME_TYPE_ERROR:
+            err = payload
+            break
+        elif frametype == FRAME_TYPE_OUTPUT:
+            output = util.stringio()
+            output.write(payload)
+        else:
+            raise error.ResponseError(_('unexpected frame: %d') % frametype)
+
+    return err, response, output.getvalue()
+
+def writeframe(fh, frametype, frameflags, payload):
+    # TODO assert size of payload.
+    frame = bytearray(FRAME_HEADER_SIZE + len(payload))
+
+    l = struct.pack(r'<I', len(payload))
+    frame[0:3] = l[0:3]
+    frame[3] = (frametype << 4) | frameflags
+    frame[4:] = payload
+
+    fh.write(frame)
+
+def writeargumentframes(fh, name, value, lastargument=False):
+    """Write frames necessary to send a command argument."""
+    # TODO handle splitting of argument values across frames.
+    payload = bytearray(ARGUMENT_FRAME_HEADER.size + len(name) + len(value))
+
+    offset = 0
+    ARGUMENT_FRAME_HEADER.pack_into(payload, offset, len(name), len(value))
+    offset += ARGUMENT_FRAME_HEADER.size
+    payload[offset:offset + len(name)] = name
+    offset += len(name)
+    payload[offset:offset + len(value)] = value
+
+    flags = FLAG_COMMAND_ARGUMENT_EOA if lastargument else 0
+    writeframe(fh, FRAME_TYPE_COMMAND_ARGUMENT, flags, payload)
+
+def writecommanddataframes(ui, fh, datafh):
+    """Write command data frames to a pipe.
+
+    The source file object is read until exhaustion.
+    """
+    while True:
+        data = datafh.read(32768)
+
+        if not data:
+            return
+
+        if len(data) == 32768:
+            flags = FLAG_COMMAND_DATA_CONTINUATION
+        else:
+            flags = FLAG_COMMAND_DATA_EOS
+
+        ui.debug('sending command data frame with %d bytes' % len(data))
+
+        writeframe(fh, FRAME_TYPE_COMMAND_DATA, flags, data)
+
+def writebytesresponsestatic(fh, value):
+    """Write bytes response frame(s) from a static value."""
+    assert isinstance(value, bytes)
+
+    # TODO handle splitting frames.
+    writeframe(fh, FRAME_TYPE_RESPONSE_BYTES, FLAG_RESPONSE_EOS, value)
+
+def writebytesresponsestream(fh, it):
+    cb = util.chunkbuffer(it)
+
+    while True:
+        data = cb.read(32768)
+
+        if not data:
+            return
+
+        if len(data) == 32768:
+            flags = FLAG_RESPONSE_CONTINUATION
+        else:
+            flags = FLAG_RESPONSE_EOS
+
+        writeframe(fh, FRAME_TYPE_RESPONSE_BYTES, flags, data)
+
+def writeerrorresponse(fh, message, protocol=False, application=False):
+    assert isinstance(message, bytes)
+
+    flags = 0
+    if protocol:
+        flags |= FLAG_ERROR_PROTOCOL
+    if application:
+        flags |= FLAG_ERROR_APPLICATION
+
+    writeframe(fh, FRAME_TYPE_ERROR, flags, message)
+
+class framedprotocolhandler(wireprototypes.baseprotocolhandler):
+    """Transport agnostic protocol handler for the unified framing protocol."""
+
+    def __init__(self, ui, args, data=None):
+        self._ui = ui
+        self._args = args
+        self._data = data
+
+    def getargs(self, args):
+        data = {}
+        keys = args.split()
+        for k in keys:
+            if k == '*':
+                star = {}
+                for key in self._args:
+                    if key not in keys:
+                        star[key] = self._args[key]
+                data['*'] = star
+            else:
+                data[k] = self._args[k]
+
+        return [data[k] for k in keys]
+
+    def forwardpayload(self, ofh):
+        # TODO It feels like this should before we get here.
+        if not self._data:
+            raise error.Abort(_('request to forward payload when no data '
+                                'defined'))
+
+        while True:
+            chunk = self._data.read(32768)
+            if not chunk:
+                break
+
+            ofh.write(chunk)
+
+    @contextlib.contextmanager
+    def mayberedirectstdio(self):
+        # TODO stream output as it is produced instead of buffering it.
+        oldout = self._ui.fout
+        olderr = self._ui.ferr
+
+        out = util.stringio()
+
+        try:
+            self._ui.fout = out
+            self._ui.ferr = out
+            yield out
+        finally:
+            self._ui.fout = oldout
+            self._ui.ferr = olderr
+
+    def addcapabilities(self, repo, caps):
+        return caps
+
+def runserver(ui, repo, readpipe, writepipe, protocls):
+    """Runs a frame-based protocol server until connction termination.
+
+    ``readpipe`` and ``writepipe`` are a pair of unidirectional
+    pipes to use for reads and writes, respectively.
+
+    ``protocls`` is a type defining the protocol handler for the
+    server.
+
+    This function acts as a state machine of sorts. The following
+    states are defined:
+
+    waiting
+       Server is waiting for a frame indicating to do something.
+
+    command-received-all
+       All arguments and data for the active command have been received.
+
+    command-receiving-args
+       A command request has been received. Actively receiving command
+       argument data.
+
+    command-receiving-data
+       A command request has been received. Actively receiving command
+       data.
+
+    arg-continuation
+       A command argument frame was received and its value did not fit
+       in a single frame. Additional argument data is expected.
+
+    And the transitions between states:
+
+    waiting -> command-received-all
+       A command request frame indicating no command arguments or data
+       to follow was received.
+
+    waiting-> command-receiving-args
+       A command request frame was received and has indicated that command
+       argument data will follow.
+    """
+    state = 'waiting'
+
+    activecommand = None
+    activeargs = None
+    expectingargs = None
+    expectingdata = None
+    activeargname = None
+    activeargchunks = None
+    activedata = None
+
+    while True:
+        if state == 'waiting':
+            frame = readframe(readpipe)
+
+            if not frame:
+                return
+
+            frametype, frameflags, payload = frame
+
+            # The only frame type that should be received in this state
+            # is a command request.
+            if frametype != FRAME_TYPE_COMMAND_NAME:
+                writeerrorresponse(writepipe,
+                                   b'expected command frame; '
+                                   b'got %d' % frametype,
+                                   protocol=True)
+                writepipe.flush()
+                return
+
+            # The frame payload is the command to execute.
+            activecommand = payload
+            activeargs = {}
+            activedata = None
+
+            available = wireproto.commands.commandavailable(
+                activecommand, protocls(ui, None))
+
+            if not available:
+                writeerrorresponse(writepipe,
+                                   b'command not available: %s' % activecommand,
+                                   application=True)
+                writepipe.flush()
+                # TODO consume remaining frames related to command
+                return
+
+            if frameflags & FLAG_COMMAND_NAME_EOS:
+                state = 'command-received-all'
+                continue
+
+            expectingargs = bool(frameflags & FLAG_COMMAND_NAME_HAVE_ARGS)
+            expectingdata = bool(frameflags & FLAG_COMMAND_NAME_HAVE_DATA)
+
+            if expectingargs:
+                state = 'command-receiving-args'
+            elif expectingdata:
+                state = 'command-receiving-data'
+            else:
+                # This is in violation of the protocol: the EOS flag should
+                # have been set.
+                writeerrorresponse(writepipe,
+                                   b'missing flags on command frame',
+                                   protocol=True)
+                writepipe.flush()
+                return
+
+            continue
+
+        elif state == 'command-receiving-args':
+            frame = readframe(readpipe)
+            if not frame:
+                return
+            frametype, frameflags, payload = frame
+
+            if frametype != FRAME_TYPE_COMMAND_ARGUMENT:
+                writeerrorresponse(writepipe,
+                                   b'expected argument frame; got %d ' %
+                                   frametype, protocl=True)
+                writepipe.flush()
+                return
+
+            offset = 0
+            namesize, valuesize = ARGUMENT_FRAME_HEADER.unpack_from(payload)
+            offset += ARGUMENT_FRAME_HEADER.size
+
+            # The argument name MUST fit inside the frame.
+            argname = payload[offset:offset + namesize]
+            offset += namesize
+
+            if len(argname) != namesize:
+                writeerrorresponse(writepipe,
+                                   b'argument name size mismatch',
+                                   protocol=True)
+                writepipe.flush()
+                return
+
+            argvalue = payload[offset:]
+
+            # Argument value spans multiple frames. Record our active
+            # state and tell the state machine to expect additional argument
+            # data.
+            if frameflags & FLAG_COMMAND_ARGUMENT_CONTINUATION:
+                activeargname = argname
+                activeargchunks = [argvalue]
+                state = 'arg-continuation'
+                continue
+
+            # Common case: the argument value is completely contained in
+            # this frame.
+
+            if len(argvalue) != valuesize:
+                writeerrorresponse(writepipe,
+                                   b'argument value size mismatch',
+                                   protocol=True)
+                writepipe.flush()
+                return
+
+            activeargs[argname] = argvalue
+
+            if frameflags & FLAG_COMMAND_ARGUMENT_EOA:
+                if expectingdata:
+                    state = 'command-receiving-data'
+                else:
+                    state = 'command-received-all'
+
+            # Else additional argument frames are expected. Continue in this
+            # state.
+            continue
+
+        elif state == 'command-received-all':
+            proto = protocls(ui, activeargs, data=activedata)
+            rsp = wireproto.dispatch(repo, proto, activecommand)
+
+            if isinstance(rsp, bytes):
+                writebytesresponsestatic(writepipe, rsp)
+            elif isinstance(rsp, wireprototypes.bytesresponse):
+                writebytesresponsestatic(writepipe, rsp.data)
+            elif isinstance(rsp, wireprototypes.streamres):
+                writebytesresponsestream(writepipe, rsp.gen)
+            elif isinstance(rsp, wireprototypes.streamreslegacy):
+                writebytesresponsestream(writepipe, rsp.gen)
+            elif isinstance(rsp, wireprototypes.pushres):
+                # Send any output collected during execution.
+                # TODO this should be streamed, if possible.
+                if rsp.output:
+                    writeframe(writepipe, FRAME_TYPE_OUTPUT, 0, rsp.output)
+
+                writebytesresponsestatic(writepipe, b'%d' % rsp.res)
+            elif isinstance(rsp, wireprototypes.pusherr):
+                if rsp.output:
+                    writeframe(writepipe, FRAME_TYPE_OUTPUT, 0,
+                               rsp.output)
+
+                writebytesresponsestatic(writepipe, rsp.res)
+            elif isinstance(rsp, wireprototypes.ooberror):
+                writeerrorresponse(writepipe, rsp.message, application=True)
+            else:
+                raise error.ProgrammingError('unhandled response type from '
+                                             'wire protocol command: %s' % rsp)
+
+            writepipe.flush()
+            state = 'waiting'
+
+        elif state == 'command-receiving-data':
+            # TODO support streaming this data instead of buffering it.
+            # This will require some refactoring to how frames are read.
+            activedata = util.stringio()
+
+            while True:
+                frame = readframe(readpipe)
+
+                if not frame:
+                    writeerrorresponse(writepipe,
+                                       b'unexpected end to command data',
+                                       protocol=True)
+                    writepipe.flush()
+                    return
+
+                frametype, frameflags, payload = frame
+
+                if frametype != FRAME_TYPE_COMMAND_DATA:
+                    writeerrorresponse(writepipe,
+                                       b'expected command data frame; got %d ' %
+                                       frametype, protocol=True)
+                    writepipe.flush()
+                    return
+
+                activedata.write(payload)
+
+                if frameflags & FLAG_COMMAND_DATA_CONTINUATION:
+                    continue
+                elif frameflags & FLAG_COMMAND_DATA_EOS:
+                    break
+                else:
+                    writeerrorresponse(writepipe,
+                                       b'no flags set on command data frame',
+                                       protocol=True)
+                    writepipe.flush()
+                    return
+
+            activedata.seek(0)
+
+            state = 'command-received-all'
+            continue
+
+        else:
+            msg = 'unhandled sshv2 server state: %s' % state
+            writeerrorresponse(writepipe,
+                               b'unhandled sshv2 server state: %s' % state,
+                               application=True)
+            writepipe.flush()
+            raise error.ProgrammingError(msg)
+
+# TODO delete this class once we stop supporting stream_out in protocol
+# version 2.
+class slowchunkbufferwithreadline(util.chunkbuffer):
+    """A variation of chunkbuffer that has readline() support.
+
+    The implementation is not very efficient. This class should not be
+    used in performance sensitive code.
+    """
+    def readline(self):
+        line = []
+        while True:
+            c = self.read(1)
+            if not c:
+                return b''.join(line)
+
+            line.append(c)
+
+            if c == b'\n':
+                return b''.join(line)
+
+class framepeer(wireproto.wirepeer):
+    """A peer that speaks the unified frame-based protocol."""
+    def __init__(self, ui, url, writepipe, readpipe, caps):
+        self._ui = ui
+        self._url = url
+        self._writepipe = writepipe
+        self._readpipe = readpipe
+        self._caps = caps
+
+    # Begin of _basepeer interface.
+
+    @util.propertycache
+    def ui(self):
+        return self._ui
+
+    def url(self):
+        return self._url
+
+    def local(self):
+        return None
+
+    def peer(self):
+        return self
+
+    def canpush(self):
+        return True
+
+    def close(self):
+        pass
+
+    # End of _basepeer interface.
+
+    # Begin of _basewirecommands interface.
+
+    def capabilities(self):
+        return self._caps
+
+    # End of _basewirecommands interface.
+
+    def _sendrequest(self, cmd, args, datafh=None):
+        # Write the command name frame.
+        flags = 0
+        if args:
+            flags |= FLAG_COMMAND_NAME_HAVE_ARGS
+        if datafh:
+            flags |= FLAG_COMMAND_NAME_HAVE_DATA
+
+        if not args and not datafh:
+            flags |= FLAG_COMMAND_NAME_EOS
+
+        self.ui.debug('sending %s command\n' % cmd)
+        writeframe(self._writepipe, FRAME_TYPE_COMMAND_NAME, flags, cmd)
+
+        for i, k in enumerate(sorted(args)):
+            v = args[k]
+            last = i == len(args) - 1
+            #self.ui.debug('sending argument frame: %s=%s\n' % (k, v))
+            writeargumentframes(self._writepipe, k, v, last)
+
+        if datafh:
+            writecommanddataframes(self.ui, self._writepipe, datafh)
+
+        self._writepipe.flush()
+
+    def _call(self, cmd, **args):
+        args = pycompat.byteskwargs(args)
+        self._sendrequest(cmd, args)
+
+        chunks = list(readresponsetogenerator(self._readpipe))
+        return ''.join(chunks)
+
+    def _callstream(self, cmd, **args):
+        args = pycompat.byteskwargs(args)
+        self._sendrequest(cmd, args)
+
+        # stream_out is the only command whose consumer calls readline()
+        # on the result. readline() requires read-ahead and adds performance
+        # overhead. We don't want to expose readline() to all callers. So
+        # only give it to consumers who need it. Eventually, we'll remove
+        # support for stream_out from protocol version 2, as it has been
+        # replaced by streaming support in getbundle.
+        # TODO remove this hack once stream_out is no longer supported.
+        if cmd == 'stream_out':
+            cls = slowchunkbufferwithreadline
+        else:
+            cls = util.chunkbuffer
+
+        return cls(readresponsetogenerator(self._readpipe))
+
+    def _callcompressable(self, cmd, **args):
+        return self._callstream(cmd, **args)
+
+    def _callpush(self, cmd, fh, **args):
+        # This method is only called for the bundle1 push case.
+        # TODO make this error when called once we ban bundle1 on this
+        # protocol.
+        args = pycompat.byteskwargs(args)
+        self._sendrequest(cmd, args, datafh=fh)
+
+        error, response, output = readoutputandsimpleresponse(self._readpipe)
+
+        if output:
+            for l in output.splitlines():
+                self._ui.status(_('remote: %s\n') % l)
+
+        # These are the values that callers expect.
+        error = error or ''
+        response = response or ''
+
+        return response, error
+
+    def _calltwowaystream(self, cmd, fh, **args):
+        args = pycompat.byteskwargs(args)
+
+        self._sendrequest(cmd, args, datafh=fh)
+        return util.chunkbuffer(readresponsetogenerator(self._readpipe))
diff --git a/mercurial/sshpeer.py b/mercurial/sshpeer.py
--- a/mercurial/sshpeer.py
+++ b/mercurial/sshpeer.py
@@ -16,6 +16,7 @@
     pycompat,
     util,
     wireproto,
+    wireprotoframing,
     wireprotoserver,
     wireprototypes,
 )
@@ -538,11 +539,19 @@
         if self._autoreadstderr:
             self._readerr()
 
-class sshv2peer(sshv1peer):
-    """A peer that speakers version 2 of the transport protocol."""
-    # Currently version 2 is identical to version 1 post handshake.
-    # And handshake is performed before the peer is instantiated. So
-    # we need no custom code.
+class sshv2peer(wireprotoframing.framepeer):
+    """A peer that speaks version 2 of the transport protocol.
+
+    This peer is a thin wrapper around the frame-based peer.
+    """
+    def __init__(self, ui, url, proc, writepipe, readpipe, caps):
+        super(sshv2peer, self).__init__(ui, url, writepipe, readpipe, caps)
+        # self._proc is unused. Keeping a handle on the process prevents it
+        # from being garbage collected.
+        self._proc = proc
+
+    def _abort(self, exc):
+        _cleanuppipes(self.ui, self._readpipe, self._writepipe, None)
 
 def makepeer(ui, path, proc, stdin, stdout, stderr, autoreadstderr=True):
     """Make a peer instance from existing pipes.
@@ -568,8 +577,7 @@
         return sshv1peer(ui, path, proc, stdin, stdout, stderr, caps,
                          autoreadstderr=autoreadstderr)
     elif protoname == wireprototypes.SSHV2:
-        return sshv2peer(ui, path, proc, stdin, stdout, stderr, caps,
-                         autoreadstderr=autoreadstderr)
+        return sshv2peer(ui, path, proc, stdin, stdout, caps)
     else:
         _cleanuppipes(ui, stdout, stdin, stderr)
         raise error.RepoError(_('unknown version of SSH protocol: %s') %
diff --git a/mercurial/help/internals/wireprotocol.txt b/mercurial/help/internals/wireprotocol.txt
--- a/mercurial/help/internals/wireprotocol.txt
+++ b/mercurial/help/internals/wireprotocol.txt
@@ -373,11 +373,13 @@
 SSH Version 2 Transport
 -----------------------
 
-**Experimental**
+**Experimental and under development**
 
-Version 2 of the SSH transport behaves identically to version 1 of the SSH
-transport with the exception of handshake semantics. See above for how
-version 2 of the SSH transport is negotiated.
+Version 2 of the SSH transport is a near complete rewrite of version 1 of
+the SSH transport.
+
+Connections initiate in version 1 of the SSH transport. The handshake
+protocol (see above) is followed to switch to version 2.
 
 Immediately following the ``upgraded`` line signaling a switch to version
 2 of the SSH protocol, the server automatically sends additional details
@@ -392,8 +394,184 @@
    s: 240\n
    s: capabilities: known getbundle batch ...\n
 
-Following capabilities advertisement, the peers communicate using version
-1 of the SSH transport.
+Following capabilities advertisement, the peers communicate using the
+*Unified Frame-Based Protocol* (see below). Only the ``stdin`` and ``stdout``
+file descriptors are used (``stderr`` is unused).
+
+Unified Frame-Based Protocol
+============================
+
+**Experimental and under development**
+
+The *Unified Frame-Based Protocol* is a Mercurial wire protocol
+implementation that aims to be mostly transport agnostic (works with
+HTTP, SSH, etc).
+
+To operate the protocol, a bi-directional, half-duplex pipe supporting
+ordered sends and receives is required. That is, each peer has one pipe
+for sending data to the other and another pipe to read data from the other.
+
+The protocol is request-response based: the client issues requests to
+the server, which issues replies to those requests. Server-initiated
+messaging is not supported.
+
+All data written to peers uses a unified framing format.
+
+Frames begin with a 4 octet header followed by a variable length
+payload::
+
+    +-----------------------------------------------+
+    |                 Length (24)                   |
+    +-----------+-----------------------------------+
+    | Type (4)  |
+    +-----------+
+    | Flags (4) |
+    +===========+===================================================|
+    |                     Frame Payload (0...)                    ...
+    +---------------------------------------------------------------+
+
+The length of the frame payload is expressed as an unsigned 24 bit
+little endian integer. Values larger than 65535 MUST NOT be used unless
+given permission by the server as part of the negotiated capabilities
+during the handshake. The frame header is not part of the advertised
+frame length.
+
+The 4-bit ``Type`` field denotes the type of message being sent.
+
+The 4-bit ``Flags`` field defines special attributes for the frame.
+
+The sections below define the frame types and their behavior.
+
+Command Request (``0x01``)
+--------------------------
+
+This frame contains a request to run a command.
+
+The name of the command to run constitutes the entirety of the frame
+payload.
+
+This frame type MUST ONLY be sent from clients to servers: it is illegal
+for a server to send this frame to a client.
+
+The following flag values are defined for this type:
+
+0x01
+   End of command data. When set, the client will not send any command
+   arguments or additional command data. When set, the command has been
+   fully issued and the server has the full context to process the command.
+   The next frame issued by the client is not part of this command.
+0x02
+   Command argument frames expected. When set, the client will send
+   *Command Argument* frames containing command argument data.
+0x04
+   Command data frames expected. When set, the client will send
+   *Command Data* frames containing a raw stream of data for this
+   command.
+
+The ``0x01`` flag is mutually exclusive with both the ``0x02`` and ``0x04``
+flags.
+
+Command Argument (``0x02``)
+---------------------------
+
+This frame contains a named argument for a command.
+
+The frame type MUST ONLY be sent from clients to servers: it is illegal
+for a server to send this frame to a client.
+
+The payload consists of:
+
+* A 16-bit little endian integer denoting the length of the
+  argument name.
+* A 16-bit little endian integer denoting the length of the
+  argument value.
+* N bytes of ASCII data containing the argument name.
+* N bytes of binary data containing the argument value.
+
+The payload MUST hold the entirety of the 32-bit header and the
+argument name. The argument value MAY span multiple frames. If this
+occurs, the appropriate frame flag should be set to indicate this.
+
+The following flag values are defined for this type:
+
+0x01
+   Argument data continuation. When set, the data for this argument did
+   not fit in a single frame and the next frame will contain additional
+   argument data.
+
+0x02
+   End of arguments data. When set, the client will not send any more
+   command arguments for the command this frame is associated with.
+   The next frame issued by the client will be command data or
+   belong to a separate request.
+
+Command Data (``0x03``)
+-----------------------
+
+This frame contains raw data for a command.
+
+Most commands can be executed by specifying arguments. However,
+arguments have an upper bound to their length. For commands that
+accept data that is beyond this length or whose length isn't known
+when the command is initially sent, they will need to stream
+arbitrary data to the server. This frame type facilitates the sending
+of this data.
+
+The payload of this frame type consists of a stream of raw data to be
+consumed by the command handler on the server. The format of the data
+is command specific.
+
+The following flag values are defined for this type:
+
+0x01
+   Command data continuation. When set, the data for this command
+   continues into a subsequent frame.
+
+0x02
+   End of data. When set, command data has been fully sent to the
+   server. The command has been fully issued and no new data for this
+   command will be sent. The next frame will belong to a new command.
+
+Output Data (``0x04``)
+----------------------
+
+This frame contains output that should be displayed on the client.
+
+The payload is textual data that should be displayed to the client.
+
+Bytes Response Data (``0x05``)
+------------------------------
+
+This frame contains raw bytes response data to an issued command.
+
+The following flag values are defined for this type:
+
+0x01
+   Data continuation. When set, an additional frame containing raw
+   response data will follow.
+0x02
+   End of data. When sent, the response data has been fully sent and
+   no additional frames for this response will be sent.
+
+The ``0x01`` flag is mutually exclusive with the ``0x02`` flag.
+
+Error Response (``0x06``)
+-------------------------
+
+An error occurred when processing a request. This could indicate
+a protocol-level failure or an application level failure depending
+on the flags for this message type.
+
+The payload for this type is an error message that should be
+displayed to the user.
+
+The following flag values are defined for this type:
+
+0x01
+   The error occurred at the transport/protocol level. If set, the
+   connection should be closed.
+0x02
+   The error occurred at the application level. e.g. invalid command.
 
 Capabilities
 ============
diff --git a/mercurial/debugcommands.py b/mercurial/debugcommands.py
--- a/mercurial/debugcommands.py
+++ b/mercurial/debugcommands.py
@@ -75,6 +75,7 @@
     url as urlmod,
     util,
     vfs as vfsmod,
+    wireprotoframing,
     wireprotoserver,
 )
 from .utils import dateutil
@@ -2683,6 +2684,11 @@
 
     Read a line of output from the server. If there are multiple output
     pipes, reads only the main pipe.
+
+    readframe
+    ---------
+
+    Read a frame protocol frame from the wire.
     """
     opts = pycompat.byteskwargs(opts)
 
@@ -2736,8 +2742,7 @@
                                      None, autoreadstderr=autoreadstderr)
         elif opts['peer'] == 'ssh2':
             ui.write(_('creating ssh peer for wire protocol version 2\n'))
-            peer = sshpeer.sshv2peer(ui, url, proc, stdin, stdout, stderr,
-                                     None, autoreadstderr=autoreadstderr)
+            peer = sshpeer.sshv2peer(ui, url, proc, stdin, stdout, None)
         elif opts['peer'] == 'raw':
             ui.write(_('using raw connection to peer\n'))
             peer = None
@@ -2826,6 +2831,8 @@
                 util.readpipe(stderr)
         elif action == 'readline':
             stdout.readline()
+        elif action == 'readframe':
+            frame = wireprotoframing.readframe(stdout)
         else:
             raise error.Abort(_('unknown action: %s') % action)
 



To: indygreg, #hg-reviewers
Cc: mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

D2620: wireproto: define and implement unified framing protocol for SSH

indygreg (Gregory Szorc)
indygreg abandoned this revision.
indygreg added a comment.


  I'll break this up and send it as multiple commits.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D2620

To: indygreg, #hg-reviewers
Cc: mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel