Patchwork [3,of,8] commandserver: add experimental option to use separate message channel

login
register
mail settings
Submitter Yuya Nishihara
Date Nov. 8, 2018, 2:24 p.m.
Message ID <6a4ccc024119f3bd983a.1541687081@mimosa>
Download mbox | patch
Permalink /patch/36472/
State Accepted
Headers show

Comments

Yuya Nishihara - Nov. 8, 2018, 2:24 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1421574599 -32400
#      Sun Jan 18 18:49:59 2015 +0900
# Node ID 6a4ccc024119f3bd983ab2cdf6337396a7bcab7e
# Parent  30c0af95d4461ad1da70ce0e0ad8a65ff367c128
commandserver: add experimental option to use separate message channel

This is loosely based on the idea of the TortoiseHg's pipeui extension,
which attaches ui.label to message text so the command-server client can
capture prompt text, for example.

https://bitbucket.org/tortoisehg/thg/src/4.7.2/tortoisehg/util/pipeui.py

I was thinking that this functionality could be generalized to templating,
but changed mind as doing template stuff would be unnecessarily complex.
It's merely a status message, a simple serialization option should suffice.

Since this slightly changes the command-server protocol, it's gated by a
config knob. If the config is enabled, and if it's supported by the server,
"message-encoding: <name>" is advertised so the client can stop parsing
'o'/'e' channel data and read encoded messages from the 'm' channel. As we
might add new message encodings in future releases, client can specify a list
of encoding names in preferred order.

This patch includes 'cbor' encoding as example. Perhaps, 'json' should be
supported as well.

Patch

diff --git a/contrib/hgclient.py b/contrib/hgclient.py
--- a/contrib/hgclient.py
+++ b/contrib/hgclient.py
@@ -27,10 +27,11 @@  else:
     stringio = cStringIO.StringIO
     bprint = print
 
-def connectpipe(path=None):
+def connectpipe(path=None, extraargs=()):
     cmdline = [b'hg', b'serve', b'--cmdserver', b'pipe']
     if path:
         cmdline += [b'-R', path]
+    cmdline.extend(extraargs)
 
     server = subprocess.Popen(cmdline, stdin=subprocess.PIPE,
                               stdout=subprocess.PIPE)
@@ -114,6 +115,8 @@  def runcommand(server, args, output=stdo
             writeblock(server, input.read(data))
         elif ch == b'L':
             writeblock(server, input.readline(data))
+        elif ch == b'm':
+            bprint(b"message: %r" % data)
         elif ch == b'r':
             ret, = struct.unpack('>i', data)
             if ret != 0:
@@ -132,3 +135,8 @@  def check(func, connect=connectpipe):
     finally:
         server.stdin.close()
         server.wait()
+
+def checkwith(connect=connectpipe, **kwargs):
+    def wrap(func):
+        return check(func, lambda: connect(**kwargs))
+    return wrap
diff --git a/mercurial/commandserver.py b/mercurial/commandserver.py
--- a/mercurial/commandserver.py
+++ b/mercurial/commandserver.py
@@ -26,9 +26,11 @@  from .i18n import _
 from . import (
     encoding,
     error,
+    pycompat,
     util,
 )
 from .utils import (
+    cborutil,
     procutil,
 )
 
@@ -70,6 +72,30 @@  class channeledoutput(object):
             raise AttributeError(attr)
         return getattr(self.out, attr)
 
+class channeledmessage(object):
+    """
+    Write encoded message and metadata to out in the following format:
+
+    data length (unsigned int),
+    encoded message and metadata, as a flat key-value dict.
+    """
+
+    # teach ui that write() can take **opts
+    structured = True
+
+    def __init__(self, out, channel, encodename, encodefn):
+        self._cout = channeledoutput(out, channel)
+        self.encoding = encodename
+        self._encodefn = encodefn
+
+    def write(self, data, **opts):
+        opts = pycompat.byteskwargs(opts)
+        opts[b'data'] = data
+        self._cout.write(self._encodefn(opts))
+
+    def __getattr__(self, attr):
+        return getattr(self._cout, attr)
+
 class channeledinput(object):
     """
     Read data from in_.
@@ -156,6 +182,20 @@  class channeledinput(object):
             raise AttributeError(attr)
         return getattr(self.in_, attr)
 
+_messageencoders = {
+    b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
+}
+
+def _selectmessageencoder(ui):
+    # experimental config: cmdserver.message-encodings
+    encnames = ui.configlist(b'cmdserver', b'message-encodings')
+    for n in encnames:
+        f = _messageencoders.get(n)
+        if f:
+            return n, f
+    raise error.Abort(b'no supported message encodings: %s'
+                      % b' '.join(encnames))
+
 class server(object):
     """
     Listens for commands on fin, runs them and writes the output on a channel
@@ -189,6 +229,14 @@  class server(object):
         self.cin = channeledinput(fin, fout, 'I')
         self.cresult = channeledoutput(fout, 'r')
 
+        # TODO: add this to help/config.txt when stabilized
+        # ``channel``
+        #   Use separate channel for structured output. (Command-server only)
+        self.cmsg = None
+        if ui.config(b'ui', b'message-output') == b'channel':
+            encname, encfn = _selectmessageencoder(ui)
+            self.cmsg = channeledmessage(fout, b'm', encname, encfn)
+
         self.client = fin
 
     def cleanup(self):
@@ -254,7 +302,7 @@  class server(object):
                 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
 
         req = dispatch.request(args[:], copiedui, self.repo, self.cin,
-                               self.cout, self.cerr)
+                               self.cout, self.cerr, self.cmsg)
 
         try:
             ret = dispatch.dispatch(req) & 255
@@ -289,6 +337,8 @@  class server(object):
         hellomsg += '\n'
         hellomsg += 'encoding: ' + encoding.encoding
         hellomsg += '\n'
+        if self.cmsg:
+            hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
         hellomsg += 'pid: %d' % procutil.getpid()
         if util.safehasattr(os, 'getpgid'):
             hellomsg += '\n'
diff --git a/mercurial/configitems.py b/mercurial/configitems.py
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -173,6 +173,9 @@  coreconfigitem('chgserver', 'skiphash',
 coreconfigitem('cmdserver', 'log',
     default=None,
 )
+coreconfigitem('cmdserver', 'message-encodings',
+    default=list,
+)
 coreconfigitem('color', '.*',
     default=None,
     generic=True,
diff --git a/mercurial/ui.py b/mercurial/ui.py
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -1015,7 +1015,11 @@  class ui(object):
         try:
             if dest is self._ferr and not getattr(self._fout, 'closed', False):
                 self._fout.flush()
-            if self._colormode == 'win32':
+            if getattr(dest, 'structured', False):
+                # channel for machine-readable output with metadata, where
+                # no extra colorization is necessary.
+                dest.write(msg, **opts)
+            elif self._colormode == 'win32':
                 # windows color printing is its own can of crab, defer to
                 # the color module and that is it.
                 color.win32print(self, dest.write, msg, **opts)
@@ -1965,6 +1969,13 @@  def haveprogbar():
 
 def _selectmsgdests(ui):
     name = ui.config(b'ui', b'message-output')
+    if name == b'channel':
+        if ui.fmsg:
+            return ui.fmsg, ui.fmsg
+        else:
+            # fall back to ferr if channel isn't ready so that status/error
+            # messages can be printed
+            return ui.ferr, ui.ferr
     if name == b'stdio':
         return ui.fout, ui.ferr
     if name == b'stderr':
diff --git a/tests/test-commandserver.t b/tests/test-commandserver.t
--- a/tests/test-commandserver.t
+++ b/tests/test-commandserver.t
@@ -724,6 +724,43 @@  don't fall back to cwd if invalid -R pat
   $ cd ..
 
 
+structured message channel:
+
+  $ cat <<'EOF' >> repo2/.hg/hgrc
+  > [ui]
+  > # server --config should precede repository option
+  > message-output = stdio
+  > EOF
+
+  >>> from hgclient import bprint, checkwith, readchannel, runcommand
+  >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
+  ...                       b'--config', b'cmdserver.message-encodings=foo cbor'])
+  ... def verify(server):
+  ...     _ch, data = readchannel(server)
+  ...     bprint(data)
+  ...     runcommand(server, [b'-R', b'repo2', b'verify'])
+  capabilities: getencoding runcommand
+  encoding: ascii
+  message-encoding: cbor
+  pid: * (glob)
+  pgid: * (glob)
+  *** runcommand -R repo2 verify
+  message: '\xa2DdataTchecking changesets\nElabelJ ui.status'
+  message: '\xa2DdataSchecking manifests\nElabelJ ui.status'
+  message: '\xa2DdataX0crosschecking files in changesets and manifests\nElabelJ ui.status'
+  message: '\xa2DdataOchecking files\nElabelJ ui.status'
+  message: '\xa2DdataX/checked 0 changesets with 0 changes to 0 files\nElabelJ ui.status'
+
+bad message encoding:
+
+  $ hg serve --cmdserver pipe --config ui.message-output=channel
+  abort: no supported message encodings: 
+  [255]
+  $ hg serve --cmdserver pipe --config ui.message-output=channel \
+  > --config cmdserver.message-encodings='foo bar'
+  abort: no supported message encodings: foo bar
+  [255]
+
 unix domain socket:
 
   $ cd repo