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.
--- a/contrib/hgclient.py Wed Nov 07 22:37:51 2018 +0900
+++ b/contrib/hgclient.py Sun Jan 18 18:49:59 2015 +0900
@@ -27,10 +27,11 @@
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 @@
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 @@
finally:
server.stdin.close()
server.wait()
+
+def checkwith(connect=connectpipe, **kwargs):
+ def wrap(func):
+ return check(func, lambda: connect(**kwargs))
+ return wrap
--- a/mercurial/commandserver.py Wed Nov 07 22:37:51 2018 +0900
+++ b/mercurial/commandserver.py Sun Jan 18 18:49:59 2015 +0900
@@ -26,9 +26,11 @@
from . import (
encoding,
error,
+ pycompat,
util,
)
from .utils import (
+ cborutil,
procutil,
)
@@ -70,6 +72,30 @@
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 @@
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 @@
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 @@
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 @@
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'
--- a/mercurial/configitems.py Wed Nov 07 22:37:51 2018 +0900
+++ b/mercurial/configitems.py Sun Jan 18 18:49:59 2015 +0900
@@ -173,6 +173,9 @@
coreconfigitem('cmdserver', 'log',
default=None,
)
+coreconfigitem('cmdserver', 'message-encodings',
+ default=list,
+)
coreconfigitem('color', '.*',
default=None,
generic=True,
--- a/mercurial/ui.py Wed Nov 07 22:37:51 2018 +0900
+++ b/mercurial/ui.py Sun Jan 18 18:49:59 2015 +0900
@@ -1012,7 +1012,11 @@
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)
@@ -1962,6 +1966,13 @@
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':
--- a/tests/test-commandserver.t Wed Nov 07 22:37:51 2018 +0900
+++ b/tests/test-commandserver.t Sun Jan 18 18:49:59 2015 +0900
@@ -724,6 +724,43 @@
$ 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