##// END OF EJS Templates
httppeer: use try/except/finally
Matt Mackall -
r25085:e05734cd default
parent child Browse files
Show More
@@ -1,275 +1,274 b''
1 1 # httppeer.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from node import nullid
10 10 from i18n import _
11 11 import tempfile
12 12 import changegroup, statichttprepo, error, httpconnection, url, util, wireproto
13 13 import os, urllib, urllib2, zlib, httplib
14 14 import errno, socket
15 15
16 16 def zgenerator(f):
17 17 zd = zlib.decompressobj()
18 18 try:
19 19 for chunk in util.filechunkiter(f):
20 20 while chunk:
21 21 yield zd.decompress(chunk, 2**18)
22 22 chunk = zd.unconsumed_tail
23 23 except httplib.HTTPException:
24 24 raise IOError(None, _('connection ended unexpectedly'))
25 25 yield zd.flush()
26 26
27 27 class httppeer(wireproto.wirepeer):
28 28 def __init__(self, ui, path):
29 29 self.path = path
30 30 self.caps = None
31 31 self.handler = None
32 32 self.urlopener = None
33 33 u = util.url(path)
34 34 if u.query or u.fragment:
35 35 raise util.Abort(_('unsupported URL component: "%s"') %
36 36 (u.query or u.fragment))
37 37
38 38 # urllib cannot handle URLs with embedded user or passwd
39 39 self._url, authinfo = u.authinfo()
40 40
41 41 self.ui = ui
42 42 self.ui.debug('using %s\n' % self._url)
43 43
44 44 self.urlopener = url.opener(ui, authinfo)
45 45
46 46 def __del__(self):
47 47 if self.urlopener:
48 48 for h in self.urlopener.handlers:
49 49 h.close()
50 50 getattr(h, "close_all", lambda : None)()
51 51
52 52 def url(self):
53 53 return self.path
54 54
55 55 # look up capabilities only when needed
56 56
57 57 def _fetchcaps(self):
58 58 self.caps = set(self._call('capabilities').split())
59 59
60 60 def _capabilities(self):
61 61 if self.caps is None:
62 62 try:
63 63 self._fetchcaps()
64 64 except error.RepoError:
65 65 self.caps = set()
66 66 self.ui.debug('capabilities: %s\n' %
67 67 (' '.join(self.caps or ['none'])))
68 68 return self.caps
69 69
70 70 def lock(self):
71 71 raise util.Abort(_('operation not supported over http'))
72 72
73 73 def _callstream(self, cmd, **args):
74 74 if cmd == 'pushkey':
75 75 args['data'] = ''
76 76 data = args.pop('data', None)
77 77 size = 0
78 78 if util.safehasattr(data, 'length'):
79 79 size = data.length
80 80 elif data is not None:
81 81 size = len(data)
82 82 headers = args.pop('headers', {})
83 83 if data is not None and 'Content-Type' not in headers:
84 84 headers['Content-Type'] = 'application/mercurial-0.1'
85 85
86 86
87 87 if size and self.ui.configbool('ui', 'usehttp2', False):
88 88 headers['Expect'] = '100-Continue'
89 89 headers['X-HgHttp2'] = '1'
90 90
91 91 self.ui.debug("sending %s command\n" % cmd)
92 92 q = [('cmd', cmd)]
93 93 headersize = 0
94 94 if len(args) > 0:
95 95 httpheader = self.capable('httpheader')
96 96 if httpheader:
97 97 headersize = int(httpheader.split(',')[0])
98 98 if headersize > 0:
99 99 # The headers can typically carry more data than the URL.
100 100 encargs = urllib.urlencode(sorted(args.items()))
101 101 headerfmt = 'X-HgArg-%s'
102 102 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
103 103 headernum = 0
104 104 for i in xrange(0, len(encargs), contentlen):
105 105 headernum += 1
106 106 header = headerfmt % str(headernum)
107 107 headers[header] = encargs[i:i + contentlen]
108 108 varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)]
109 109 headers['Vary'] = ','.join(varyheaders)
110 110 else:
111 111 q += sorted(args.items())
112 112 qs = '?%s' % urllib.urlencode(q)
113 113 cu = "%s%s" % (self._url, qs)
114 114 req = urllib2.Request(cu, data, headers)
115 115 if data is not None:
116 116 self.ui.debug("sending %s bytes\n" % size)
117 117 req.add_unredirected_header('Content-Length', '%d' % size)
118 118 try:
119 119 resp = self.urlopener.open(req)
120 120 except urllib2.HTTPError, inst:
121 121 if inst.code == 401:
122 122 raise util.Abort(_('authorization failed'))
123 123 raise
124 124 except httplib.HTTPException, inst:
125 125 self.ui.debug('http error while sending %s command\n' % cmd)
126 126 self.ui.traceback()
127 127 raise IOError(None, inst)
128 128 except IndexError:
129 129 # this only happens with Python 2.3, later versions raise URLError
130 130 raise util.Abort(_('http error, possibly caused by proxy setting'))
131 131 # record the url we got redirected to
132 132 resp_url = resp.geturl()
133 133 if resp_url.endswith(qs):
134 134 resp_url = resp_url[:-len(qs)]
135 135 if self._url.rstrip('/') != resp_url.rstrip('/'):
136 136 if not self.ui.quiet:
137 137 self.ui.warn(_('real URL is %s\n') % resp_url)
138 138 self._url = resp_url
139 139 try:
140 140 proto = resp.getheader('content-type')
141 141 except AttributeError:
142 142 proto = resp.headers.get('content-type', '')
143 143
144 144 safeurl = util.hidepassword(self._url)
145 145 if proto.startswith('application/hg-error'):
146 146 raise error.OutOfBandError(resp.read())
147 147 # accept old "text/plain" and "application/hg-changegroup" for now
148 148 if not (proto.startswith('application/mercurial-') or
149 149 (proto.startswith('text/plain')
150 150 and not resp.headers.get('content-length')) or
151 151 proto.startswith('application/hg-changegroup')):
152 152 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
153 153 raise error.RepoError(
154 154 _("'%s' does not appear to be an hg repository:\n"
155 155 "---%%<--- (%s)\n%s\n---%%<---\n")
156 156 % (safeurl, proto or 'no content-type', resp.read(1024)))
157 157
158 158 if proto.startswith('application/mercurial-'):
159 159 try:
160 160 version = proto.split('-', 1)[1]
161 161 version_info = tuple([int(n) for n in version.split('.')])
162 162 except ValueError:
163 163 raise error.RepoError(_("'%s' sent a broken Content-Type "
164 164 "header (%s)") % (safeurl, proto))
165 165 if version_info > (0, 1):
166 166 raise error.RepoError(_("'%s' uses newer protocol %s") %
167 167 (safeurl, version))
168 168
169 169 return resp
170 170
171 171 def _call(self, cmd, **args):
172 172 fp = self._callstream(cmd, **args)
173 173 try:
174 174 return fp.read()
175 175 finally:
176 176 # if using keepalive, allow connection to be reused
177 177 fp.close()
178 178
179 179 def _callpush(self, cmd, cg, **args):
180 180 # have to stream bundle to a temp file because we do not have
181 181 # http 1.1 chunked transfer.
182 182
183 183 types = self.capable('unbundle')
184 184 try:
185 185 types = types.split(',')
186 186 except AttributeError:
187 187 # servers older than d1b16a746db6 will send 'unbundle' as a
188 188 # boolean capability. They only support headerless/uncompressed
189 189 # bundles.
190 190 types = [""]
191 191 for x in types:
192 192 if x in changegroup.bundletypes:
193 193 type = x
194 194 break
195 195
196 196 tempname = changegroup.writebundle(self.ui, cg, None, type)
197 197 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
198 198 headers = {'Content-Type': 'application/mercurial-0.1'}
199 199
200 200 try:
201 try:
202 201 r = self._call(cmd, data=fp, headers=headers, **args)
203 202 vals = r.split('\n', 1)
204 203 if len(vals) < 2:
205 204 raise error.ResponseError(_("unexpected response:"), r)
206 205 return vals
207 206 except socket.error, err:
208 207 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
209 208 raise util.Abort(_('push failed: %s') % err.args[1])
210 209 raise util.Abort(err.args[1])
211 210 finally:
212 211 fp.close()
213 212 os.unlink(tempname)
214 213
215 214 def _calltwowaystream(self, cmd, fp, **args):
216 215 fh = None
217 216 fp_ = None
218 217 filename = None
219 218 try:
220 219 # dump bundle to disk
221 220 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
222 221 fh = os.fdopen(fd, "wb")
223 222 d = fp.read(4096)
224 223 while d:
225 224 fh.write(d)
226 225 d = fp.read(4096)
227 226 fh.close()
228 227 # start http push
229 228 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
230 229 headers = {'Content-Type': 'application/mercurial-0.1'}
231 230 return self._callstream(cmd, data=fp_, headers=headers, **args)
232 231 finally:
233 232 if fp_ is not None:
234 233 fp_.close()
235 234 if fh is not None:
236 235 fh.close()
237 236 os.unlink(filename)
238 237
239 238 def _callcompressable(self, cmd, **args):
240 239 stream = self._callstream(cmd, **args)
241 240 return util.chunkbuffer(zgenerator(stream))
242 241
243 242 def _abort(self, exception):
244 243 raise exception
245 244
246 245 class httpspeer(httppeer):
247 246 def __init__(self, ui, path):
248 247 if not url.has_https:
249 248 raise util.Abort(_('Python support for SSL and HTTPS '
250 249 'is not installed'))
251 250 httppeer.__init__(self, ui, path)
252 251
253 252 def instance(ui, path, create):
254 253 if create:
255 254 raise util.Abort(_('cannot create new http repository'))
256 255 try:
257 256 if path.startswith('https:'):
258 257 inst = httpspeer(ui, path)
259 258 else:
260 259 inst = httppeer(ui, path)
261 260 try:
262 261 # Try to do useful work when checking compatibility.
263 262 # Usually saves a roundtrip since we want the caps anyway.
264 263 inst._fetchcaps()
265 264 except error.RepoError:
266 265 # No luck, try older compatibility check.
267 266 inst.between([(nullid, nullid)])
268 267 return inst
269 268 except error.RepoError, httpexception:
270 269 try:
271 270 r = statichttprepo.instance(ui, "static-" + path, create)
272 271 ui.note('(falling back to static-http)\n')
273 272 return r
274 273 except error.RepoError:
275 274 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now