##// END OF EJS Templates
wireproto: define human output side channel frame...
Gregory Szorc -
r37078:0a6c5cc0 default
parent child Browse files
Show More
@@ -660,6 +660,64 b' 0x01'
660 0x02
660 0x02
661 The error occurred at the application level. e.g. invalid command.
661 The error occurred at the application level. e.g. invalid command.
662
662
663 Human Output Side-Channel (``0x06``)
664 ------------------------------------
665
666 This frame contains a message that is intended to be displayed to
667 people. Whereas most frames communicate machine readable data, this
668 frame communicates textual data that is intended to be shown to
669 humans.
670
671 The frame consists of a series of *formatting requests*. Each formatting
672 request consists of a formatting string, arguments for that formatting
673 string, and labels to apply to that formatting string.
674
675 A formatting string is a printf()-like string that allows variable
676 substitution within the string. Labels allow the rendered text to be
677 *decorated*. Assuming use of the canonical Mercurial code base, a
678 formatting string can be the input to the ``i18n._`` function. This
679 allows messages emitted from the server to be localized. So even if
680 the server has different i18n settings, people could see messages in
681 their *native* settings. Similarly, the use of labels allows
682 decorations like coloring and underlining to be applied using the
683 client's configured rendering settings.
684
685 Formatting strings are similar to ``printf()`` strings or how
686 Python's ``%`` operator works. The only supported formatting sequences
687 are ``%s`` and ``%%``. ``%s`` will be replaced by whatever the string
688 at that position resolves to. ``%%`` will be replaced by ``%``. All
689 other 2-byte sequences beginning with ``%`` represent a literal
690 ``%`` followed by that character. However, future versions of the
691 wire protocol reserve the right to allow clients to opt in to receiving
692 formatting strings with additional formatters, hence why ``%%`` is
693 required to represent the literal ``%``.
694
695 The raw frame consists of a series of data structures representing
696 textual atoms to print. Each atom begins with a struct defining the
697 size of the data that follows:
698
699 * A 16-bit little endian unsigned integer denoting the length of the
700 formatting string.
701 * An 8-bit unsigned integer denoting the number of label strings
702 that follow.
703 * An 8-bit unsigned integer denoting the number of formatting string
704 arguments strings that follow.
705 * An array of 8-bit unsigned integers denoting the lengths of
706 *labels* data.
707 * An array of 16-bit unsigned integers denoting the lengths of
708 formatting strings.
709 * The formatting string, encoded as UTF-8.
710 * 0 or more ASCII strings defining labels to apply to this atom.
711 * 0 or more UTF-8 strings that will be used as arguments to the
712 formatting string.
713
714 All data to be printed MUST be encoded into a single frame: this frame
715 does not support spanning data across multiple frames.
716
717 All textual data encoded in these frames is assumed to be line delimited.
718 The last atom in the frame SHOULD end with a newline (``\n``). If it
719 doesn't, clients MAY add a newline to facilitate immediate printing.
720
663 Issuing Commands
721 Issuing Commands
664 ----------------
722 ----------------
665
723
@@ -27,6 +27,7 b' FRAME_TYPE_COMMAND_ARGUMENT = 0x02'
27 FRAME_TYPE_COMMAND_DATA = 0x03
27 FRAME_TYPE_COMMAND_DATA = 0x03
28 FRAME_TYPE_BYTES_RESPONSE = 0x04
28 FRAME_TYPE_BYTES_RESPONSE = 0x04
29 FRAME_TYPE_ERROR_RESPONSE = 0x05
29 FRAME_TYPE_ERROR_RESPONSE = 0x05
30 FRAME_TYPE_TEXT_OUTPUT = 0x06
30
31
31 FRAME_TYPES = {
32 FRAME_TYPES = {
32 b'command-name': FRAME_TYPE_COMMAND_NAME,
33 b'command-name': FRAME_TYPE_COMMAND_NAME,
@@ -34,6 +35,7 b' FRAME_TYPES = {'
34 b'command-data': FRAME_TYPE_COMMAND_DATA,
35 b'command-data': FRAME_TYPE_COMMAND_DATA,
35 b'bytes-response': FRAME_TYPE_BYTES_RESPONSE,
36 b'bytes-response': FRAME_TYPE_BYTES_RESPONSE,
36 b'error-response': FRAME_TYPE_ERROR_RESPONSE,
37 b'error-response': FRAME_TYPE_ERROR_RESPONSE,
38 b'text-output': FRAME_TYPE_TEXT_OUTPUT,
37 }
39 }
38
40
39 FLAG_COMMAND_NAME_EOS = 0x01
41 FLAG_COMMAND_NAME_EOS = 0x01
@@ -85,6 +87,7 b' FRAME_TYPE_FLAGS = {'
85 FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA,
87 FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA,
86 FRAME_TYPE_BYTES_RESPONSE: FLAGS_BYTES_RESPONSE,
88 FRAME_TYPE_BYTES_RESPONSE: FLAGS_BYTES_RESPONSE,
87 FRAME_TYPE_ERROR_RESPONSE: FLAGS_ERROR_RESPONSE,
89 FRAME_TYPE_ERROR_RESPONSE: FLAGS_ERROR_RESPONSE,
90 FRAME_TYPE_TEXT_OUTPUT: {},
88 }
91 }
89
92
90 ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH')
93 ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH')
@@ -281,6 +284,74 b' def createerrorframe(requestid, msg, pro'
281
284
282 yield makeframe(requestid, FRAME_TYPE_ERROR_RESPONSE, flags, msg)
285 yield makeframe(requestid, FRAME_TYPE_ERROR_RESPONSE, flags, msg)
283
286
287 def createtextoutputframe(requestid, atoms):
288 """Create a text output frame to render text to people.
289
290 ``atoms`` is a 3-tuple of (formatting string, args, labels).
291
292 The formatting string contains ``%s`` tokens to be replaced by the
293 corresponding indexed entry in ``args``. ``labels`` is an iterable of
294 formatters to be applied at rendering time. In terms of the ``ui``
295 class, each atom corresponds to a ``ui.write()``.
296 """
297 bytesleft = DEFAULT_MAX_FRAME_SIZE
298 atomchunks = []
299
300 for (formatting, args, labels) in atoms:
301 if len(args) > 255:
302 raise ValueError('cannot use more than 255 formatting arguments')
303 if len(labels) > 255:
304 raise ValueError('cannot use more than 255 labels')
305
306 # TODO look for localstr, other types here?
307
308 if not isinstance(formatting, bytes):
309 raise ValueError('must use bytes formatting strings')
310 for arg in args:
311 if not isinstance(arg, bytes):
312 raise ValueError('must use bytes for arguments')
313 for label in labels:
314 if not isinstance(label, bytes):
315 raise ValueError('must use bytes for labels')
316
317 # Formatting string must be UTF-8.
318 formatting = formatting.decode(r'utf-8', r'replace').encode(r'utf-8')
319
320 # Arguments must be UTF-8.
321 args = [a.decode(r'utf-8', r'replace').encode(r'utf-8') for a in args]
322
323 # Labels must be ASCII.
324 labels = [l.decode(r'ascii', r'strict').encode(r'ascii')
325 for l in labels]
326
327 if len(formatting) > 65535:
328 raise ValueError('formatting string cannot be longer than 64k')
329
330 if any(len(a) > 65535 for a in args):
331 raise ValueError('argument string cannot be longer than 64k')
332
333 if any(len(l) > 255 for l in labels):
334 raise ValueError('label string cannot be longer than 255 bytes')
335
336 chunks = [
337 struct.pack(r'<H', len(formatting)),
338 struct.pack(r'<BB', len(labels), len(args)),
339 struct.pack(r'<' + r'B' * len(labels), *map(len, labels)),
340 struct.pack(r'<' + r'H' * len(args), *map(len, args)),
341 ]
342 chunks.append(formatting)
343 chunks.extend(labels)
344 chunks.extend(args)
345
346 atom = b''.join(chunks)
347 atomchunks.append(atom)
348 bytesleft -= len(atom)
349
350 if bytesleft < 0:
351 raise ValueError('cannot encode data in a single frame')
352
353 yield makeframe(requestid, FRAME_TYPE_TEXT_OUTPUT, 0, b''.join(atomchunks))
354
284 class serverreactor(object):
355 class serverreactor(object):
285 """Holds state of a server handling frame-based protocol requests.
356 """Holds state of a server handling frame-based protocol requests.
286
357
@@ -67,6 +67,109 b' class FrameTests(unittest.TestCase):'
67 ffs(b'1 command-data eos %s' % data.getvalue()),
67 ffs(b'1 command-data eos %s' % data.getvalue()),
68 ])
68 ])
69
69
70 def testtextoutputexcessiveargs(self):
71 """At most 255 formatting arguments are allowed."""
72 with self.assertRaisesRegexp(ValueError,
73 'cannot use more than 255 formatting'):
74 args = [b'x' for i in range(256)]
75 list(framing.createtextoutputframe(1, [(b'bleh', args, [])]))
76
77 def testtextoutputexcessivelabels(self):
78 """At most 255 labels are allowed."""
79 with self.assertRaisesRegexp(ValueError,
80 'cannot use more than 255 labels'):
81 labels = [b'l' for i in range(256)]
82 list(framing.createtextoutputframe(1, [(b'bleh', [], labels)]))
83
84 def testtextoutputformattingstringtype(self):
85 """Formatting string must be bytes."""
86 with self.assertRaisesRegexp(ValueError, 'must use bytes formatting '):
87 list(framing.createtextoutputframe(1, [
88 (b'foo'.decode('ascii'), [], [])]))
89
90 def testtextoutputargumentbytes(self):
91 with self.assertRaisesRegexp(ValueError, 'must use bytes for argument'):
92 list(framing.createtextoutputframe(1, [
93 (b'foo', [b'foo'.decode('ascii')], [])]))
94
95 def testtextoutputlabelbytes(self):
96 with self.assertRaisesRegexp(ValueError, 'must use bytes for labels'):
97 list(framing.createtextoutputframe(1, [
98 (b'foo', [], [b'foo'.decode('ascii')])]))
99
100 def testtextoutputtoolongformatstring(self):
101 with self.assertRaisesRegexp(ValueError,
102 'formatting string cannot be longer than'):
103 list(framing.createtextoutputframe(1, [
104 (b'x' * 65536, [], [])]))
105
106 def testtextoutputtoolongargumentstring(self):
107 with self.assertRaisesRegexp(ValueError,
108 'argument string cannot be longer than'):
109 list(framing.createtextoutputframe(1, [
110 (b'bleh', [b'x' * 65536], [])]))
111
112 def testtextoutputtoolonglabelstring(self):
113 with self.assertRaisesRegexp(ValueError,
114 'label string cannot be longer than'):
115 list(framing.createtextoutputframe(1, [
116 (b'bleh', [], [b'x' * 65536])]))
117
118 def testtextoutput1simpleatom(self):
119 val = list(framing.createtextoutputframe(1, [
120 (b'foo', [], [])]))
121
122 self.assertEqual(val, [
123 ffs(br'1 text-output 0 \x03\x00\x00\x00foo'),
124 ])
125
126 def testtextoutput2simpleatoms(self):
127 val = list(framing.createtextoutputframe(1, [
128 (b'foo', [], []),
129 (b'bar', [], []),
130 ]))
131
132 self.assertEqual(val, [
133 ffs(br'1 text-output 0 \x03\x00\x00\x00foo\x03\x00\x00\x00bar'),
134 ])
135
136 def testtextoutput1arg(self):
137 val = list(framing.createtextoutputframe(1, [
138 (b'foo %s', [b'val1'], []),
139 ]))
140
141 self.assertEqual(val, [
142 ffs(br'1 text-output 0 \x06\x00\x00\x01\x04\x00foo %sval1'),
143 ])
144
145 def testtextoutput2arg(self):
146 val = list(framing.createtextoutputframe(1, [
147 (b'foo %s %s', [b'val', b'value'], []),
148 ]))
149
150 self.assertEqual(val, [
151 ffs(br'1 text-output 0 \x09\x00\x00\x02\x03\x00\x05\x00'
152 br'foo %s %svalvalue'),
153 ])
154
155 def testtextoutput1label(self):
156 val = list(framing.createtextoutputframe(1, [
157 (b'foo', [], [b'label']),
158 ]))
159
160 self.assertEqual(val, [
161 ffs(br'1 text-output 0 \x03\x00\x01\x00\x05foolabel'),
162 ])
163
164 def testargandlabel(self):
165 val = list(framing.createtextoutputframe(1, [
166 (b'foo %s', [b'arg'], [b'label']),
167 ]))
168
169 self.assertEqual(val, [
170 ffs(br'1 text-output 0 \x06\x00\x01\x01\x05\x03\x00foo %slabelarg'),
171 ])
172
70 class ServerReactorTests(unittest.TestCase):
173 class ServerReactorTests(unittest.TestCase):
71 def _sendsingleframe(self, reactor, s):
174 def _sendsingleframe(self, reactor, s):
72 results = list(sendframes(reactor, [ffs(s)]))
175 results = list(sendframes(reactor, [ffs(s)]))
General Comments 0
You need to be logged in to leave comments. Login now