##// END OF EJS Templates
wireprotov2: advertise redirect targets in capabilities...
Gregory Szorc -
r40059:10cf8b11 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (601 lines changed) Show them Hide them
@@ -0,0 +1,601
1 $ . $TESTDIR/wireprotohelpers.sh
2
3 $ hg init server
4 $ enablehttpv2 server
5 $ cd server
6 $ cat >> .hg/hgrc << EOF
7 > [extensions]
8 > simplecache = $TESTDIR/wireprotosimplecache.py
9 > EOF
10
11 $ echo a0 > a
12 $ echo b0 > b
13 $ hg -q commit -A -m 'commit 0'
14 $ echo a1 > a
15 $ hg commit -m 'commit 1'
16
17 $ hg --debug debugindex -m
18 rev linkrev nodeid p1 p2
19 0 0 992f4779029a3df8d0666d00bb924f69634e2641 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000
20 1 1 a988fb43583e871d1ed5750ee074c6d840bbbfc8 992f4779029a3df8d0666d00bb924f69634e2641 0000000000000000000000000000000000000000
21
22 $ hg --config simplecache.redirectsfile=redirects.py serve -p $HGPORT -d --pid-file hg.pid -E error.log
23 $ cat hg.pid > $DAEMON_PIDS
24
25 $ cat > redirects.py << EOF
26 > [
27 > {
28 > b'name': b'target-a',
29 > b'protocol': b'http',
30 > b'snirequired': False,
31 > b'tlsversions': [b'1.2', b'1.3'],
32 > b'uris': [b'http://example.com/'],
33 > },
34 > ]
35 > EOF
36
37 Redirect targets advertised when configured
38
39 $ sendhttpv2peerhandshake << EOF
40 > command capabilities
41 > EOF
42 creating http peer for wire protocol version 2
43 s> GET /?cmd=capabilities HTTP/1.1\r\n
44 s> Accept-Encoding: identity\r\n
45 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
46 s> x-hgproto-1: cbor\r\n
47 s> x-hgupgrade-1: exp-http-v2-0002\r\n
48 s> accept: application/mercurial-0.1\r\n
49 s> host: $LOCALIP:$HGPORT\r\n (glob)
50 s> user-agent: Mercurial debugwireproto\r\n
51 s> \r\n
52 s> makefile('rb', None)
53 s> HTTP/1.1 200 OK\r\n
54 s> Server: testing stub value\r\n
55 s> Date: $HTTP_DATE$\r\n
56 s> Content-Type: application/mercurial-cbor\r\n
57 s> Content-Length: 1970\r\n
58 s> \r\n
59 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0002\xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa5DnameHtarget-aHprotocolDhttpKsnirequired\xf4Ktlsversions\x82C1.2C1.3Duris\x81Shttp://example.com/Nv1capabilitiesY\x01\xd8batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
60 sending capabilities command
61 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
62 s> Accept-Encoding: identity\r\n
63 s> accept: application/mercurial-exp-framing-0005\r\n
64 s> content-type: application/mercurial-exp-framing-0005\r\n
65 s> content-length: 27\r\n
66 s> host: $LOCALIP:$HGPORT\r\n (glob)
67 s> user-agent: Mercurial debugwireproto\r\n
68 s> \r\n
69 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
70 s> makefile('rb', None)
71 s> HTTP/1.1 200 OK\r\n
72 s> Server: testing stub value\r\n
73 s> Date: $HTTP_DATE$\r\n
74 s> Content-Type: application/mercurial-exp-framing-0005\r\n
75 s> Transfer-Encoding: chunked\r\n
76 s> \r\n
77 s> 13\r\n
78 s> \x0b\x00\x00\x01\x00\x02\x011
79 s> \xa1FstatusBok
80 s> \r\n
81 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
82 s> 5ab\r\n
83 s> \xa3\x05\x00\x01\x00\x02\x001
84 s> \xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa5DnameHtarget-aHprotocolDhttpKsnirequired\xf4Ktlsversions\x82C1.2C1.3Duris\x81Shttp://example.com/
85 s> \r\n
86 received frame(size=1443; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
87 s> 8\r\n
88 s> \x00\x00\x00\x01\x00\x02\x002
89 s> \r\n
90 s> 0\r\n
91 s> \r\n
92 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
93 response: gen[
94 {
95 b'commands': {
96 b'branchmap': {
97 b'args': {},
98 b'permissions': [
99 b'pull'
100 ]
101 },
102 b'capabilities': {
103 b'args': {},
104 b'permissions': [
105 b'pull'
106 ]
107 },
108 b'changesetdata': {
109 b'args': {
110 b'fields': {
111 b'default': set([]),
112 b'required': False,
113 b'type': b'set',
114 b'validvalues': set([
115 b'bookmarks',
116 b'parents',
117 b'phase',
118 b'revision'
119 ])
120 },
121 b'noderange': {
122 b'default': None,
123 b'required': False,
124 b'type': b'list'
125 },
126 b'nodes': {
127 b'default': None,
128 b'required': False,
129 b'type': b'list'
130 },
131 b'nodesdepth': {
132 b'default': None,
133 b'required': False,
134 b'type': b'int'
135 }
136 },
137 b'permissions': [
138 b'pull'
139 ]
140 },
141 b'filedata': {
142 b'args': {
143 b'fields': {
144 b'default': set([]),
145 b'required': False,
146 b'type': b'set',
147 b'validvalues': set([
148 b'parents',
149 b'revision'
150 ])
151 },
152 b'haveparents': {
153 b'default': False,
154 b'required': False,
155 b'type': b'bool'
156 },
157 b'nodes': {
158 b'required': True,
159 b'type': b'list'
160 },
161 b'path': {
162 b'required': True,
163 b'type': b'bytes'
164 }
165 },
166 b'permissions': [
167 b'pull'
168 ]
169 },
170 b'heads': {
171 b'args': {
172 b'publiconly': {
173 b'default': False,
174 b'required': False,
175 b'type': b'bool'
176 }
177 },
178 b'permissions': [
179 b'pull'
180 ]
181 },
182 b'known': {
183 b'args': {
184 b'nodes': {
185 b'default': [],
186 b'required': False,
187 b'type': b'list'
188 }
189 },
190 b'permissions': [
191 b'pull'
192 ]
193 },
194 b'listkeys': {
195 b'args': {
196 b'namespace': {
197 b'required': True,
198 b'type': b'bytes'
199 }
200 },
201 b'permissions': [
202 b'pull'
203 ]
204 },
205 b'lookup': {
206 b'args': {
207 b'key': {
208 b'required': True,
209 b'type': b'bytes'
210 }
211 },
212 b'permissions': [
213 b'pull'
214 ]
215 },
216 b'manifestdata': {
217 b'args': {
218 b'fields': {
219 b'default': set([]),
220 b'required': False,
221 b'type': b'set',
222 b'validvalues': set([
223 b'parents',
224 b'revision'
225 ])
226 },
227 b'haveparents': {
228 b'default': False,
229 b'required': False,
230 b'type': b'bool'
231 },
232 b'nodes': {
233 b'required': True,
234 b'type': b'list'
235 },
236 b'tree': {
237 b'required': True,
238 b'type': b'bytes'
239 }
240 },
241 b'permissions': [
242 b'pull'
243 ]
244 },
245 b'pushkey': {
246 b'args': {
247 b'key': {
248 b'required': True,
249 b'type': b'bytes'
250 },
251 b'namespace': {
252 b'required': True,
253 b'type': b'bytes'
254 },
255 b'new': {
256 b'required': True,
257 b'type': b'bytes'
258 },
259 b'old': {
260 b'required': True,
261 b'type': b'bytes'
262 }
263 },
264 b'permissions': [
265 b'push'
266 ]
267 }
268 },
269 b'compression': [
270 {
271 b'name': b'zstd'
272 },
273 {
274 b'name': b'zlib'
275 }
276 ],
277 b'framingmediatypes': [
278 b'application/mercurial-exp-framing-0005'
279 ],
280 b'pathfilterprefixes': set([
281 b'path:',
282 b'rootfilesin:'
283 ]),
284 b'rawrepoformats': [
285 b'generaldelta',
286 b'revlogv1'
287 ],
288 b'redirect': {
289 b'hashes': [
290 b'sha256',
291 b'sha1'
292 ],
293 b'targets': [
294 {
295 b'name': b'target-a',
296 b'protocol': b'http',
297 b'snirequired': False,
298 b'tlsversions': [
299 b'1.2',
300 b'1.3'
301 ],
302 b'uris': [
303 b'http://example.com/'
304 ]
305 }
306 ]
307 }
308 }
309 ]
310
311 $ cat > redirects.py << EOF
312 > [
313 > {
314 > b'name': b'target-a',
315 > b'protocol': b'http',
316 > b'uris': [b'http://example.com/'],
317 > },
318 > {
319 > b'name': b'target-b',
320 > b'protocol': b'unknown',
321 > b'uris': [b'unknown://example.com/'],
322 > },
323 > ]
324 > EOF
325
326 $ sendhttpv2peerhandshake << EOF
327 > command capabilities
328 > EOF
329 creating http peer for wire protocol version 2
330 s> GET /?cmd=capabilities HTTP/1.1\r\n
331 s> Accept-Encoding: identity\r\n
332 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
333 s> x-hgproto-1: cbor\r\n
334 s> x-hgupgrade-1: exp-http-v2-0002\r\n
335 s> accept: application/mercurial-0.1\r\n
336 s> host: $LOCALIP:$HGPORT\r\n (glob)
337 s> user-agent: Mercurial debugwireproto\r\n
338 s> \r\n
339 s> makefile('rb', None)
340 s> HTTP/1.1 200 OK\r\n
341 s> Server: testing stub value\r\n
342 s> Date: $HTTP_DATE$\r\n
343 s> Content-Type: application/mercurial-cbor\r\n
344 s> Content-Length: 1997\r\n
345 s> \r\n
346 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0002\xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x82\xa3DnameHtarget-aHprotocolDhttpDuris\x81Shttp://example.com/\xa3DnameHtarget-bHprotocolGunknownDuris\x81Vunknown://example.com/Nv1capabilitiesY\x01\xd8batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
347 sending capabilities command
348 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
349 s> Accept-Encoding: identity\r\n
350 s> accept: application/mercurial-exp-framing-0005\r\n
351 s> content-type: application/mercurial-exp-framing-0005\r\n
352 s> content-length: 27\r\n
353 s> host: $LOCALIP:$HGPORT\r\n (glob)
354 s> user-agent: Mercurial debugwireproto\r\n
355 s> \r\n
356 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
357 s> makefile('rb', None)
358 s> HTTP/1.1 200 OK\r\n
359 s> Server: testing stub value\r\n
360 s> Date: $HTTP_DATE$\r\n
361 s> Content-Type: application/mercurial-exp-framing-0005\r\n
362 s> Transfer-Encoding: chunked\r\n
363 s> \r\n
364 s> 13\r\n
365 s> \x0b\x00\x00\x01\x00\x02\x011
366 s> \xa1FstatusBok
367 s> \r\n
368 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
369 s> 5c6\r\n
370 s> \xbe\x05\x00\x01\x00\x02\x001
371 s> \xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x82\xa3DnameHtarget-aHprotocolDhttpDuris\x81Shttp://example.com/\xa3DnameHtarget-bHprotocolGunknownDuris\x81Vunknown://example.com/
372 s> \r\n
373 received frame(size=1470; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
374 s> 8\r\n
375 s> \x00\x00\x00\x01\x00\x02\x002
376 s> \r\n
377 s> 0\r\n
378 s> \r\n
379 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
380 response: gen[
381 {
382 b'commands': {
383 b'branchmap': {
384 b'args': {},
385 b'permissions': [
386 b'pull'
387 ]
388 },
389 b'capabilities': {
390 b'args': {},
391 b'permissions': [
392 b'pull'
393 ]
394 },
395 b'changesetdata': {
396 b'args': {
397 b'fields': {
398 b'default': set([]),
399 b'required': False,
400 b'type': b'set',
401 b'validvalues': set([
402 b'bookmarks',
403 b'parents',
404 b'phase',
405 b'revision'
406 ])
407 },
408 b'noderange': {
409 b'default': None,
410 b'required': False,
411 b'type': b'list'
412 },
413 b'nodes': {
414 b'default': None,
415 b'required': False,
416 b'type': b'list'
417 },
418 b'nodesdepth': {
419 b'default': None,
420 b'required': False,
421 b'type': b'int'
422 }
423 },
424 b'permissions': [
425 b'pull'
426 ]
427 },
428 b'filedata': {
429 b'args': {
430 b'fields': {
431 b'default': set([]),
432 b'required': False,
433 b'type': b'set',
434 b'validvalues': set([
435 b'parents',
436 b'revision'
437 ])
438 },
439 b'haveparents': {
440 b'default': False,
441 b'required': False,
442 b'type': b'bool'
443 },
444 b'nodes': {
445 b'required': True,
446 b'type': b'list'
447 },
448 b'path': {
449 b'required': True,
450 b'type': b'bytes'
451 }
452 },
453 b'permissions': [
454 b'pull'
455 ]
456 },
457 b'heads': {
458 b'args': {
459 b'publiconly': {
460 b'default': False,
461 b'required': False,
462 b'type': b'bool'
463 }
464 },
465 b'permissions': [
466 b'pull'
467 ]
468 },
469 b'known': {
470 b'args': {
471 b'nodes': {
472 b'default': [],
473 b'required': False,
474 b'type': b'list'
475 }
476 },
477 b'permissions': [
478 b'pull'
479 ]
480 },
481 b'listkeys': {
482 b'args': {
483 b'namespace': {
484 b'required': True,
485 b'type': b'bytes'
486 }
487 },
488 b'permissions': [
489 b'pull'
490 ]
491 },
492 b'lookup': {
493 b'args': {
494 b'key': {
495 b'required': True,
496 b'type': b'bytes'
497 }
498 },
499 b'permissions': [
500 b'pull'
501 ]
502 },
503 b'manifestdata': {
504 b'args': {
505 b'fields': {
506 b'default': set([]),
507 b'required': False,
508 b'type': b'set',
509 b'validvalues': set([
510 b'parents',
511 b'revision'
512 ])
513 },
514 b'haveparents': {
515 b'default': False,
516 b'required': False,
517 b'type': b'bool'
518 },
519 b'nodes': {
520 b'required': True,
521 b'type': b'list'
522 },
523 b'tree': {
524 b'required': True,
525 b'type': b'bytes'
526 }
527 },
528 b'permissions': [
529 b'pull'
530 ]
531 },
532 b'pushkey': {
533 b'args': {
534 b'key': {
535 b'required': True,
536 b'type': b'bytes'
537 },
538 b'namespace': {
539 b'required': True,
540 b'type': b'bytes'
541 },
542 b'new': {
543 b'required': True,
544 b'type': b'bytes'
545 },
546 b'old': {
547 b'required': True,
548 b'type': b'bytes'
549 }
550 },
551 b'permissions': [
552 b'push'
553 ]
554 }
555 },
556 b'compression': [
557 {
558 b'name': b'zstd'
559 },
560 {
561 b'name': b'zlib'
562 }
563 ],
564 b'framingmediatypes': [
565 b'application/mercurial-exp-framing-0005'
566 ],
567 b'pathfilterprefixes': set([
568 b'path:',
569 b'rootfilesin:'
570 ]),
571 b'rawrepoformats': [
572 b'generaldelta',
573 b'revlogv1'
574 ],
575 b'redirect': {
576 b'hashes': [
577 b'sha256',
578 b'sha1'
579 ],
580 b'targets': [
581 {
582 b'name': b'target-a',
583 b'protocol': b'http',
584 b'uris': [
585 b'http://example.com/'
586 ]
587 },
588 {
589 b'name': b'target-b',
590 b'protocol': b'unknown',
591 b'uris': [
592 b'unknown://example.com/'
593 ]
594 }
595 ]
596 }
597 }
598 ]
599
600 $ cat error.log
601 $ killdaemons.py
@@ -1,1129 +1,1187
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10 import hashlib
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 hex,
15 15 nullid,
16 16 )
17 17 from . import (
18 18 discovery,
19 19 encoding,
20 20 error,
21 21 narrowspec,
22 22 pycompat,
23 23 streamclone,
24 24 util,
25 25 wireprotoframing,
26 26 wireprototypes,
27 27 )
28 28 from .utils import (
29 29 cborutil,
30 30 interfaceutil,
31 31 stringutil,
32 32 )
33 33
34 34 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
35 35
36 36 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
37 37
38 38 COMMANDS = wireprototypes.commanddict()
39 39
40 40 # Value inserted into cache key computation function. Change the value to
41 41 # force new cache keys for every command request. This should be done when
42 42 # there is a change to how caching works, etc.
43 43 GLOBAL_CACHE_VERSION = 1
44 44
45 45 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
46 46 from .hgweb import common as hgwebcommon
47 47
48 48 # URL space looks like: <permissions>/<command>, where <permission> can
49 49 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
50 50
51 51 # Root URL does nothing meaningful... yet.
52 52 if not urlparts:
53 53 res.status = b'200 OK'
54 54 res.headers[b'Content-Type'] = b'text/plain'
55 55 res.setbodybytes(_('HTTP version 2 API handler'))
56 56 return
57 57
58 58 if len(urlparts) == 1:
59 59 res.status = b'404 Not Found'
60 60 res.headers[b'Content-Type'] = b'text/plain'
61 61 res.setbodybytes(_('do not know how to process %s\n') %
62 62 req.dispatchpath)
63 63 return
64 64
65 65 permission, command = urlparts[0:2]
66 66
67 67 if permission not in (b'ro', b'rw'):
68 68 res.status = b'404 Not Found'
69 69 res.headers[b'Content-Type'] = b'text/plain'
70 70 res.setbodybytes(_('unknown permission: %s') % permission)
71 71 return
72 72
73 73 if req.method != 'POST':
74 74 res.status = b'405 Method Not Allowed'
75 75 res.headers[b'Allow'] = b'POST'
76 76 res.setbodybytes(_('commands require POST requests'))
77 77 return
78 78
79 79 # At some point we'll want to use our own API instead of recycling the
80 80 # behavior of version 1 of the wire protocol...
81 81 # TODO return reasonable responses - not responses that overload the
82 82 # HTTP status line message for error reporting.
83 83 try:
84 84 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
85 85 except hgwebcommon.ErrorResponse as e:
86 86 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
87 87 for k, v in e.headers:
88 88 res.headers[k] = v
89 89 res.setbodybytes('permission denied')
90 90 return
91 91
92 92 # We have a special endpoint to reflect the request back at the client.
93 93 if command == b'debugreflect':
94 94 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
95 95 return
96 96
97 97 # Extra commands that we handle that aren't really wire protocol
98 98 # commands. Think extra hard before making this hackery available to
99 99 # extension.
100 100 extracommands = {'multirequest'}
101 101
102 102 if command not in COMMANDS and command not in extracommands:
103 103 res.status = b'404 Not Found'
104 104 res.headers[b'Content-Type'] = b'text/plain'
105 105 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
106 106 return
107 107
108 108 repo = rctx.repo
109 109 ui = repo.ui
110 110
111 111 proto = httpv2protocolhandler(req, ui)
112 112
113 113 if (not COMMANDS.commandavailable(command, proto)
114 114 and command not in extracommands):
115 115 res.status = b'404 Not Found'
116 116 res.headers[b'Content-Type'] = b'text/plain'
117 117 res.setbodybytes(_('invalid wire protocol command: %s') % command)
118 118 return
119 119
120 120 # TODO consider cases where proxies may add additional Accept headers.
121 121 if req.headers.get(b'Accept') != FRAMINGTYPE:
122 122 res.status = b'406 Not Acceptable'
123 123 res.headers[b'Content-Type'] = b'text/plain'
124 124 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
125 125 % FRAMINGTYPE)
126 126 return
127 127
128 128 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
129 129 res.status = b'415 Unsupported Media Type'
130 130 # TODO we should send a response with appropriate media type,
131 131 # since client does Accept it.
132 132 res.headers[b'Content-Type'] = b'text/plain'
133 133 res.setbodybytes(_('client MUST send Content-Type header with '
134 134 'value: %s\n') % FRAMINGTYPE)
135 135 return
136 136
137 137 _processhttpv2request(ui, repo, req, res, permission, command, proto)
138 138
139 139 def _processhttpv2reflectrequest(ui, repo, req, res):
140 140 """Reads unified frame protocol request and dumps out state to client.
141 141
142 142 This special endpoint can be used to help debug the wire protocol.
143 143
144 144 Instead of routing the request through the normal dispatch mechanism,
145 145 we instead read all frames, decode them, and feed them into our state
146 146 tracker. We then dump the log of all that activity back out to the
147 147 client.
148 148 """
149 149 import json
150 150
151 151 # Reflection APIs have a history of being abused, accidentally disclosing
152 152 # sensitive data, etc. So we have a config knob.
153 153 if not ui.configbool('experimental', 'web.api.debugreflect'):
154 154 res.status = b'404 Not Found'
155 155 res.headers[b'Content-Type'] = b'text/plain'
156 156 res.setbodybytes(_('debugreflect service not available'))
157 157 return
158 158
159 159 # We assume we have a unified framing protocol request body.
160 160
161 161 reactor = wireprotoframing.serverreactor()
162 162 states = []
163 163
164 164 while True:
165 165 frame = wireprotoframing.readframe(req.bodyfh)
166 166
167 167 if not frame:
168 168 states.append(b'received: <no frame>')
169 169 break
170 170
171 171 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
172 172 frame.requestid,
173 173 frame.payload))
174 174
175 175 action, meta = reactor.onframerecv(frame)
176 176 states.append(json.dumps((action, meta), sort_keys=True,
177 177 separators=(', ', ': ')))
178 178
179 179 action, meta = reactor.oninputeof()
180 180 meta['action'] = action
181 181 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
182 182
183 183 res.status = b'200 OK'
184 184 res.headers[b'Content-Type'] = b'text/plain'
185 185 res.setbodybytes(b'\n'.join(states))
186 186
187 187 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
188 188 """Post-validation handler for HTTPv2 requests.
189 189
190 190 Called when the HTTP request contains unified frame-based protocol
191 191 frames for evaluation.
192 192 """
193 193 # TODO Some HTTP clients are full duplex and can receive data before
194 194 # the entire request is transmitted. Figure out a way to indicate support
195 195 # for that so we can opt into full duplex mode.
196 196 reactor = wireprotoframing.serverreactor(deferoutput=True)
197 197 seencommand = False
198 198
199 199 outstream = reactor.makeoutputstream()
200 200
201 201 while True:
202 202 frame = wireprotoframing.readframe(req.bodyfh)
203 203 if not frame:
204 204 break
205 205
206 206 action, meta = reactor.onframerecv(frame)
207 207
208 208 if action == 'wantframe':
209 209 # Need more data before we can do anything.
210 210 continue
211 211 elif action == 'runcommand':
212 212 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
213 213 reqcommand, reactor, outstream,
214 214 meta, issubsequent=seencommand)
215 215
216 216 if sentoutput:
217 217 return
218 218
219 219 seencommand = True
220 220
221 221 elif action == 'error':
222 222 # TODO define proper error mechanism.
223 223 res.status = b'200 OK'
224 224 res.headers[b'Content-Type'] = b'text/plain'
225 225 res.setbodybytes(meta['message'] + b'\n')
226 226 return
227 227 else:
228 228 raise error.ProgrammingError(
229 229 'unhandled action from frame processor: %s' % action)
230 230
231 231 action, meta = reactor.oninputeof()
232 232 if action == 'sendframes':
233 233 # We assume we haven't started sending the response yet. If we're
234 234 # wrong, the response type will raise an exception.
235 235 res.status = b'200 OK'
236 236 res.headers[b'Content-Type'] = FRAMINGTYPE
237 237 res.setbodygen(meta['framegen'])
238 238 elif action == 'noop':
239 239 pass
240 240 else:
241 241 raise error.ProgrammingError('unhandled action from frame processor: %s'
242 242 % action)
243 243
244 244 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
245 245 outstream, command, issubsequent):
246 246 """Dispatch a wire protocol command made from HTTPv2 requests.
247 247
248 248 The authenticated permission (``authedperm``) along with the original
249 249 command from the URL (``reqcommand``) are passed in.
250 250 """
251 251 # We already validated that the session has permissions to perform the
252 252 # actions in ``authedperm``. In the unified frame protocol, the canonical
253 253 # command to run is expressed in a frame. However, the URL also requested
254 254 # to run a specific command. We need to be careful that the command we
255 255 # run doesn't have permissions requirements greater than what was granted
256 256 # by ``authedperm``.
257 257 #
258 258 # Our rule for this is we only allow one command per HTTP request and
259 259 # that command must match the command in the URL. However, we make
260 260 # an exception for the ``multirequest`` URL. This URL is allowed to
261 261 # execute multiple commands. We double check permissions of each command
262 262 # as it is invoked to ensure there is no privilege escalation.
263 263 # TODO consider allowing multiple commands to regular command URLs
264 264 # iff each command is the same.
265 265
266 266 proto = httpv2protocolhandler(req, ui, args=command['args'])
267 267
268 268 if reqcommand == b'multirequest':
269 269 if not COMMANDS.commandavailable(command['command'], proto):
270 270 # TODO proper error mechanism
271 271 res.status = b'200 OK'
272 272 res.headers[b'Content-Type'] = b'text/plain'
273 273 res.setbodybytes(_('wire protocol command not available: %s') %
274 274 command['command'])
275 275 return True
276 276
277 277 # TODO don't use assert here, since it may be elided by -O.
278 278 assert authedperm in (b'ro', b'rw')
279 279 wirecommand = COMMANDS[command['command']]
280 280 assert wirecommand.permission in ('push', 'pull')
281 281
282 282 if authedperm == b'ro' and wirecommand.permission != 'pull':
283 283 # TODO proper error mechanism
284 284 res.status = b'403 Forbidden'
285 285 res.headers[b'Content-Type'] = b'text/plain'
286 286 res.setbodybytes(_('insufficient permissions to execute '
287 287 'command: %s') % command['command'])
288 288 return True
289 289
290 290 # TODO should we also call checkperm() here? Maybe not if we're going
291 291 # to overhaul that API. The granted scope from the URL check should
292 292 # be good enough.
293 293
294 294 else:
295 295 # Don't allow multiple commands outside of ``multirequest`` URL.
296 296 if issubsequent:
297 297 # TODO proper error mechanism
298 298 res.status = b'200 OK'
299 299 res.headers[b'Content-Type'] = b'text/plain'
300 300 res.setbodybytes(_('multiple commands cannot be issued to this '
301 301 'URL'))
302 302 return True
303 303
304 304 if reqcommand != command['command']:
305 305 # TODO define proper error mechanism
306 306 res.status = b'200 OK'
307 307 res.headers[b'Content-Type'] = b'text/plain'
308 308 res.setbodybytes(_('command in frame must match command in URL'))
309 309 return True
310 310
311 311 res.status = b'200 OK'
312 312 res.headers[b'Content-Type'] = FRAMINGTYPE
313 313
314 314 try:
315 315 objs = dispatch(repo, proto, command['command'])
316 316
317 317 action, meta = reactor.oncommandresponsereadyobjects(
318 318 outstream, command['requestid'], objs)
319 319
320 320 except error.WireprotoCommandError as e:
321 321 action, meta = reactor.oncommanderror(
322 322 outstream, command['requestid'], e.message, e.messageargs)
323 323
324 324 except Exception as e:
325 325 action, meta = reactor.onservererror(
326 326 outstream, command['requestid'],
327 327 _('exception when invoking command: %s') %
328 328 stringutil.forcebytestr(e))
329 329
330 330 if action == 'sendframes':
331 331 res.setbodygen(meta['framegen'])
332 332 return True
333 333 elif action == 'noop':
334 334 return False
335 335 else:
336 336 raise error.ProgrammingError('unhandled event from reactor: %s' %
337 337 action)
338 338
339 339 def getdispatchrepo(repo, proto, command):
340 340 return repo.filtered('served')
341 341
342 342 def dispatch(repo, proto, command):
343 343 """Run a wire protocol command.
344 344
345 345 Returns an iterable of objects that will be sent to the client.
346 346 """
347 347 repo = getdispatchrepo(repo, proto, command)
348 348
349 349 entry = COMMANDS[command]
350 350 func = entry.func
351 351 spec = entry.args
352 352
353 353 args = proto.getargs(spec)
354 354
355 355 # There is some duplicate boilerplate code here for calling the command and
356 356 # emitting objects. It is either that or a lot of indented code that looks
357 357 # like a pyramid (since there are a lot of code paths that result in not
358 358 # using the cacher).
359 359 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
360 360
361 361 # Request is not cacheable. Don't bother instantiating a cacher.
362 362 if not entry.cachekeyfn:
363 363 for o in callcommand():
364 364 yield o
365 365 return
366 366
367 367 cacher = makeresponsecacher(repo, proto, command, args,
368 368 cborutil.streamencode)
369 369
370 370 # But we have no cacher. Do default handling.
371 371 if not cacher:
372 372 for o in callcommand():
373 373 yield o
374 374 return
375 375
376 376 with cacher:
377 377 cachekey = entry.cachekeyfn(repo, proto, cacher, **args)
378 378
379 379 # No cache key or the cacher doesn't like it. Do default handling.
380 380 if cachekey is None or not cacher.setcachekey(cachekey):
381 381 for o in callcommand():
382 382 yield o
383 383 return
384 384
385 385 # Serve it from the cache, if possible.
386 386 cached = cacher.lookup()
387 387
388 388 if cached:
389 389 for o in cached['objs']:
390 390 yield o
391 391 return
392 392
393 393 # Else call the command and feed its output into the cacher, allowing
394 394 # the cacher to buffer/mutate objects as it desires.
395 395 for o in callcommand():
396 396 for o in cacher.onobject(o):
397 397 yield o
398 398
399 399 for o in cacher.onfinished():
400 400 yield o
401 401
402 402 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
403 403 class httpv2protocolhandler(object):
404 404 def __init__(self, req, ui, args=None):
405 405 self._req = req
406 406 self._ui = ui
407 407 self._args = args
408 408
409 409 @property
410 410 def name(self):
411 411 return HTTP_WIREPROTO_V2
412 412
413 413 def getargs(self, args):
414 414 # First look for args that were passed but aren't registered on this
415 415 # command.
416 416 extra = set(self._args) - set(args)
417 417 if extra:
418 418 raise error.WireprotoCommandError(
419 419 'unsupported argument to command: %s' %
420 420 ', '.join(sorted(extra)))
421 421
422 422 # And look for required arguments that are missing.
423 423 missing = {a for a in args if args[a]['required']} - set(self._args)
424 424
425 425 if missing:
426 426 raise error.WireprotoCommandError(
427 427 'missing required arguments: %s' % ', '.join(sorted(missing)))
428 428
429 429 # Now derive the arguments to pass to the command, taking into
430 430 # account the arguments specified by the client.
431 431 data = {}
432 432 for k, meta in sorted(args.items()):
433 433 # This argument wasn't passed by the client.
434 434 if k not in self._args:
435 435 data[k] = meta['default']()
436 436 continue
437 437
438 438 v = self._args[k]
439 439
440 440 # Sets may be expressed as lists. Silently normalize.
441 441 if meta['type'] == 'set' and isinstance(v, list):
442 442 v = set(v)
443 443
444 444 # TODO consider more/stronger type validation.
445 445
446 446 data[k] = v
447 447
448 448 return data
449 449
450 450 def getprotocaps(self):
451 451 # Protocol capabilities are currently not implemented for HTTP V2.
452 452 return set()
453 453
454 454 def getpayload(self):
455 455 raise NotImplementedError
456 456
457 457 @contextlib.contextmanager
458 458 def mayberedirectstdio(self):
459 459 raise NotImplementedError
460 460
461 461 def client(self):
462 462 raise NotImplementedError
463 463
464 464 def addcapabilities(self, repo, caps):
465 465 return caps
466 466
467 467 def checkperm(self, perm):
468 468 raise NotImplementedError
469 469
470 470 def httpv2apidescriptor(req, repo):
471 471 proto = httpv2protocolhandler(req, repo.ui)
472 472
473 473 return _capabilitiesv2(repo, proto)
474 474
475 475 def _capabilitiesv2(repo, proto):
476 476 """Obtain the set of capabilities for version 2 transports.
477 477
478 478 These capabilities are distinct from the capabilities for version 1
479 479 transports.
480 480 """
481 481 compression = []
482 482 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
483 483 compression.append({
484 484 b'name': engine.wireprotosupport().name,
485 485 })
486 486
487 487 caps = {
488 488 'commands': {},
489 489 'compression': compression,
490 490 'framingmediatypes': [FRAMINGTYPE],
491 491 'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
492 492 }
493 493
494 494 for command, entry in COMMANDS.items():
495 495 args = {}
496 496
497 497 for arg, meta in entry.args.items():
498 498 args[arg] = {
499 499 # TODO should this be a normalized type using CBOR's
500 500 # terminology?
501 501 b'type': meta['type'],
502 502 b'required': meta['required'],
503 503 }
504 504
505 505 if not meta['required']:
506 506 args[arg][b'default'] = meta['default']()
507 507
508 508 if meta['validvalues']:
509 509 args[arg][b'validvalues'] = meta['validvalues']
510 510
511 511 caps['commands'][command] = {
512 512 'args': args,
513 513 'permissions': [entry.permission],
514 514 }
515 515
516 516 if streamclone.allowservergeneration(repo):
517 517 caps['rawrepoformats'] = sorted(repo.requirements &
518 518 repo.supportedformats)
519 519
520 targets = getadvertisedredirecttargets(repo, proto)
521 if targets:
522 caps[b'redirect'] = {
523 b'targets': [],
524 b'hashes': [b'sha256', b'sha1'],
525 }
526
527 for target in targets:
528 entry = {
529 b'name': target['name'],
530 b'protocol': target['protocol'],
531 b'uris': target['uris'],
532 }
533
534 for key in ('snirequired', 'tlsversions'):
535 if key in target:
536 entry[key] = target[key]
537
538 caps[b'redirect'][b'targets'].append(entry)
539
520 540 return proto.addcapabilities(repo, caps)
521 541
542 def getadvertisedredirecttargets(repo, proto):
543 """Obtain a list of content redirect targets.
544
545 Returns a list containing potential redirect targets that will be
546 advertised in capabilities data. Each dict MUST have the following
547 keys:
548
549 name
550 The name of this redirect target. This is the identifier clients use
551 to refer to a target. It is transferred as part of every command
552 request.
553
554 protocol
555 Network protocol used by this target. Typically this is the string
556 in front of the ``://`` in a URL. e.g. ``https``.
557
558 uris
559 List of representative URIs for this target. Clients can use the
560 URIs to test parsing for compatibility or for ordering preference
561 for which target to use.
562
563 The following optional keys are recognized:
564
565 snirequired
566 Bool indicating if Server Name Indication (SNI) is required to
567 connect to this target.
568
569 tlsversions
570 List of bytes indicating which TLS versions are supported by this
571 target.
572
573 By default, clients reflect the target order advertised by servers
574 and servers will use the first client-advertised target when picking
575 a redirect target. So targets should be advertised in the order the
576 server prefers they be used.
577 """
578 return []
579
522 580 def wireprotocommand(name, args=None, permission='push', cachekeyfn=None):
523 581 """Decorator to declare a wire protocol command.
524 582
525 583 ``name`` is the name of the wire protocol command being provided.
526 584
527 585 ``args`` is a dict defining arguments accepted by the command. Keys are
528 586 the argument name. Values are dicts with the following keys:
529 587
530 588 ``type``
531 589 The argument data type. Must be one of the following string
532 590 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
533 591 or ``bool``.
534 592
535 593 ``default``
536 594 A callable returning the default value for this argument. If not
537 595 specified, ``None`` will be the default value.
538 596
539 597 ``example``
540 598 An example value for this argument.
541 599
542 600 ``validvalues``
543 601 Set of recognized values for this argument.
544 602
545 603 ``permission`` defines the permission type needed to run this command.
546 604 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
547 605 respectively. Default is to assume command requires ``push`` permissions
548 606 because otherwise commands not declaring their permissions could modify
549 607 a repository that is supposed to be read-only.
550 608
551 609 ``cachekeyfn`` defines an optional callable that can derive the
552 610 cache key for this request.
553 611
554 612 Wire protocol commands are generators of objects to be serialized and
555 613 sent to the client.
556 614
557 615 If a command raises an uncaught exception, this will be translated into
558 616 a command error.
559 617
560 618 All commands can opt in to being cacheable by defining a function
561 619 (``cachekeyfn``) that is called to derive a cache key. This function
562 620 receives the same arguments as the command itself plus a ``cacher``
563 621 argument containing the active cacher for the request and returns a bytes
564 622 containing the key in a cache the response to this command may be cached
565 623 under.
566 624 """
567 625 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
568 626 if v['version'] == 2}
569 627
570 628 if permission not in ('push', 'pull'):
571 629 raise error.ProgrammingError('invalid wire protocol permission; '
572 630 'got %s; expected "push" or "pull"' %
573 631 permission)
574 632
575 633 if args is None:
576 634 args = {}
577 635
578 636 if not isinstance(args, dict):
579 637 raise error.ProgrammingError('arguments for version 2 commands '
580 638 'must be declared as dicts')
581 639
582 640 for arg, meta in args.items():
583 641 if arg == '*':
584 642 raise error.ProgrammingError('* argument name not allowed on '
585 643 'version 2 commands')
586 644
587 645 if not isinstance(meta, dict):
588 646 raise error.ProgrammingError('arguments for version 2 commands '
589 647 'must declare metadata as a dict')
590 648
591 649 if 'type' not in meta:
592 650 raise error.ProgrammingError('%s argument for command %s does not '
593 651 'declare type field' % (arg, name))
594 652
595 653 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'):
596 654 raise error.ProgrammingError('%s argument for command %s has '
597 655 'illegal type: %s' % (arg, name,
598 656 meta['type']))
599 657
600 658 if 'example' not in meta:
601 659 raise error.ProgrammingError('%s argument for command %s does not '
602 660 'declare example field' % (arg, name))
603 661
604 662 meta['required'] = 'default' not in meta
605 663
606 664 meta.setdefault('default', lambda: None)
607 665 meta.setdefault('validvalues', None)
608 666
609 667 def register(func):
610 668 if name in COMMANDS:
611 669 raise error.ProgrammingError('%s command already registered '
612 670 'for version 2' % name)
613 671
614 672 COMMANDS[name] = wireprototypes.commandentry(
615 673 func, args=args, transports=transports, permission=permission,
616 674 cachekeyfn=cachekeyfn)
617 675
618 676 return func
619 677
620 678 return register
621 679
622 680 def makecommandcachekeyfn(command, localversion=None, allargs=False):
623 681 """Construct a cache key derivation function with common features.
624 682
625 683 By default, the cache key is a hash of:
626 684
627 685 * The command name.
628 686 * A global cache version number.
629 687 * A local cache version number (passed via ``localversion``).
630 688 * All the arguments passed to the command.
631 689 * The media type used.
632 690 * Wire protocol version string.
633 691 * The repository path.
634 692 """
635 693 if not allargs:
636 694 raise error.ProgrammingError('only allargs=True is currently supported')
637 695
638 696 if localversion is None:
639 697 raise error.ProgrammingError('must set localversion argument value')
640 698
641 699 def cachekeyfn(repo, proto, cacher, **args):
642 700 spec = COMMANDS[command]
643 701
644 702 # Commands that mutate the repo can not be cached.
645 703 if spec.permission == 'push':
646 704 return None
647 705
648 706 # TODO config option to disable caching.
649 707
650 708 # Our key derivation strategy is to construct a data structure
651 709 # holding everything that could influence cacheability and to hash
652 710 # the CBOR representation of that. Using CBOR seems like it might
653 711 # be overkill. However, simpler hashing mechanisms are prone to
654 712 # duplicate input issues. e.g. if you just concatenate two values,
655 713 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
656 714 # "padding" between values and prevents these problems.
657 715
658 716 # Seed the hash with various data.
659 717 state = {
660 718 # To invalidate all cache keys.
661 719 b'globalversion': GLOBAL_CACHE_VERSION,
662 720 # More granular cache key invalidation.
663 721 b'localversion': localversion,
664 722 # Cache keys are segmented by command.
665 723 b'command': pycompat.sysbytes(command),
666 724 # Throw in the media type and API version strings so changes
667 725 # to exchange semantics invalid cache.
668 726 b'mediatype': FRAMINGTYPE,
669 727 b'version': HTTP_WIREPROTO_V2,
670 728 # So same requests for different repos don't share cache keys.
671 729 b'repo': repo.root,
672 730 }
673 731
674 732 # The arguments passed to us will have already been normalized.
675 733 # Default values will be set, etc. This is important because it
676 734 # means that it doesn't matter if clients send an explicit argument
677 735 # or rely on the default value: it will all normalize to the same
678 736 # set of arguments on the server and therefore the same cache key.
679 737 #
680 738 # Arguments by their very nature must support being encoded to CBOR.
681 739 # And the CBOR encoder is deterministic. So we hash the arguments
682 740 # by feeding the CBOR of their representation into the hasher.
683 741 if allargs:
684 742 state[b'args'] = pycompat.byteskwargs(args)
685 743
686 744 cacher.adjustcachekeystate(state)
687 745
688 746 hasher = hashlib.sha1()
689 747 for chunk in cborutil.streamencode(state):
690 748 hasher.update(chunk)
691 749
692 750 return pycompat.sysbytes(hasher.hexdigest())
693 751
694 752 return cachekeyfn
695 753
696 754 def makeresponsecacher(repo, proto, command, args, objencoderfn):
697 755 """Construct a cacher for a cacheable command.
698 756
699 757 Returns an ``iwireprotocolcommandcacher`` instance.
700 758
701 759 Extensions can monkeypatch this function to provide custom caching
702 760 backends.
703 761 """
704 762 return None
705 763
706 764 @wireprotocommand('branchmap', permission='pull')
707 765 def branchmapv2(repo, proto):
708 766 yield {encoding.fromlocal(k): v
709 767 for k, v in repo.branchmap().iteritems()}
710 768
711 769 @wireprotocommand('capabilities', permission='pull')
712 770 def capabilitiesv2(repo, proto):
713 771 yield _capabilitiesv2(repo, proto)
714 772
715 773 @wireprotocommand(
716 774 'changesetdata',
717 775 args={
718 776 'noderange': {
719 777 'type': 'list',
720 778 'default': lambda: None,
721 779 'example': [[b'0123456...'], [b'abcdef...']],
722 780 },
723 781 'nodes': {
724 782 'type': 'list',
725 783 'default': lambda: None,
726 784 'example': [b'0123456...'],
727 785 },
728 786 'nodesdepth': {
729 787 'type': 'int',
730 788 'default': lambda: None,
731 789 'example': 10,
732 790 },
733 791 'fields': {
734 792 'type': 'set',
735 793 'default': set,
736 794 'example': {b'parents', b'revision'},
737 795 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
738 796 },
739 797 },
740 798 permission='pull')
741 799 def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields):
742 800 # TODO look for unknown fields and abort when they can't be serviced.
743 801 # This could probably be validated by dispatcher using validvalues.
744 802
745 803 if noderange is None and nodes is None:
746 804 raise error.WireprotoCommandError(
747 805 'noderange or nodes must be defined')
748 806
749 807 if nodesdepth is not None and nodes is None:
750 808 raise error.WireprotoCommandError(
751 809 'nodesdepth requires the nodes argument')
752 810
753 811 if noderange is not None:
754 812 if len(noderange) != 2:
755 813 raise error.WireprotoCommandError(
756 814 'noderange must consist of 2 elements')
757 815
758 816 if not noderange[1]:
759 817 raise error.WireprotoCommandError(
760 818 'heads in noderange request cannot be empty')
761 819
762 820 cl = repo.changelog
763 821 hasnode = cl.hasnode
764 822
765 823 seen = set()
766 824 outgoing = []
767 825
768 826 if nodes is not None:
769 827 outgoing = [n for n in nodes if hasnode(n)]
770 828
771 829 if nodesdepth:
772 830 outgoing = [cl.node(r) for r in
773 831 repo.revs(b'ancestors(%ln, %d)', outgoing,
774 832 nodesdepth - 1)]
775 833
776 834 seen |= set(outgoing)
777 835
778 836 if noderange is not None:
779 837 if noderange[0]:
780 838 common = [n for n in noderange[0] if hasnode(n)]
781 839 else:
782 840 common = [nullid]
783 841
784 842 for n in discovery.outgoing(repo, common, noderange[1]).missing:
785 843 if n not in seen:
786 844 outgoing.append(n)
787 845 # Don't need to add to seen here because this is the final
788 846 # source of nodes and there should be no duplicates in this
789 847 # list.
790 848
791 849 seen.clear()
792 850 publishing = repo.publishing()
793 851
794 852 if outgoing:
795 853 repo.hook('preoutgoing', throw=True, source='serve')
796 854
797 855 yield {
798 856 b'totalitems': len(outgoing),
799 857 }
800 858
801 859 # The phases of nodes already transferred to the client may have changed
802 860 # since the client last requested data. We send phase-only records
803 861 # for these revisions, if requested.
804 862 if b'phase' in fields and noderange is not None:
805 863 # TODO skip nodes whose phase will be reflected by a node in the
806 864 # outgoing set. This is purely an optimization to reduce data
807 865 # size.
808 866 for node in noderange[0]:
809 867 yield {
810 868 b'node': node,
811 869 b'phase': b'public' if publishing else repo[node].phasestr()
812 870 }
813 871
814 872 nodebookmarks = {}
815 873 for mark, node in repo._bookmarks.items():
816 874 nodebookmarks.setdefault(node, set()).add(mark)
817 875
818 876 # It is already topologically sorted by revision number.
819 877 for node in outgoing:
820 878 d = {
821 879 b'node': node,
822 880 }
823 881
824 882 if b'parents' in fields:
825 883 d[b'parents'] = cl.parents(node)
826 884
827 885 if b'phase' in fields:
828 886 if publishing:
829 887 d[b'phase'] = b'public'
830 888 else:
831 889 ctx = repo[node]
832 890 d[b'phase'] = ctx.phasestr()
833 891
834 892 if b'bookmarks' in fields and node in nodebookmarks:
835 893 d[b'bookmarks'] = sorted(nodebookmarks[node])
836 894 del nodebookmarks[node]
837 895
838 896 followingmeta = []
839 897 followingdata = []
840 898
841 899 if b'revision' in fields:
842 900 revisiondata = cl.revision(node, raw=True)
843 901 followingmeta.append((b'revision', len(revisiondata)))
844 902 followingdata.append(revisiondata)
845 903
846 904 # TODO make it possible for extensions to wrap a function or register
847 905 # a handler to service custom fields.
848 906
849 907 if followingmeta:
850 908 d[b'fieldsfollowing'] = followingmeta
851 909
852 910 yield d
853 911
854 912 for extra in followingdata:
855 913 yield extra
856 914
857 915 # If requested, send bookmarks from nodes that didn't have revision
858 916 # data sent so receiver is aware of any bookmark updates.
859 917 if b'bookmarks' in fields:
860 918 for node, marks in sorted(nodebookmarks.iteritems()):
861 919 yield {
862 920 b'node': node,
863 921 b'bookmarks': sorted(marks),
864 922 }
865 923
866 924 class FileAccessError(Exception):
867 925 """Represents an error accessing a specific file."""
868 926
869 927 def __init__(self, path, msg, args):
870 928 self.path = path
871 929 self.msg = msg
872 930 self.args = args
873 931
874 932 def getfilestore(repo, proto, path):
875 933 """Obtain a file storage object for use with wire protocol.
876 934
877 935 Exists as a standalone function so extensions can monkeypatch to add
878 936 access control.
879 937 """
880 938 # This seems to work even if the file doesn't exist. So catch
881 939 # "empty" files and return an error.
882 940 fl = repo.file(path)
883 941
884 942 if not len(fl):
885 943 raise FileAccessError(path, 'unknown file: %s', (path,))
886 944
887 945 return fl
888 946
889 947 @wireprotocommand(
890 948 'filedata',
891 949 args={
892 950 'haveparents': {
893 951 'type': 'bool',
894 952 'default': lambda: False,
895 953 'example': True,
896 954 },
897 955 'nodes': {
898 956 'type': 'list',
899 957 'example': [b'0123456...'],
900 958 },
901 959 'fields': {
902 960 'type': 'set',
903 961 'default': set,
904 962 'example': {b'parents', b'revision'},
905 963 'validvalues': {b'parents', b'revision'},
906 964 },
907 965 'path': {
908 966 'type': 'bytes',
909 967 'example': b'foo.txt',
910 968 }
911 969 },
912 970 permission='pull',
913 971 # TODO censoring a file revision won't invalidate the cache.
914 972 # Figure out a way to take censoring into account when deriving
915 973 # the cache key.
916 974 cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True))
917 975 def filedata(repo, proto, haveparents, nodes, fields, path):
918 976 try:
919 977 # Extensions may wish to access the protocol handler.
920 978 store = getfilestore(repo, proto, path)
921 979 except FileAccessError as e:
922 980 raise error.WireprotoCommandError(e.msg, e.args)
923 981
924 982 # Validate requested nodes.
925 983 for node in nodes:
926 984 try:
927 985 store.rev(node)
928 986 except error.LookupError:
929 987 raise error.WireprotoCommandError('unknown file node: %s',
930 988 (hex(node),))
931 989
932 990 revisions = store.emitrevisions(nodes,
933 991 revisiondata=b'revision' in fields,
934 992 assumehaveparentrevisions=haveparents)
935 993
936 994 yield {
937 995 b'totalitems': len(nodes),
938 996 }
939 997
940 998 for revision in revisions:
941 999 d = {
942 1000 b'node': revision.node,
943 1001 }
944 1002
945 1003 if b'parents' in fields:
946 1004 d[b'parents'] = [revision.p1node, revision.p2node]
947 1005
948 1006 followingmeta = []
949 1007 followingdata = []
950 1008
951 1009 if b'revision' in fields:
952 1010 if revision.revision is not None:
953 1011 followingmeta.append((b'revision', len(revision.revision)))
954 1012 followingdata.append(revision.revision)
955 1013 else:
956 1014 d[b'deltabasenode'] = revision.basenode
957 1015 followingmeta.append((b'delta', len(revision.delta)))
958 1016 followingdata.append(revision.delta)
959 1017
960 1018 if followingmeta:
961 1019 d[b'fieldsfollowing'] = followingmeta
962 1020
963 1021 yield d
964 1022
965 1023 for extra in followingdata:
966 1024 yield extra
967 1025
968 1026 @wireprotocommand(
969 1027 'heads',
970 1028 args={
971 1029 'publiconly': {
972 1030 'type': 'bool',
973 1031 'default': lambda: False,
974 1032 'example': False,
975 1033 },
976 1034 },
977 1035 permission='pull')
978 1036 def headsv2(repo, proto, publiconly):
979 1037 if publiconly:
980 1038 repo = repo.filtered('immutable')
981 1039
982 1040 yield repo.heads()
983 1041
984 1042 @wireprotocommand(
985 1043 'known',
986 1044 args={
987 1045 'nodes': {
988 1046 'type': 'list',
989 1047 'default': list,
990 1048 'example': [b'deadbeef'],
991 1049 },
992 1050 },
993 1051 permission='pull')
994 1052 def knownv2(repo, proto, nodes):
995 1053 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
996 1054 yield result
997 1055
998 1056 @wireprotocommand(
999 1057 'listkeys',
1000 1058 args={
1001 1059 'namespace': {
1002 1060 'type': 'bytes',
1003 1061 'example': b'ns',
1004 1062 },
1005 1063 },
1006 1064 permission='pull')
1007 1065 def listkeysv2(repo, proto, namespace):
1008 1066 keys = repo.listkeys(encoding.tolocal(namespace))
1009 1067 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
1010 1068 for k, v in keys.iteritems()}
1011 1069
1012 1070 yield keys
1013 1071
1014 1072 @wireprotocommand(
1015 1073 'lookup',
1016 1074 args={
1017 1075 'key': {
1018 1076 'type': 'bytes',
1019 1077 'example': b'foo',
1020 1078 },
1021 1079 },
1022 1080 permission='pull')
1023 1081 def lookupv2(repo, proto, key):
1024 1082 key = encoding.tolocal(key)
1025 1083
1026 1084 # TODO handle exception.
1027 1085 node = repo.lookup(key)
1028 1086
1029 1087 yield node
1030 1088
1031 1089 @wireprotocommand(
1032 1090 'manifestdata',
1033 1091 args={
1034 1092 'nodes': {
1035 1093 'type': 'list',
1036 1094 'example': [b'0123456...'],
1037 1095 },
1038 1096 'haveparents': {
1039 1097 'type': 'bool',
1040 1098 'default': lambda: False,
1041 1099 'example': True,
1042 1100 },
1043 1101 'fields': {
1044 1102 'type': 'set',
1045 1103 'default': set,
1046 1104 'example': {b'parents', b'revision'},
1047 1105 'validvalues': {b'parents', b'revision'},
1048 1106 },
1049 1107 'tree': {
1050 1108 'type': 'bytes',
1051 1109 'example': b'',
1052 1110 },
1053 1111 },
1054 1112 permission='pull',
1055 1113 cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True))
1056 1114 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1057 1115 store = repo.manifestlog.getstorage(tree)
1058 1116
1059 1117 # Validate the node is known and abort on unknown revisions.
1060 1118 for node in nodes:
1061 1119 try:
1062 1120 store.rev(node)
1063 1121 except error.LookupError:
1064 1122 raise error.WireprotoCommandError(
1065 1123 'unknown node: %s', (node,))
1066 1124
1067 1125 revisions = store.emitrevisions(nodes,
1068 1126 revisiondata=b'revision' in fields,
1069 1127 assumehaveparentrevisions=haveparents)
1070 1128
1071 1129 yield {
1072 1130 b'totalitems': len(nodes),
1073 1131 }
1074 1132
1075 1133 for revision in revisions:
1076 1134 d = {
1077 1135 b'node': revision.node,
1078 1136 }
1079 1137
1080 1138 if b'parents' in fields:
1081 1139 d[b'parents'] = [revision.p1node, revision.p2node]
1082 1140
1083 1141 followingmeta = []
1084 1142 followingdata = []
1085 1143
1086 1144 if b'revision' in fields:
1087 1145 if revision.revision is not None:
1088 1146 followingmeta.append((b'revision', len(revision.revision)))
1089 1147 followingdata.append(revision.revision)
1090 1148 else:
1091 1149 d[b'deltabasenode'] = revision.basenode
1092 1150 followingmeta.append((b'delta', len(revision.delta)))
1093 1151 followingdata.append(revision.delta)
1094 1152
1095 1153 if followingmeta:
1096 1154 d[b'fieldsfollowing'] = followingmeta
1097 1155
1098 1156 yield d
1099 1157
1100 1158 for extra in followingdata:
1101 1159 yield extra
1102 1160
1103 1161 @wireprotocommand(
1104 1162 'pushkey',
1105 1163 args={
1106 1164 'namespace': {
1107 1165 'type': 'bytes',
1108 1166 'example': b'ns',
1109 1167 },
1110 1168 'key': {
1111 1169 'type': 'bytes',
1112 1170 'example': b'key',
1113 1171 },
1114 1172 'old': {
1115 1173 'type': 'bytes',
1116 1174 'example': b'old',
1117 1175 },
1118 1176 'new': {
1119 1177 'type': 'bytes',
1120 1178 'example': 'new',
1121 1179 },
1122 1180 },
1123 1181 permission='push')
1124 1182 def pushkeyv2(repo, proto, namespace, key, old, new):
1125 1183 # TODO handle ui output redirection
1126 1184 yield repo.pushkey(encoding.tolocal(namespace),
1127 1185 encoding.tolocal(key),
1128 1186 encoding.tolocal(old),
1129 1187 encoding.tolocal(new))
@@ -1,100 +1,118
1 1 # wireprotosimplecache.py - Extension providing in-memory wire protocol cache
2 2 #
3 3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from mercurial import (
11 11 extensions,
12 12 registrar,
13 13 repository,
14 14 util,
15 15 wireprototypes,
16 16 wireprotov2server,
17 17 )
18 18 from mercurial.utils import (
19 19 interfaceutil,
20 stringutil,
20 21 )
21 22
22 23 CACHE = None
23 24
24 25 configtable = {}
25 26 configitem = registrar.configitem(configtable)
26 27
27 28 configitem('simplecache', 'cacheobjects',
28 29 default=False)
30 configitem('simplecache', 'redirectsfile',
31 default=None)
29 32
30 33 @interfaceutil.implementer(repository.iwireprotocolcommandcacher)
31 34 class memorycacher(object):
32 35 def __init__(self, ui, command, encodefn):
33 36 self.ui = ui
34 37 self.encodefn = encodefn
35 38 self.key = None
36 39 self.cacheobjects = ui.configbool('simplecache', 'cacheobjects')
37 40 self.buffered = []
38 41
39 42 ui.log('simplecache', 'cacher constructed for %s\n', command)
40 43
41 44 def __enter__(self):
42 45 return self
43 46
44 47 def __exit__(self, exctype, excvalue, exctb):
45 48 if exctype:
46 49 self.ui.log('simplecache', 'cacher exiting due to error\n')
47 50
48 51 def adjustcachekeystate(self, state):
49 52 # Needed in order to make tests deterministic. Don't copy this
50 53 # pattern for production caches!
51 54 del state[b'repo']
52 55
53 56 def setcachekey(self, key):
54 57 self.key = key
55 58 return True
56 59
57 60 def lookup(self):
58 61 if self.key not in CACHE:
59 62 self.ui.log('simplecache', 'cache miss for %s\n', self.key)
60 63 return None
61 64
62 65 entry = CACHE[self.key]
63 66 self.ui.log('simplecache', 'cache hit for %s\n', self.key)
64 67
65 68 if self.cacheobjects:
66 69 return {
67 70 'objs': entry,
68 71 }
69 72 else:
70 73 return {
71 74 'objs': [wireprototypes.encodedresponse(entry)],
72 75 }
73 76
74 77 def onobject(self, obj):
75 78 if self.cacheobjects:
76 79 self.buffered.append(obj)
77 80 else:
78 81 self.buffered.extend(self.encodefn(obj))
79 82
80 83 yield obj
81 84
82 85 def onfinished(self):
83 86 self.ui.log('simplecache', 'storing cache entry for %s\n', self.key)
84 87 if self.cacheobjects:
85 88 CACHE[self.key] = self.buffered
86 89 else:
87 90 CACHE[self.key] = b''.join(self.buffered)
88 91
89 92 return []
90 93
91 94 def makeresponsecacher(orig, repo, proto, command, args, objencoderfn):
92 95 return memorycacher(repo.ui, command, objencoderfn)
93 96
97 def loadredirecttargets(ui):
98 path = ui.config('simplecache', 'redirectsfile')
99 if not path:
100 return []
101
102 with open(path, 'rb') as fh:
103 s = fh.read()
104
105 return stringutil.evalpythonliteral(s)
106
107 def getadvertisedredirecttargets(orig, repo, proto):
108 return loadredirecttargets(repo.ui)
109
94 110 def extsetup(ui):
95 111 global CACHE
96 112
97 113 CACHE = util.lrucachedict(10000)
98 114
99 115 extensions.wrapfunction(wireprotov2server, 'makeresponsecacher',
100 116 makeresponsecacher)
117 extensions.wrapfunction(wireprotov2server, 'getadvertisedredirecttargets',
118 getadvertisedredirecttargets)
General Comments 0
You need to be logged in to leave comments. Login now