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