##// END OF EJS Templates
wireproto: implement basic frame reading and processing...
Gregory Szorc -
r37070:8c3c4736 default
parent child Browse files
Show More
@@ -0,0 +1,275 b''
1 from __future__ import absolute_import, print_function
2
3 import unittest
4
5 from mercurial import (
6 util,
7 wireprotoframing as framing,
8 )
9
10 ffs = framing.makeframefromhumanstring
11
12 def makereactor():
13 return framing.serverreactor()
14
15 def sendframes(reactor, gen):
16 """Send a generator of frame bytearray to a reactor.
17
18 Emits a generator of results from ``onframerecv()`` calls.
19 """
20 for frame in gen:
21 frametype, frameflags, framelength = framing.parseheader(frame)
22 payload = frame[framing.FRAME_HEADER_SIZE:]
23 assert len(payload) == framelength
24
25 yield reactor.onframerecv(frametype, frameflags, payload)
26
27 def sendcommandframes(reactor, cmd, args, datafh=None):
28 """Generate frames to run a command and send them to a reactor."""
29 return sendframes(reactor, framing.createcommandframes(cmd, args, datafh))
30
31 class FrameTests(unittest.TestCase):
32 def testdataexactframesize(self):
33 data = util.bytesio(b'x' * framing.DEFAULT_MAX_FRAME_SIZE)
34
35 frames = list(framing.createcommandframes(b'command', {}, data))
36 self.assertEqual(frames, [
37 ffs(b'command-name have-data command'),
38 ffs(b'command-data continuation %s' % data.getvalue()),
39 ffs(b'command-data eos ')
40 ])
41
42 def testdatamultipleframes(self):
43 data = util.bytesio(b'x' * (framing.DEFAULT_MAX_FRAME_SIZE + 1))
44 frames = list(framing.createcommandframes(b'command', {}, data))
45 self.assertEqual(frames, [
46 ffs(b'command-name have-data command'),
47 ffs(b'command-data continuation %s' % (
48 b'x' * framing.DEFAULT_MAX_FRAME_SIZE)),
49 ffs(b'command-data eos x'),
50 ])
51
52 def testargsanddata(self):
53 data = util.bytesio(b'x' * 100)
54
55 frames = list(framing.createcommandframes(b'command', {
56 b'key1': b'key1value',
57 b'key2': b'key2value',
58 b'key3': b'key3value',
59 }, data))
60
61 self.assertEqual(frames, [
62 ffs(b'command-name have-args|have-data command'),
63 ffs(br'command-argument 0 \x04\x00\x09\x00key1key1value'),
64 ffs(br'command-argument 0 \x04\x00\x09\x00key2key2value'),
65 ffs(br'command-argument eoa \x04\x00\x09\x00key3key3value'),
66 ffs(b'command-data eos %s' % data.getvalue()),
67 ])
68
69 class ServerReactorTests(unittest.TestCase):
70 def _sendsingleframe(self, reactor, s):
71 results = list(sendframes(reactor, [ffs(s)]))
72 self.assertEqual(len(results), 1)
73
74 return results[0]
75
76 def assertaction(self, res, expected):
77 self.assertIsInstance(res, tuple)
78 self.assertEqual(len(res), 2)
79 self.assertIsInstance(res[1], dict)
80 self.assertEqual(res[0], expected)
81
82 def test1framecommand(self):
83 """Receiving a command in a single frame yields request to run it."""
84 reactor = makereactor()
85 results = list(sendcommandframes(reactor, b'mycommand', {}))
86 self.assertEqual(len(results), 1)
87 self.assertaction(results[0], 'runcommand')
88 self.assertEqual(results[0][1], {
89 'command': b'mycommand',
90 'args': {},
91 'data': None,
92 })
93
94 def test1argument(self):
95 reactor = makereactor()
96 results = list(sendcommandframes(reactor, b'mycommand',
97 {b'foo': b'bar'}))
98 self.assertEqual(len(results), 2)
99 self.assertaction(results[0], 'wantframe')
100 self.assertaction(results[1], 'runcommand')
101 self.assertEqual(results[1][1], {
102 'command': b'mycommand',
103 'args': {b'foo': b'bar'},
104 'data': None,
105 })
106
107 def testmultiarguments(self):
108 reactor = makereactor()
109 results = list(sendcommandframes(reactor, b'mycommand',
110 {b'foo': b'bar', b'biz': b'baz'}))
111 self.assertEqual(len(results), 3)
112 self.assertaction(results[0], 'wantframe')
113 self.assertaction(results[1], 'wantframe')
114 self.assertaction(results[2], 'runcommand')
115 self.assertEqual(results[2][1], {
116 'command': b'mycommand',
117 'args': {b'foo': b'bar', b'biz': b'baz'},
118 'data': None,
119 })
120
121 def testsimplecommanddata(self):
122 reactor = makereactor()
123 results = list(sendcommandframes(reactor, b'mycommand', {},
124 util.bytesio(b'data!')))
125 self.assertEqual(len(results), 2)
126 self.assertaction(results[0], 'wantframe')
127 self.assertaction(results[1], 'runcommand')
128 self.assertEqual(results[1][1], {
129 'command': b'mycommand',
130 'args': {},
131 'data': b'data!',
132 })
133
134 def testmultipledataframes(self):
135 frames = [
136 ffs(b'command-name have-data mycommand'),
137 ffs(b'command-data continuation data1'),
138 ffs(b'command-data continuation data2'),
139 ffs(b'command-data eos data3'),
140 ]
141
142 reactor = makereactor()
143 results = list(sendframes(reactor, frames))
144 self.assertEqual(len(results), 4)
145 for i in range(3):
146 self.assertaction(results[i], 'wantframe')
147 self.assertaction(results[3], 'runcommand')
148 self.assertEqual(results[3][1], {
149 'command': b'mycommand',
150 'args': {},
151 'data': b'data1data2data3',
152 })
153
154 def testargumentanddata(self):
155 frames = [
156 ffs(b'command-name have-args|have-data command'),
157 ffs(br'command-argument 0 \x03\x00\x03\x00keyval'),
158 ffs(br'command-argument eoa \x03\x00\x03\x00foobar'),
159 ffs(b'command-data continuation value1'),
160 ffs(b'command-data eos value2'),
161 ]
162
163 reactor = makereactor()
164 results = list(sendframes(reactor, frames))
165
166 self.assertaction(results[-1], 'runcommand')
167 self.assertEqual(results[-1][1], {
168 'command': b'command',
169 'args': {
170 b'key': b'val',
171 b'foo': b'bar',
172 },
173 'data': b'value1value2',
174 })
175
176 def testunexpectedcommandargument(self):
177 """Command argument frame when not running a command is an error."""
178 result = self._sendsingleframe(makereactor(),
179 b'command-argument 0 ignored')
180 self.assertaction(result, 'error')
181 self.assertEqual(result[1], {
182 'message': b'expected command frame; got 2',
183 })
184
185 def testunexpectedcommanddata(self):
186 """Command argument frame when not running a command is an error."""
187 result = self._sendsingleframe(makereactor(),
188 b'command-data 0 ignored')
189 self.assertaction(result, 'error')
190 self.assertEqual(result[1], {
191 'message': b'expected command frame; got 3',
192 })
193
194 def testmissingcommandframeflags(self):
195 """Command name frame must have flags set."""
196 result = self._sendsingleframe(makereactor(),
197 b'command-name 0 command')
198 self.assertaction(result, 'error')
199 self.assertEqual(result[1], {
200 'message': b'missing frame flags on command frame',
201 })
202
203 def testmissingargumentframe(self):
204 frames = [
205 ffs(b'command-name have-args command'),
206 ffs(b'command-name 0 ignored'),
207 ]
208
209 results = list(sendframes(makereactor(), frames))
210 self.assertEqual(len(results), 2)
211 self.assertaction(results[0], 'wantframe')
212 self.assertaction(results[1], 'error')
213 self.assertEqual(results[1][1], {
214 'message': b'expected command argument frame; got 1',
215 })
216
217 def testincompleteargumentname(self):
218 """Argument frame with incomplete name."""
219 frames = [
220 ffs(b'command-name have-args command1'),
221 ffs(br'command-argument eoa \x04\x00\xde\xadfoo'),
222 ]
223
224 results = list(sendframes(makereactor(), frames))
225 self.assertEqual(len(results), 2)
226 self.assertaction(results[0], 'wantframe')
227 self.assertaction(results[1], 'error')
228 self.assertEqual(results[1][1], {
229 'message': b'malformed argument frame: partial argument name',
230 })
231
232 def testincompleteargumentvalue(self):
233 """Argument frame with incomplete value."""
234 frames = [
235 ffs(b'command-name have-args command'),
236 ffs(br'command-argument eoa \x03\x00\xaa\xaafoopartialvalue'),
237 ]
238
239 results = list(sendframes(makereactor(), frames))
240 self.assertEqual(len(results), 2)
241 self.assertaction(results[0], 'wantframe')
242 self.assertaction(results[1], 'error')
243 self.assertEqual(results[1][1], {
244 'message': b'malformed argument frame: partial argument value',
245 })
246
247 def testmissingcommanddataframe(self):
248 frames = [
249 ffs(b'command-name have-data command1'),
250 ffs(b'command-name eos command2'),
251 ]
252 results = list(sendframes(makereactor(), frames))
253 self.assertEqual(len(results), 2)
254 self.assertaction(results[0], 'wantframe')
255 self.assertaction(results[1], 'error')
256 self.assertEqual(results[1][1], {
257 'message': b'expected command data frame; got 1',
258 })
259
260 def testmissingcommanddataframeflags(self):
261 frames = [
262 ffs(b'command-name have-data command1'),
263 ffs(b'command-data 0 data'),
264 ]
265 results = list(sendframes(makereactor(), frames))
266 self.assertEqual(len(results), 2)
267 self.assertaction(results[0], 'wantframe')
268 self.assertaction(results[1], 'error')
269 self.assertEqual(results[1][1], {
270 'message': b'command data frame without flags',
271 })
272
273 if __name__ == '__main__':
274 import silenttestrunner
275 silenttestrunner.main(__name__)
@@ -586,6 +586,9 b" coreconfigitem('experimental', 'web.apis"
586 coreconfigitem('experimental', 'web.api.http-v2',
586 coreconfigitem('experimental', 'web.api.http-v2',
587 default=False,
587 default=False,
588 )
588 )
589 coreconfigitem('experimental', 'web.api.debugreflect',
590 default=False,
591 )
589 coreconfigitem('experimental', 'xdiff',
592 coreconfigitem('experimental', 'xdiff',
590 default=False,
593 default=False,
591 )
594 )
@@ -2564,6 +2564,14 b' class cappedreader(object):'
2564
2564
2565 return data
2565 return data
2566
2566
2567 def readinto(self, b):
2568 res = self.read(len(b))
2569 if res is None:
2570 return None
2571
2572 b[0:len(res)] = res
2573 return len(res)
2574
2567 def stringmatcher(pattern, casesensitive=True):
2575 def stringmatcher(pattern, casesensitive=True):
2568 """
2576 """
2569 accepts a string, possibly starting with 're:' or 'literal:' prefix.
2577 accepts a string, possibly starting with 're:' or 'literal:' prefix.
@@ -13,7 +13,9 b' from __future__ import absolute_import'
13
13
14 import struct
14 import struct
15
15
16 from .i18n import _
16 from . import (
17 from . import (
18 error,
17 util,
19 util,
18 )
20 )
19
21
@@ -105,6 +107,51 b' def makeframefromhumanstring(s):'
105
107
106 return makeframe(frametype, finalflags, payload)
108 return makeframe(frametype, finalflags, payload)
107
109
110 def parseheader(data):
111 """Parse a unified framing protocol frame header from a buffer.
112
113 The header is expected to be in the buffer at offset 0 and the
114 buffer is expected to be large enough to hold a full header.
115 """
116 # 24 bits payload length (little endian)
117 # 4 bits frame type
118 # 4 bits frame flags
119 # ... payload
120 framelength = data[0] + 256 * data[1] + 16384 * data[2]
121 typeflags = data[3]
122
123 frametype = (typeflags & 0xf0) >> 4
124 frameflags = typeflags & 0x0f
125
126 return frametype, frameflags, framelength
127
128 def readframe(fh):
129 """Read a unified framing protocol frame from a file object.
130
131 Returns a 3-tuple of (type, flags, payload) for the decoded frame or
132 None if no frame is available. May raise if a malformed frame is
133 seen.
134 """
135 header = bytearray(FRAME_HEADER_SIZE)
136
137 readcount = fh.readinto(header)
138
139 if readcount == 0:
140 return None
141
142 if readcount != FRAME_HEADER_SIZE:
143 raise error.Abort(_('received incomplete frame: got %d bytes: %s') %
144 (readcount, header))
145
146 frametype, frameflags, framelength = parseheader(header)
147
148 payload = fh.read(framelength)
149 if len(payload) != framelength:
150 raise error.Abort(_('frame length error: expected %d; got %d') %
151 (framelength, len(payload)))
152
153 return frametype, frameflags, payload
154
108 def createcommandframes(cmd, args, datafh=None):
155 def createcommandframes(cmd, args, datafh=None):
109 """Create frames necessary to transmit a request to run a command.
156 """Create frames necessary to transmit a request to run a command.
110
157
@@ -154,3 +201,195 b' def createcommandframes(cmd, args, dataf'
154
201
155 if done:
202 if done:
156 break
203 break
204
205 class serverreactor(object):
206 """Holds state of a server handling frame-based protocol requests.
207
208 This class is the "brain" of the unified frame-based protocol server
209 component. While the protocol is stateless from the perspective of
210 requests/commands, something needs to track which frames have been
211 received, what frames to expect, etc. This class is that thing.
212
213 Instances are modeled as a state machine of sorts. Instances are also
214 reactionary to external events. The point of this class is to encapsulate
215 the state of the connection and the exchange of frames, not to perform
216 work. Instead, callers tell this class when something occurs, like a
217 frame arriving. If that activity is worthy of a follow-up action (say
218 *run a command*), the return value of that handler will say so.
219
220 I/O and CPU intensive operations are purposefully delegated outside of
221 this class.
222
223 Consumers are expected to tell instances when events occur. They do so by
224 calling the various ``on*`` methods. These methods return a 2-tuple
225 describing any follow-up action(s) to take. The first element is the
226 name of an action to perform. The second is a data structure (usually
227 a dict) specific to that action that contains more information. e.g.
228 if the server wants to send frames back to the client, the data structure
229 will contain a reference to those frames.
230
231 Valid actions that consumers can be instructed to take are:
232
233 error
234 Indicates that an error occurred. Consumer should probably abort.
235
236 runcommand
237 Indicates that the consumer should run a wire protocol command. Details
238 of the command to run are given in the data structure.
239
240 wantframe
241 Indicates that nothing of interest happened and the server is waiting on
242 more frames from the client before anything interesting can be done.
243 """
244
245 def __init__(self):
246 self._state = 'idle'
247 self._activecommand = None
248 self._activeargs = None
249 self._activedata = None
250 self._expectingargs = None
251 self._expectingdata = None
252 self._activeargname = None
253 self._activeargchunks = None
254
255 def onframerecv(self, frametype, frameflags, payload):
256 """Process a frame that has been received off the wire.
257
258 Returns a dict with an ``action`` key that details what action,
259 if any, the consumer should take next.
260 """
261 handlers = {
262 'idle': self._onframeidle,
263 'command-receiving-args': self._onframereceivingargs,
264 'command-receiving-data': self._onframereceivingdata,
265 'errored': self._onframeerrored,
266 }
267
268 meth = handlers.get(self._state)
269 if not meth:
270 raise error.ProgrammingError('unhandled state: %s' % self._state)
271
272 return meth(frametype, frameflags, payload)
273
274 def _makeerrorresult(self, msg):
275 return 'error', {
276 'message': msg,
277 }
278
279 def _makeruncommandresult(self):
280 return 'runcommand', {
281 'command': self._activecommand,
282 'args': self._activeargs,
283 'data': self._activedata.getvalue() if self._activedata else None,
284 }
285
286 def _makewantframeresult(self):
287 return 'wantframe', {
288 'state': self._state,
289 }
290
291 def _onframeidle(self, frametype, frameflags, payload):
292 # The only frame type that should be received in this state is a
293 # command request.
294 if frametype != FRAME_TYPE_COMMAND_NAME:
295 self._state = 'errored'
296 return self._makeerrorresult(
297 _('expected command frame; got %d') % frametype)
298
299 self._activecommand = payload
300 self._activeargs = {}
301 self._activedata = None
302
303 if frameflags & FLAG_COMMAND_NAME_EOS:
304 return self._makeruncommandresult()
305
306 self._expectingargs = bool(frameflags & FLAG_COMMAND_NAME_HAVE_ARGS)
307 self._expectingdata = bool(frameflags & FLAG_COMMAND_NAME_HAVE_DATA)
308
309 if self._expectingargs:
310 self._state = 'command-receiving-args'
311 return self._makewantframeresult()
312 elif self._expectingdata:
313 self._activedata = util.bytesio()
314 self._state = 'command-receiving-data'
315 return self._makewantframeresult()
316 else:
317 self._state = 'errored'
318 return self._makeerrorresult(_('missing frame flags on '
319 'command frame'))
320
321 def _onframereceivingargs(self, frametype, frameflags, payload):
322 if frametype != FRAME_TYPE_COMMAND_ARGUMENT:
323 self._state = 'errored'
324 return self._makeerrorresult(_('expected command argument '
325 'frame; got %d') % frametype)
326
327 offset = 0
328 namesize, valuesize = ARGUMENT_FRAME_HEADER.unpack_from(payload)
329 offset += ARGUMENT_FRAME_HEADER.size
330
331 # The argument name MUST fit inside the frame.
332 argname = bytes(payload[offset:offset + namesize])
333 offset += namesize
334
335 if len(argname) != namesize:
336 self._state = 'errored'
337 return self._makeerrorresult(_('malformed argument frame: '
338 'partial argument name'))
339
340 argvalue = bytes(payload[offset:])
341
342 # Argument value spans multiple frames. Record our active state
343 # and wait for the next frame.
344 if frameflags & FLAG_COMMAND_ARGUMENT_CONTINUATION:
345 raise error.ProgrammingError('not yet implemented')
346 self._activeargname = argname
347 self._activeargchunks = [argvalue]
348 self._state = 'command-arg-continuation'
349 return self._makewantframeresult()
350
351 # Common case: the argument value is completely contained in this
352 # frame.
353
354 if len(argvalue) != valuesize:
355 self._state = 'errored'
356 return self._makeerrorresult(_('malformed argument frame: '
357 'partial argument value'))
358
359 self._activeargs[argname] = argvalue
360
361 if frameflags & FLAG_COMMAND_ARGUMENT_EOA:
362 if self._expectingdata:
363 self._state = 'command-receiving-data'
364 self._activedata = util.bytesio()
365 # TODO signal request to run a command once we don't
366 # buffer data frames.
367 return self._makewantframeresult()
368 else:
369 self._state = 'waiting'
370 return self._makeruncommandresult()
371 else:
372 return self._makewantframeresult()
373
374 def _onframereceivingdata(self, frametype, frameflags, payload):
375 if frametype != FRAME_TYPE_COMMAND_DATA:
376 self._state = 'errored'
377 return self._makeerrorresult(_('expected command data frame; '
378 'got %d') % frametype)
379
380 # TODO support streaming data instead of buffering it.
381 self._activedata.write(payload)
382
383 if frameflags & FLAG_COMMAND_DATA_CONTINUATION:
384 return self._makewantframeresult()
385 elif frameflags & FLAG_COMMAND_DATA_EOS:
386 self._activedata.seek(0)
387 self._state = 'idle'
388 return self._makeruncommandresult()
389 else:
390 self._state = 'errored'
391 return self._makeerrorresult(_('command data frame without '
392 'flags'))
393
394 def _onframeerrored(self, frametype, frameflags, payload):
395 return self._makeerrorresult(_('server already errored'))
@@ -19,6 +19,7 b' from . import ('
19 pycompat,
19 pycompat,
20 util,
20 util,
21 wireproto,
21 wireproto,
22 wireprotoframing,
22 wireprototypes,
23 wireprototypes,
23 )
24 )
24
25
@@ -319,6 +320,11 b' def _handlehttpv2request(rctx, req, res,'
319 res.setbodybytes('permission denied')
320 res.setbodybytes('permission denied')
320 return
321 return
321
322
323 # We have a special endpoint to reflect the request back at the client.
324 if command == b'debugreflect':
325 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
326 return
327
322 if command not in wireproto.commands:
328 if command not in wireproto.commands:
323 res.status = b'404 Not Found'
329 res.status = b'404 Not Found'
324 res.headers[b'Content-Type'] = b'text/plain'
330 res.headers[b'Content-Type'] = b'text/plain'
@@ -343,8 +349,7 b' def _handlehttpv2request(rctx, req, res,'
343 % FRAMINGTYPE)
349 % FRAMINGTYPE)
344 return
350 return
345
351
346 if (b'Content-Type' in req.headers
352 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
347 and req.headers[b'Content-Type'] != FRAMINGTYPE):
348 res.status = b'415 Unsupported Media Type'
353 res.status = b'415 Unsupported Media Type'
349 # TODO we should send a response with appropriate media type,
354 # TODO we should send a response with appropriate media type,
350 # since client does Accept it.
355 # since client does Accept it.
@@ -358,6 +363,49 b' def _handlehttpv2request(rctx, req, res,'
358 res.headers[b'Content-Type'] = b'text/plain'
363 res.headers[b'Content-Type'] = b'text/plain'
359 res.setbodybytes(b'/'.join(urlparts) + b'\n')
364 res.setbodybytes(b'/'.join(urlparts) + b'\n')
360
365
366 def _processhttpv2reflectrequest(ui, repo, req, res):
367 """Reads unified frame protocol request and dumps out state to client.
368
369 This special endpoint can be used to help debug the wire protocol.
370
371 Instead of routing the request through the normal dispatch mechanism,
372 we instead read all frames, decode them, and feed them into our state
373 tracker. We then dump the log of all that activity back out to the
374 client.
375 """
376 import json
377
378 # Reflection APIs have a history of being abused, accidentally disclosing
379 # sensitive data, etc. So we have a config knob.
380 if not ui.configbool('experimental', 'web.api.debugreflect'):
381 res.status = b'404 Not Found'
382 res.headers[b'Content-Type'] = b'text/plain'
383 res.setbodybytes(_('debugreflect service not available'))
384 return
385
386 # We assume we have a unified framing protocol request body.
387
388 reactor = wireprotoframing.serverreactor()
389 states = []
390
391 while True:
392 frame = wireprotoframing.readframe(req.bodyfh)
393
394 if not frame:
395 states.append(b'received: <no frame>')
396 break
397
398 frametype, frameflags, payload = frame
399 states.append(b'received: %d %d %s' % (frametype, frameflags, payload))
400
401 action, meta = reactor.onframerecv(frametype, frameflags, payload)
402 states.append(json.dumps((action, meta), sort_keys=True,
403 separators=(', ', ': ')))
404
405 res.status = b'200 OK'
406 res.headers[b'Content-Type'] = b'text/plain'
407 res.setbodybytes(b'\n'.join(states))
408
361 # Maps API name to metadata so custom API can be registered.
409 # Maps API name to metadata so custom API can be registered.
362 API_HANDLERS = {
410 API_HANDLERS = {
363 HTTPV2: {
411 HTTPV2: {
@@ -276,7 +276,7 b' Restart server to allow non-ssl read-wri'
276 > allow-push = *
276 > allow-push = *
277 > EOF
277 > EOF
278
278
279 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
279 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
280 $ cat hg.pid > $DAEMON_PIDS
280 $ cat hg.pid > $DAEMON_PIDS
281
281
282 Authorized request for valid read-write command works
282 Authorized request for valid read-write command works
@@ -329,3 +329,78 b' Authorized request for unknown command i'
329 s> Content-Length: 42\r\n
329 s> Content-Length: 42\r\n
330 s> \r\n
330 s> \r\n
331 s> unknown wire protocol command: badcommand\n
331 s> unknown wire protocol command: badcommand\n
332
333 debugreflect isn't enabled by default
334
335 $ send << EOF
336 > httprequest POST api/$HTTPV2/ro/debugreflect
337 > user-agent: test
338 > EOF
339 using raw connection to peer
340 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
341 s> Accept-Encoding: identity\r\n
342 s> user-agent: test\r\n
343 s> host: $LOCALIP:$HGPORT\r\n (glob)
344 s> \r\n
345 s> makefile('rb', None)
346 s> HTTP/1.1 404 Not Found\r\n
347 s> Server: testing stub value\r\n
348 s> Date: $HTTP_DATE$\r\n
349 s> Content-Type: text/plain\r\n
350 s> Content-Length: 34\r\n
351 s> \r\n
352 s> debugreflect service not available
353
354 Restart server to get debugreflect endpoint
355
356 $ killdaemons.py
357 $ cat > server/.hg/hgrc << EOF
358 > [experimental]
359 > web.apiserver = true
360 > web.api.debugreflect = true
361 > web.api.http-v2 = true
362 > [web]
363 > push_ssl = false
364 > allow-push = *
365 > EOF
366
367 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
368 $ cat hg.pid > $DAEMON_PIDS
369
370 Command frames can be reflected via debugreflect
371
372 $ send << EOF
373 > httprequest POST api/$HTTPV2/ro/debugreflect
374 > accept: $MEDIATYPE
375 > content-type: $MEDIATYPE
376 > user-agent: test
377 > frame command-name have-args command1
378 > frame command-argument 0 \x03\x00\x04\x00fooval1
379 > frame command-argument eoa \x04\x00\x03\x00bar1val
380 > EOF
381 using raw connection to peer
382 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
383 s> Accept-Encoding: identity\r\n
384 s> accept: application/mercurial-exp-framing-0001\r\n
385 s> content-type: application/mercurial-exp-framing-0001\r\n
386 s> user-agent: test\r\n
387 s> content-length: 42\r\n
388 s> host: $LOCALIP:$HGPORT\r\n (glob)
389 s> \r\n
390 s> \x08\x00\x00\x12command1\x0b\x00\x00 \x03\x00\x04\x00fooval1\x0b\x00\x00"\x04\x00\x03\x00bar1val
391 s> makefile('rb', None)
392 s> HTTP/1.1 200 OK\r\n
393 s> Server: testing stub value\r\n
394 s> Date: $HTTP_DATE$\r\n
395 s> Content-Type: text/plain\r\n
396 s> Content-Length: 291\r\n
397 s> \r\n
398 s> received: 1 2 command1\n
399 s> ["wantframe", {"state": "command-receiving-args"}]\n
400 s> received: 2 0 \x03\x00\x04\x00fooval1\n
401 s> ["wantframe", {"state": "command-receiving-args"}]\n
402 s> received: 2 2 \x04\x00\x03\x00bar1val\n
403 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null}]\n
404 s> received: <no frame>
405
406 $ cat error.log
General Comments 0
You need to be logged in to leave comments. Login now