# HG changeset patch # User Yuya Nishihara # Date 1421574599 -32400 # Node ID 054d0fcba2c4a4f65b009d92c1fd9d285e9af5f6 # Parent 9683dfb6f13a1dd1cf8182e743bab8a270394642 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: " 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. diff -r 9683dfb6f13a -r 054d0fcba2c4 contrib/hgclient.py --- 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 diff -r 9683dfb6f13a -r 054d0fcba2c4 mercurial/commandserver.py --- 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' diff -r 9683dfb6f13a -r 054d0fcba2c4 mercurial/configitems.py --- 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, diff -r 9683dfb6f13a -r 054d0fcba2c4 mercurial/ui.py --- 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': diff -r 9683dfb6f13a -r 054d0fcba2c4 tests/test-commandserver.t --- 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