wireproto: document the wonky push protocol for SSH
authorGregory Szorc <gregory.szorc@gmail.com>
Wed, 21 Feb 2018 16:47:39 -0800
changeset 36372 b8d0761a85c7
parent 36371 0c231df1ffdc
child 36373 0147a4730420
wireproto: document the wonky push protocol for SSH It took me several minutes to figure out how the "unbundle" protocol worked. It turns out that the SSH protocol handler sends an empty reply that is interpreted as "OK to send" and only then does the client send the bundle payload. On top of that, the response is different depending on whether the operation was successful or not. I nearly pulled out my hair deciphering this. Differential Revision: https://phab.mercurial-scm.org/D2385
mercurial/sshpeer.py
mercurial/wireprotoserver.py
--- a/mercurial/sshpeer.py	Wed Feb 21 14:21:05 2018 -0800
+++ b/mercurial/sshpeer.py	Wed Feb 21 16:47:39 2018 -0800
@@ -466,25 +466,40 @@
         return self._sendrequest(cmd, args, framed=True).read()
 
     def _callpush(self, cmd, fp, **args):
+        # The server responds with an empty frame if the client should
+        # continue submitting the payload.
         r = self._call(cmd, **args)
         if r:
             return '', r
+
+        # The payload consists of frames with content followed by an empty
+        # frame.
         for d in iter(lambda: fp.read(4096), ''):
             self._writeframed(d)
         self._writeframed("", flush=True)
+
+        # In case of success, there is an empty frame and a frame containing
+        # the integer result (as a string).
+        # In case of error, there is a non-empty frame containing the error.
         r = self._readframed()
         if r:
             return '', r
         return self._readframed(), ''
 
     def _calltwowaystream(self, cmd, fp, **args):
+        # The server responds with an empty frame if the client should
+        # continue submitting the payload.
         r = self._call(cmd, **args)
         if r:
             # XXX needs to be made better
             raise error.Abort(_('unexpected remote reply: %s') % r)
+
+        # The payload consists of frames with content followed by an empty
+        # frame.
         for d in iter(lambda: fp.read(4096), ''):
             self._writeframed(d)
         self._writeframed("", flush=True)
+
         return self._pipei
 
     def _getamount(self):
--- a/mercurial/wireprotoserver.py	Wed Feb 21 14:21:05 2018 -0800
+++ b/mercurial/wireprotoserver.py	Wed Feb 21 16:47:39 2018 -0800
@@ -347,12 +347,16 @@
         return [data[k] for k in keys]
 
     def forwardpayload(self, fpout):
+        # We initially send an empty response. This tells the client it is
+        # OK to start sending data. If a client sees any other response, it
+        # interprets it as an error.
+        _sshv1respondbytes(self._fout, b'')
+
         # The file is in the form:
         #
         # <chunk size>\n<chunk>
         # ...
         # 0\n
-        _sshv1respondbytes(self._fout, b'')
         count = int(self._fin.readline())
         while count:
             fpout.write(self._fin.read(count))