##// END OF EJS Templates
sshserver: do setbinary() by caller (API)...
Yuya Nishihara -
r37963:dc1ed7fe default
parent child Browse files
Show More
@@ -1,93 +1,96
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
3 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
4 #
4 #
5 # Author(s):
5 # Author(s):
6 # Thomas Arendsen Hein <thomas@intevation.de>
6 # Thomas Arendsen Hein <thomas@intevation.de>
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10
10
11 """
11 """
12 hg-ssh - a wrapper for ssh access to a limited set of mercurial repos
12 hg-ssh - a wrapper for ssh access to a limited set of mercurial repos
13
13
14 To be used in ~/.ssh/authorized_keys with the "command" option, see sshd(8):
14 To be used in ~/.ssh/authorized_keys with the "command" option, see sshd(8):
15 command="hg-ssh path/to/repo1 /path/to/repo2 ~/repo3 ~user/repo4" ssh-dss ...
15 command="hg-ssh path/to/repo1 /path/to/repo2 ~/repo3 ~user/repo4" ssh-dss ...
16 (probably together with these other useful options:
16 (probably together with these other useful options:
17 no-port-forwarding,no-X11-forwarding,no-agent-forwarding)
17 no-port-forwarding,no-X11-forwarding,no-agent-forwarding)
18
18
19 This allows pull/push over ssh from/to the repositories given as arguments.
19 This allows pull/push over ssh from/to the repositories given as arguments.
20
20
21 If all your repositories are subdirectories of a common directory, you can
21 If all your repositories are subdirectories of a common directory, you can
22 allow shorter paths with:
22 allow shorter paths with:
23 command="cd path/to/my/repositories && hg-ssh repo1 subdir/repo2"
23 command="cd path/to/my/repositories && hg-ssh repo1 subdir/repo2"
24
24
25 You can use pattern matching of your normal shell, e.g.:
25 You can use pattern matching of your normal shell, e.g.:
26 command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}"
26 command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}"
27
27
28 You can also add a --read-only flag to allow read-only access to a key, e.g.:
28 You can also add a --read-only flag to allow read-only access to a key, e.g.:
29 command="hg-ssh --read-only repos/*"
29 command="hg-ssh --read-only repos/*"
30 """
30 """
31 from __future__ import absolute_import
31 from __future__ import absolute_import
32
32
33 import os
33 import os
34 import shlex
34 import shlex
35 import sys
35 import sys
36
36
37 # enable importing on demand to reduce startup time
37 # enable importing on demand to reduce startup time
38 import hgdemandimport ; hgdemandimport.enable()
38 import hgdemandimport ; hgdemandimport.enable()
39
39
40 from mercurial import (
40 from mercurial import (
41 dispatch,
41 dispatch,
42 ui as uimod,
42 ui as uimod,
43 )
43 )
44
44
45 def main():
45 def main():
46 # Prevent insertion/deletion of CRs
47 dispatch.initstdio()
48
46 cwd = os.getcwd()
49 cwd = os.getcwd()
47 readonly = False
50 readonly = False
48 args = sys.argv[1:]
51 args = sys.argv[1:]
49 while len(args):
52 while len(args):
50 if args[0] == '--read-only':
53 if args[0] == '--read-only':
51 readonly = True
54 readonly = True
52 args.pop(0)
55 args.pop(0)
53 else:
56 else:
54 break
57 break
55 allowed_paths = [os.path.normpath(os.path.join(cwd,
58 allowed_paths = [os.path.normpath(os.path.join(cwd,
56 os.path.expanduser(path)))
59 os.path.expanduser(path)))
57 for path in args]
60 for path in args]
58 orig_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '?')
61 orig_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '?')
59 try:
62 try:
60 cmdargv = shlex.split(orig_cmd)
63 cmdargv = shlex.split(orig_cmd)
61 except ValueError as e:
64 except ValueError as e:
62 sys.stderr.write('Illegal command "%s": %s\n' % (orig_cmd, e))
65 sys.stderr.write('Illegal command "%s": %s\n' % (orig_cmd, e))
63 sys.exit(255)
66 sys.exit(255)
64
67
65 if cmdargv[:2] == ['hg', '-R'] and cmdargv[3:] == ['serve', '--stdio']:
68 if cmdargv[:2] == ['hg', '-R'] and cmdargv[3:] == ['serve', '--stdio']:
66 path = cmdargv[2]
69 path = cmdargv[2]
67 repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
70 repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
68 if repo in allowed_paths:
71 if repo in allowed_paths:
69 cmd = ['-R', repo, 'serve', '--stdio']
72 cmd = ['-R', repo, 'serve', '--stdio']
70 req = dispatch.request(cmd)
73 req = dispatch.request(cmd)
71 if readonly:
74 if readonly:
72 if not req.ui:
75 if not req.ui:
73 req.ui = uimod.ui.load()
76 req.ui = uimod.ui.load()
74 req.ui.setconfig('hooks', 'pretxnopen.hg-ssh',
77 req.ui.setconfig('hooks', 'pretxnopen.hg-ssh',
75 'python:__main__.rejectpush', 'hg-ssh')
78 'python:__main__.rejectpush', 'hg-ssh')
76 req.ui.setconfig('hooks', 'prepushkey.hg-ssh',
79 req.ui.setconfig('hooks', 'prepushkey.hg-ssh',
77 'python:__main__.rejectpush', 'hg-ssh')
80 'python:__main__.rejectpush', 'hg-ssh')
78 dispatch.dispatch(req)
81 dispatch.dispatch(req)
79 else:
82 else:
80 sys.stderr.write('Illegal repository "%s"\n' % repo)
83 sys.stderr.write('Illegal repository "%s"\n' % repo)
81 sys.exit(255)
84 sys.exit(255)
82 else:
85 else:
83 sys.stderr.write('Illegal command "%s"\n' % orig_cmd)
86 sys.stderr.write('Illegal command "%s"\n' % orig_cmd)
84 sys.exit(255)
87 sys.exit(255)
85
88
86 def rejectpush(ui, **kwargs):
89 def rejectpush(ui, **kwargs):
87 ui.warn(("Permission denied\n"))
90 ui.warn(("Permission denied\n"))
88 # mercurial hooks use unix process conventions for hook return values
91 # mercurial hooks use unix process conventions for hook return values
89 # so a truthy return means failure
92 # so a truthy return means failure
90 return True
93 return True
91
94
92 if __name__ == '__main__':
95 if __name__ == '__main__':
93 main()
96 main()
@@ -1,1053 +1,1053
1 # dispatch.py - command dispatching for mercurial
1 # dispatch.py - command dispatching for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import difflib
10 import difflib
11 import errno
11 import errno
12 import getopt
12 import getopt
13 import os
13 import os
14 import pdb
14 import pdb
15 import re
15 import re
16 import signal
16 import signal
17 import sys
17 import sys
18 import time
18 import time
19 import traceback
19 import traceback
20
20
21
21
22 from .i18n import _
22 from .i18n import _
23
23
24 from . import (
24 from . import (
25 cmdutil,
25 cmdutil,
26 color,
26 color,
27 commands,
27 commands,
28 demandimport,
28 demandimport,
29 encoding,
29 encoding,
30 error,
30 error,
31 extensions,
31 extensions,
32 fancyopts,
32 fancyopts,
33 help,
33 help,
34 hg,
34 hg,
35 hook,
35 hook,
36 profiling,
36 profiling,
37 pycompat,
37 pycompat,
38 scmutil,
38 scmutil,
39 ui as uimod,
39 ui as uimod,
40 util,
40 util,
41 )
41 )
42
42
43 from .utils import (
43 from .utils import (
44 procutil,
44 procutil,
45 stringutil,
45 stringutil,
46 )
46 )
47
47
48 class request(object):
48 class request(object):
49 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
49 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
50 ferr=None, prereposetups=None):
50 ferr=None, prereposetups=None):
51 self.args = args
51 self.args = args
52 self.ui = ui
52 self.ui = ui
53 self.repo = repo
53 self.repo = repo
54
54
55 # input/output/error streams
55 # input/output/error streams
56 self.fin = fin
56 self.fin = fin
57 self.fout = fout
57 self.fout = fout
58 self.ferr = ferr
58 self.ferr = ferr
59
59
60 # remember options pre-parsed by _earlyparseopts()
60 # remember options pre-parsed by _earlyparseopts()
61 self.earlyoptions = {}
61 self.earlyoptions = {}
62
62
63 # reposetups which run before extensions, useful for chg to pre-fill
63 # reposetups which run before extensions, useful for chg to pre-fill
64 # low-level repo state (for example, changelog) before extensions.
64 # low-level repo state (for example, changelog) before extensions.
65 self.prereposetups = prereposetups or []
65 self.prereposetups = prereposetups or []
66
66
67 def _runexithandlers(self):
67 def _runexithandlers(self):
68 exc = None
68 exc = None
69 handlers = self.ui._exithandlers
69 handlers = self.ui._exithandlers
70 try:
70 try:
71 while handlers:
71 while handlers:
72 func, args, kwargs = handlers.pop()
72 func, args, kwargs = handlers.pop()
73 try:
73 try:
74 func(*args, **kwargs)
74 func(*args, **kwargs)
75 except: # re-raises below
75 except: # re-raises below
76 if exc is None:
76 if exc is None:
77 exc = sys.exc_info()[1]
77 exc = sys.exc_info()[1]
78 self.ui.warn(('error in exit handlers:\n'))
78 self.ui.warn(('error in exit handlers:\n'))
79 self.ui.traceback(force=True)
79 self.ui.traceback(force=True)
80 finally:
80 finally:
81 if exc is not None:
81 if exc is not None:
82 raise exc
82 raise exc
83
83
84 def run():
84 def run():
85 "run the command in sys.argv"
85 "run the command in sys.argv"
86 _initstdio()
86 initstdio()
87 req = request(pycompat.sysargv[1:])
87 req = request(pycompat.sysargv[1:])
88 err = None
88 err = None
89 try:
89 try:
90 status = (dispatch(req) or 0)
90 status = (dispatch(req) or 0)
91 except error.StdioError as e:
91 except error.StdioError as e:
92 err = e
92 err = e
93 status = -1
93 status = -1
94 if util.safehasattr(req.ui, 'fout'):
94 if util.safehasattr(req.ui, 'fout'):
95 try:
95 try:
96 req.ui.fout.flush()
96 req.ui.fout.flush()
97 except IOError as e:
97 except IOError as e:
98 err = e
98 err = e
99 status = -1
99 status = -1
100 if util.safehasattr(req.ui, 'ferr'):
100 if util.safehasattr(req.ui, 'ferr'):
101 try:
101 try:
102 if err is not None and err.errno != errno.EPIPE:
102 if err is not None and err.errno != errno.EPIPE:
103 req.ui.ferr.write('abort: %s\n' %
103 req.ui.ferr.write('abort: %s\n' %
104 encoding.strtolocal(err.strerror))
104 encoding.strtolocal(err.strerror))
105 req.ui.ferr.flush()
105 req.ui.ferr.flush()
106 # There's not much we can do about an I/O error here. So (possibly)
106 # There's not much we can do about an I/O error here. So (possibly)
107 # change the status code and move on.
107 # change the status code and move on.
108 except IOError:
108 except IOError:
109 status = -1
109 status = -1
110
110
111 _silencestdio()
111 _silencestdio()
112 sys.exit(status & 255)
112 sys.exit(status & 255)
113
113
114 if pycompat.ispy3:
114 if pycompat.ispy3:
115 def _initstdio():
115 def initstdio():
116 pass
116 pass
117
117
118 def _silencestdio():
118 def _silencestdio():
119 for fp in (sys.stdout, sys.stderr):
119 for fp in (sys.stdout, sys.stderr):
120 # Check if the file is okay
120 # Check if the file is okay
121 try:
121 try:
122 fp.flush()
122 fp.flush()
123 continue
123 continue
124 except IOError:
124 except IOError:
125 pass
125 pass
126 # Otherwise mark it as closed to silence "Exception ignored in"
126 # Otherwise mark it as closed to silence "Exception ignored in"
127 # message emitted by the interpreter finalizer. Be careful to
127 # message emitted by the interpreter finalizer. Be careful to
128 # not close procutil.stdout, which may be a fdopen-ed file object
128 # not close procutil.stdout, which may be a fdopen-ed file object
129 # and its close() actually closes the underlying file descriptor.
129 # and its close() actually closes the underlying file descriptor.
130 try:
130 try:
131 fp.close()
131 fp.close()
132 except IOError:
132 except IOError:
133 pass
133 pass
134 else:
134 else:
135 def _initstdio():
135 def initstdio():
136 for fp in (sys.stdin, sys.stdout, sys.stderr):
136 for fp in (sys.stdin, sys.stdout, sys.stderr):
137 procutil.setbinary(fp)
137 procutil.setbinary(fp)
138
138
139 def _silencestdio():
139 def _silencestdio():
140 pass
140 pass
141
141
142 def _getsimilar(symbols, value):
142 def _getsimilar(symbols, value):
143 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
143 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
144 # The cutoff for similarity here is pretty arbitrary. It should
144 # The cutoff for similarity here is pretty arbitrary. It should
145 # probably be investigated and tweaked.
145 # probably be investigated and tweaked.
146 return [s for s in symbols if sim(s) > 0.6]
146 return [s for s in symbols if sim(s) > 0.6]
147
147
148 def _reportsimilar(write, similar):
148 def _reportsimilar(write, similar):
149 if len(similar) == 1:
149 if len(similar) == 1:
150 write(_("(did you mean %s?)\n") % similar[0])
150 write(_("(did you mean %s?)\n") % similar[0])
151 elif similar:
151 elif similar:
152 ss = ", ".join(sorted(similar))
152 ss = ", ".join(sorted(similar))
153 write(_("(did you mean one of %s?)\n") % ss)
153 write(_("(did you mean one of %s?)\n") % ss)
154
154
155 def _formatparse(write, inst):
155 def _formatparse(write, inst):
156 similar = []
156 similar = []
157 if isinstance(inst, error.UnknownIdentifier):
157 if isinstance(inst, error.UnknownIdentifier):
158 # make sure to check fileset first, as revset can invoke fileset
158 # make sure to check fileset first, as revset can invoke fileset
159 similar = _getsimilar(inst.symbols, inst.function)
159 similar = _getsimilar(inst.symbols, inst.function)
160 if len(inst.args) > 1:
160 if len(inst.args) > 1:
161 write(_("hg: parse error at %s: %s\n") %
161 write(_("hg: parse error at %s: %s\n") %
162 (pycompat.bytestr(inst.args[1]), inst.args[0]))
162 (pycompat.bytestr(inst.args[1]), inst.args[0]))
163 if inst.args[0].startswith(' '):
163 if inst.args[0].startswith(' '):
164 write(_("unexpected leading whitespace\n"))
164 write(_("unexpected leading whitespace\n"))
165 else:
165 else:
166 write(_("hg: parse error: %s\n") % inst.args[0])
166 write(_("hg: parse error: %s\n") % inst.args[0])
167 _reportsimilar(write, similar)
167 _reportsimilar(write, similar)
168 if inst.hint:
168 if inst.hint:
169 write(_("(%s)\n") % inst.hint)
169 write(_("(%s)\n") % inst.hint)
170
170
171 def _formatargs(args):
171 def _formatargs(args):
172 return ' '.join(procutil.shellquote(a) for a in args)
172 return ' '.join(procutil.shellquote(a) for a in args)
173
173
174 def dispatch(req):
174 def dispatch(req):
175 "run the command specified in req.args"
175 "run the command specified in req.args"
176 if req.ferr:
176 if req.ferr:
177 ferr = req.ferr
177 ferr = req.ferr
178 elif req.ui:
178 elif req.ui:
179 ferr = req.ui.ferr
179 ferr = req.ui.ferr
180 else:
180 else:
181 ferr = procutil.stderr
181 ferr = procutil.stderr
182
182
183 try:
183 try:
184 if not req.ui:
184 if not req.ui:
185 req.ui = uimod.ui.load()
185 req.ui = uimod.ui.load()
186 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
186 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
187 if req.earlyoptions['traceback']:
187 if req.earlyoptions['traceback']:
188 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
188 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
189
189
190 # set ui streams from the request
190 # set ui streams from the request
191 if req.fin:
191 if req.fin:
192 req.ui.fin = req.fin
192 req.ui.fin = req.fin
193 if req.fout:
193 if req.fout:
194 req.ui.fout = req.fout
194 req.ui.fout = req.fout
195 if req.ferr:
195 if req.ferr:
196 req.ui.ferr = req.ferr
196 req.ui.ferr = req.ferr
197 except error.Abort as inst:
197 except error.Abort as inst:
198 ferr.write(_("abort: %s\n") % inst)
198 ferr.write(_("abort: %s\n") % inst)
199 if inst.hint:
199 if inst.hint:
200 ferr.write(_("(%s)\n") % inst.hint)
200 ferr.write(_("(%s)\n") % inst.hint)
201 return -1
201 return -1
202 except error.ParseError as inst:
202 except error.ParseError as inst:
203 _formatparse(ferr.write, inst)
203 _formatparse(ferr.write, inst)
204 return -1
204 return -1
205
205
206 msg = _formatargs(req.args)
206 msg = _formatargs(req.args)
207 starttime = util.timer()
207 starttime = util.timer()
208 ret = None
208 ret = None
209 try:
209 try:
210 ret = _runcatch(req)
210 ret = _runcatch(req)
211 except error.ProgrammingError as inst:
211 except error.ProgrammingError as inst:
212 req.ui.warn(_('** ProgrammingError: %s\n') % inst)
212 req.ui.warn(_('** ProgrammingError: %s\n') % inst)
213 if inst.hint:
213 if inst.hint:
214 req.ui.warn(_('** (%s)\n') % inst.hint)
214 req.ui.warn(_('** (%s)\n') % inst.hint)
215 raise
215 raise
216 except KeyboardInterrupt as inst:
216 except KeyboardInterrupt as inst:
217 try:
217 try:
218 if isinstance(inst, error.SignalInterrupt):
218 if isinstance(inst, error.SignalInterrupt):
219 msg = _("killed!\n")
219 msg = _("killed!\n")
220 else:
220 else:
221 msg = _("interrupted!\n")
221 msg = _("interrupted!\n")
222 req.ui.warn(msg)
222 req.ui.warn(msg)
223 except error.SignalInterrupt:
223 except error.SignalInterrupt:
224 # maybe pager would quit without consuming all the output, and
224 # maybe pager would quit without consuming all the output, and
225 # SIGPIPE was raised. we cannot print anything in this case.
225 # SIGPIPE was raised. we cannot print anything in this case.
226 pass
226 pass
227 except IOError as inst:
227 except IOError as inst:
228 if inst.errno != errno.EPIPE:
228 if inst.errno != errno.EPIPE:
229 raise
229 raise
230 ret = -1
230 ret = -1
231 finally:
231 finally:
232 duration = util.timer() - starttime
232 duration = util.timer() - starttime
233 req.ui.flush()
233 req.ui.flush()
234 if req.ui.logblockedtimes:
234 if req.ui.logblockedtimes:
235 req.ui._blockedtimes['command_duration'] = duration * 1000
235 req.ui._blockedtimes['command_duration'] = duration * 1000
236 req.ui.log('uiblocked', 'ui blocked ms',
236 req.ui.log('uiblocked', 'ui blocked ms',
237 **pycompat.strkwargs(req.ui._blockedtimes))
237 **pycompat.strkwargs(req.ui._blockedtimes))
238 req.ui.log("commandfinish", "%s exited %d after %0.2f seconds\n",
238 req.ui.log("commandfinish", "%s exited %d after %0.2f seconds\n",
239 msg, ret or 0, duration)
239 msg, ret or 0, duration)
240 try:
240 try:
241 req._runexithandlers()
241 req._runexithandlers()
242 except: # exiting, so no re-raises
242 except: # exiting, so no re-raises
243 ret = ret or -1
243 ret = ret or -1
244 return ret
244 return ret
245
245
246 def _runcatch(req):
246 def _runcatch(req):
247 def catchterm(*args):
247 def catchterm(*args):
248 raise error.SignalInterrupt
248 raise error.SignalInterrupt
249
249
250 ui = req.ui
250 ui = req.ui
251 try:
251 try:
252 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
252 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
253 num = getattr(signal, name, None)
253 num = getattr(signal, name, None)
254 if num:
254 if num:
255 signal.signal(num, catchterm)
255 signal.signal(num, catchterm)
256 except ValueError:
256 except ValueError:
257 pass # happens if called in a thread
257 pass # happens if called in a thread
258
258
259 def _runcatchfunc():
259 def _runcatchfunc():
260 realcmd = None
260 realcmd = None
261 try:
261 try:
262 cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {})
262 cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {})
263 cmd = cmdargs[0]
263 cmd = cmdargs[0]
264 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
264 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
265 realcmd = aliases[0]
265 realcmd = aliases[0]
266 except (error.UnknownCommand, error.AmbiguousCommand,
266 except (error.UnknownCommand, error.AmbiguousCommand,
267 IndexError, getopt.GetoptError):
267 IndexError, getopt.GetoptError):
268 # Don't handle this here. We know the command is
268 # Don't handle this here. We know the command is
269 # invalid, but all we're worried about for now is that
269 # invalid, but all we're worried about for now is that
270 # it's not a command that server operators expect to
270 # it's not a command that server operators expect to
271 # be safe to offer to users in a sandbox.
271 # be safe to offer to users in a sandbox.
272 pass
272 pass
273 if realcmd == 'serve' and '--stdio' in cmdargs:
273 if realcmd == 'serve' and '--stdio' in cmdargs:
274 # We want to constrain 'hg serve --stdio' instances pretty
274 # We want to constrain 'hg serve --stdio' instances pretty
275 # closely, as many shared-ssh access tools want to grant
275 # closely, as many shared-ssh access tools want to grant
276 # access to run *only* 'hg -R $repo serve --stdio'. We
276 # access to run *only* 'hg -R $repo serve --stdio'. We
277 # restrict to exactly that set of arguments, and prohibit
277 # restrict to exactly that set of arguments, and prohibit
278 # any repo name that starts with '--' to prevent
278 # any repo name that starts with '--' to prevent
279 # shenanigans wherein a user does something like pass
279 # shenanigans wherein a user does something like pass
280 # --debugger or --config=ui.debugger=1 as a repo
280 # --debugger or --config=ui.debugger=1 as a repo
281 # name. This used to actually run the debugger.
281 # name. This used to actually run the debugger.
282 if (len(req.args) != 4 or
282 if (len(req.args) != 4 or
283 req.args[0] != '-R' or
283 req.args[0] != '-R' or
284 req.args[1].startswith('--') or
284 req.args[1].startswith('--') or
285 req.args[2] != 'serve' or
285 req.args[2] != 'serve' or
286 req.args[3] != '--stdio'):
286 req.args[3] != '--stdio'):
287 raise error.Abort(
287 raise error.Abort(
288 _('potentially unsafe serve --stdio invocation: %r') %
288 _('potentially unsafe serve --stdio invocation: %r') %
289 (req.args,))
289 (req.args,))
290
290
291 try:
291 try:
292 debugger = 'pdb'
292 debugger = 'pdb'
293 debugtrace = {
293 debugtrace = {
294 'pdb': pdb.set_trace
294 'pdb': pdb.set_trace
295 }
295 }
296 debugmortem = {
296 debugmortem = {
297 'pdb': pdb.post_mortem
297 'pdb': pdb.post_mortem
298 }
298 }
299
299
300 # read --config before doing anything else
300 # read --config before doing anything else
301 # (e.g. to change trust settings for reading .hg/hgrc)
301 # (e.g. to change trust settings for reading .hg/hgrc)
302 cfgs = _parseconfig(req.ui, req.earlyoptions['config'])
302 cfgs = _parseconfig(req.ui, req.earlyoptions['config'])
303
303
304 if req.repo:
304 if req.repo:
305 # copy configs that were passed on the cmdline (--config) to
305 # copy configs that were passed on the cmdline (--config) to
306 # the repo ui
306 # the repo ui
307 for sec, name, val in cfgs:
307 for sec, name, val in cfgs:
308 req.repo.ui.setconfig(sec, name, val, source='--config')
308 req.repo.ui.setconfig(sec, name, val, source='--config')
309
309
310 # developer config: ui.debugger
310 # developer config: ui.debugger
311 debugger = ui.config("ui", "debugger")
311 debugger = ui.config("ui", "debugger")
312 debugmod = pdb
312 debugmod = pdb
313 if not debugger or ui.plain():
313 if not debugger or ui.plain():
314 # if we are in HGPLAIN mode, then disable custom debugging
314 # if we are in HGPLAIN mode, then disable custom debugging
315 debugger = 'pdb'
315 debugger = 'pdb'
316 elif req.earlyoptions['debugger']:
316 elif req.earlyoptions['debugger']:
317 # This import can be slow for fancy debuggers, so only
317 # This import can be slow for fancy debuggers, so only
318 # do it when absolutely necessary, i.e. when actual
318 # do it when absolutely necessary, i.e. when actual
319 # debugging has been requested
319 # debugging has been requested
320 with demandimport.deactivated():
320 with demandimport.deactivated():
321 try:
321 try:
322 debugmod = __import__(debugger)
322 debugmod = __import__(debugger)
323 except ImportError:
323 except ImportError:
324 pass # Leave debugmod = pdb
324 pass # Leave debugmod = pdb
325
325
326 debugtrace[debugger] = debugmod.set_trace
326 debugtrace[debugger] = debugmod.set_trace
327 debugmortem[debugger] = debugmod.post_mortem
327 debugmortem[debugger] = debugmod.post_mortem
328
328
329 # enter the debugger before command execution
329 # enter the debugger before command execution
330 if req.earlyoptions['debugger']:
330 if req.earlyoptions['debugger']:
331 ui.warn(_("entering debugger - "
331 ui.warn(_("entering debugger - "
332 "type c to continue starting hg or h for help\n"))
332 "type c to continue starting hg or h for help\n"))
333
333
334 if (debugger != 'pdb' and
334 if (debugger != 'pdb' and
335 debugtrace[debugger] == debugtrace['pdb']):
335 debugtrace[debugger] == debugtrace['pdb']):
336 ui.warn(_("%s debugger specified "
336 ui.warn(_("%s debugger specified "
337 "but its module was not found\n") % debugger)
337 "but its module was not found\n") % debugger)
338 with demandimport.deactivated():
338 with demandimport.deactivated():
339 debugtrace[debugger]()
339 debugtrace[debugger]()
340 try:
340 try:
341 return _dispatch(req)
341 return _dispatch(req)
342 finally:
342 finally:
343 ui.flush()
343 ui.flush()
344 except: # re-raises
344 except: # re-raises
345 # enter the debugger when we hit an exception
345 # enter the debugger when we hit an exception
346 if req.earlyoptions['debugger']:
346 if req.earlyoptions['debugger']:
347 traceback.print_exc()
347 traceback.print_exc()
348 debugmortem[debugger](sys.exc_info()[2])
348 debugmortem[debugger](sys.exc_info()[2])
349 raise
349 raise
350
350
351 return _callcatch(ui, _runcatchfunc)
351 return _callcatch(ui, _runcatchfunc)
352
352
353 def _callcatch(ui, func):
353 def _callcatch(ui, func):
354 """like scmutil.callcatch but handles more high-level exceptions about
354 """like scmutil.callcatch but handles more high-level exceptions about
355 config parsing and commands. besides, use handlecommandexception to handle
355 config parsing and commands. besides, use handlecommandexception to handle
356 uncaught exceptions.
356 uncaught exceptions.
357 """
357 """
358 try:
358 try:
359 return scmutil.callcatch(ui, func)
359 return scmutil.callcatch(ui, func)
360 except error.AmbiguousCommand as inst:
360 except error.AmbiguousCommand as inst:
361 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
361 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
362 (inst.args[0], " ".join(inst.args[1])))
362 (inst.args[0], " ".join(inst.args[1])))
363 except error.CommandError as inst:
363 except error.CommandError as inst:
364 if inst.args[0]:
364 if inst.args[0]:
365 ui.pager('help')
365 ui.pager('help')
366 msgbytes = pycompat.bytestr(inst.args[1])
366 msgbytes = pycompat.bytestr(inst.args[1])
367 ui.warn(_("hg %s: %s\n") % (inst.args[0], msgbytes))
367 ui.warn(_("hg %s: %s\n") % (inst.args[0], msgbytes))
368 commands.help_(ui, inst.args[0], full=False, command=True)
368 commands.help_(ui, inst.args[0], full=False, command=True)
369 else:
369 else:
370 ui.pager('help')
370 ui.pager('help')
371 ui.warn(_("hg: %s\n") % inst.args[1])
371 ui.warn(_("hg: %s\n") % inst.args[1])
372 commands.help_(ui, 'shortlist')
372 commands.help_(ui, 'shortlist')
373 except error.ParseError as inst:
373 except error.ParseError as inst:
374 _formatparse(ui.warn, inst)
374 _formatparse(ui.warn, inst)
375 return -1
375 return -1
376 except error.UnknownCommand as inst:
376 except error.UnknownCommand as inst:
377 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
377 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
378 try:
378 try:
379 # check if the command is in a disabled extension
379 # check if the command is in a disabled extension
380 # (but don't check for extensions themselves)
380 # (but don't check for extensions themselves)
381 formatted = help.formattedhelp(ui, commands, inst.args[0],
381 formatted = help.formattedhelp(ui, commands, inst.args[0],
382 unknowncmd=True)
382 unknowncmd=True)
383 ui.warn(nocmdmsg)
383 ui.warn(nocmdmsg)
384 ui.write(formatted)
384 ui.write(formatted)
385 except (error.UnknownCommand, error.Abort):
385 except (error.UnknownCommand, error.Abort):
386 suggested = False
386 suggested = False
387 if len(inst.args) == 2:
387 if len(inst.args) == 2:
388 sim = _getsimilar(inst.args[1], inst.args[0])
388 sim = _getsimilar(inst.args[1], inst.args[0])
389 if sim:
389 if sim:
390 ui.warn(nocmdmsg)
390 ui.warn(nocmdmsg)
391 _reportsimilar(ui.warn, sim)
391 _reportsimilar(ui.warn, sim)
392 suggested = True
392 suggested = True
393 if not suggested:
393 if not suggested:
394 ui.pager('help')
394 ui.pager('help')
395 ui.warn(nocmdmsg)
395 ui.warn(nocmdmsg)
396 commands.help_(ui, 'shortlist')
396 commands.help_(ui, 'shortlist')
397 except IOError:
397 except IOError:
398 raise
398 raise
399 except KeyboardInterrupt:
399 except KeyboardInterrupt:
400 raise
400 raise
401 except: # probably re-raises
401 except: # probably re-raises
402 if not handlecommandexception(ui):
402 if not handlecommandexception(ui):
403 raise
403 raise
404
404
405 return -1
405 return -1
406
406
407 def aliasargs(fn, givenargs):
407 def aliasargs(fn, givenargs):
408 args = []
408 args = []
409 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
409 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
410 if not util.safehasattr(fn, '_origfunc'):
410 if not util.safehasattr(fn, '_origfunc'):
411 args = getattr(fn, 'args', args)
411 args = getattr(fn, 'args', args)
412 if args:
412 if args:
413 cmd = ' '.join(map(procutil.shellquote, args))
413 cmd = ' '.join(map(procutil.shellquote, args))
414
414
415 nums = []
415 nums = []
416 def replacer(m):
416 def replacer(m):
417 num = int(m.group(1)) - 1
417 num = int(m.group(1)) - 1
418 nums.append(num)
418 nums.append(num)
419 if num < len(givenargs):
419 if num < len(givenargs):
420 return givenargs[num]
420 return givenargs[num]
421 raise error.Abort(_('too few arguments for command alias'))
421 raise error.Abort(_('too few arguments for command alias'))
422 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
422 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
423 givenargs = [x for i, x in enumerate(givenargs)
423 givenargs = [x for i, x in enumerate(givenargs)
424 if i not in nums]
424 if i not in nums]
425 args = pycompat.shlexsplit(cmd)
425 args = pycompat.shlexsplit(cmd)
426 return args + givenargs
426 return args + givenargs
427
427
428 def aliasinterpolate(name, args, cmd):
428 def aliasinterpolate(name, args, cmd):
429 '''interpolate args into cmd for shell aliases
429 '''interpolate args into cmd for shell aliases
430
430
431 This also handles $0, $@ and "$@".
431 This also handles $0, $@ and "$@".
432 '''
432 '''
433 # util.interpolate can't deal with "$@" (with quotes) because it's only
433 # util.interpolate can't deal with "$@" (with quotes) because it's only
434 # built to match prefix + patterns.
434 # built to match prefix + patterns.
435 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
435 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
436 replacemap['$0'] = name
436 replacemap['$0'] = name
437 replacemap['$$'] = '$'
437 replacemap['$$'] = '$'
438 replacemap['$@'] = ' '.join(args)
438 replacemap['$@'] = ' '.join(args)
439 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
439 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
440 # parameters, separated out into words. Emulate the same behavior here by
440 # parameters, separated out into words. Emulate the same behavior here by
441 # quoting the arguments individually. POSIX shells will then typically
441 # quoting the arguments individually. POSIX shells will then typically
442 # tokenize each argument into exactly one word.
442 # tokenize each argument into exactly one word.
443 replacemap['"$@"'] = ' '.join(procutil.shellquote(arg) for arg in args)
443 replacemap['"$@"'] = ' '.join(procutil.shellquote(arg) for arg in args)
444 # escape '\$' for regex
444 # escape '\$' for regex
445 regex = '|'.join(replacemap.keys()).replace('$', br'\$')
445 regex = '|'.join(replacemap.keys()).replace('$', br'\$')
446 r = re.compile(regex)
446 r = re.compile(regex)
447 return r.sub(lambda x: replacemap[x.group()], cmd)
447 return r.sub(lambda x: replacemap[x.group()], cmd)
448
448
449 class cmdalias(object):
449 class cmdalias(object):
450 def __init__(self, ui, name, definition, cmdtable, source):
450 def __init__(self, ui, name, definition, cmdtable, source):
451 self.name = self.cmd = name
451 self.name = self.cmd = name
452 self.cmdname = ''
452 self.cmdname = ''
453 self.definition = definition
453 self.definition = definition
454 self.fn = None
454 self.fn = None
455 self.givenargs = []
455 self.givenargs = []
456 self.opts = []
456 self.opts = []
457 self.help = ''
457 self.help = ''
458 self.badalias = None
458 self.badalias = None
459 self.unknowncmd = False
459 self.unknowncmd = False
460 self.source = source
460 self.source = source
461
461
462 try:
462 try:
463 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
463 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
464 for alias, e in cmdtable.iteritems():
464 for alias, e in cmdtable.iteritems():
465 if e is entry:
465 if e is entry:
466 self.cmd = alias
466 self.cmd = alias
467 break
467 break
468 self.shadows = True
468 self.shadows = True
469 except error.UnknownCommand:
469 except error.UnknownCommand:
470 self.shadows = False
470 self.shadows = False
471
471
472 if not self.definition:
472 if not self.definition:
473 self.badalias = _("no definition for alias '%s'") % self.name
473 self.badalias = _("no definition for alias '%s'") % self.name
474 return
474 return
475
475
476 if self.definition.startswith('!'):
476 if self.definition.startswith('!'):
477 shdef = self.definition[1:]
477 shdef = self.definition[1:]
478 self.shell = True
478 self.shell = True
479 def fn(ui, *args):
479 def fn(ui, *args):
480 env = {'HG_ARGS': ' '.join((self.name,) + args)}
480 env = {'HG_ARGS': ' '.join((self.name,) + args)}
481 def _checkvar(m):
481 def _checkvar(m):
482 if m.groups()[0] == '$':
482 if m.groups()[0] == '$':
483 return m.group()
483 return m.group()
484 elif int(m.groups()[0]) <= len(args):
484 elif int(m.groups()[0]) <= len(args):
485 return m.group()
485 return m.group()
486 else:
486 else:
487 ui.debug("No argument found for substitution "
487 ui.debug("No argument found for substitution "
488 "of %i variable in alias '%s' definition.\n"
488 "of %i variable in alias '%s' definition.\n"
489 % (int(m.groups()[0]), self.name))
489 % (int(m.groups()[0]), self.name))
490 return ''
490 return ''
491 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
491 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
492 cmd = aliasinterpolate(self.name, args, cmd)
492 cmd = aliasinterpolate(self.name, args, cmd)
493 return ui.system(cmd, environ=env,
493 return ui.system(cmd, environ=env,
494 blockedtag='alias_%s' % self.name)
494 blockedtag='alias_%s' % self.name)
495 self.fn = fn
495 self.fn = fn
496 self._populatehelp(ui, name, shdef, self.fn)
496 self._populatehelp(ui, name, shdef, self.fn)
497 return
497 return
498
498
499 try:
499 try:
500 args = pycompat.shlexsplit(self.definition)
500 args = pycompat.shlexsplit(self.definition)
501 except ValueError as inst:
501 except ValueError as inst:
502 self.badalias = (_("error in definition for alias '%s': %s")
502 self.badalias = (_("error in definition for alias '%s': %s")
503 % (self.name, stringutil.forcebytestr(inst)))
503 % (self.name, stringutil.forcebytestr(inst)))
504 return
504 return
505 earlyopts, args = _earlysplitopts(args)
505 earlyopts, args = _earlysplitopts(args)
506 if earlyopts:
506 if earlyopts:
507 self.badalias = (_("error in definition for alias '%s': %s may "
507 self.badalias = (_("error in definition for alias '%s': %s may "
508 "only be given on the command line")
508 "only be given on the command line")
509 % (self.name, '/'.join(pycompat.ziplist(*earlyopts)
509 % (self.name, '/'.join(pycompat.ziplist(*earlyopts)
510 [0])))
510 [0])))
511 return
511 return
512 self.cmdname = cmd = args.pop(0)
512 self.cmdname = cmd = args.pop(0)
513 self.givenargs = args
513 self.givenargs = args
514
514
515 try:
515 try:
516 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
516 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
517 if len(tableentry) > 2:
517 if len(tableentry) > 2:
518 self.fn, self.opts, cmdhelp = tableentry
518 self.fn, self.opts, cmdhelp = tableentry
519 else:
519 else:
520 self.fn, self.opts = tableentry
520 self.fn, self.opts = tableentry
521 cmdhelp = None
521 cmdhelp = None
522
522
523 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
523 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
524
524
525 except error.UnknownCommand:
525 except error.UnknownCommand:
526 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
526 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
527 % (self.name, cmd))
527 % (self.name, cmd))
528 self.unknowncmd = True
528 self.unknowncmd = True
529 except error.AmbiguousCommand:
529 except error.AmbiguousCommand:
530 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
530 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
531 % (self.name, cmd))
531 % (self.name, cmd))
532
532
533 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
533 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
534 # confine strings to be passed to i18n.gettext()
534 # confine strings to be passed to i18n.gettext()
535 cfg = {}
535 cfg = {}
536 for k in ('doc', 'help'):
536 for k in ('doc', 'help'):
537 v = ui.config('alias', '%s:%s' % (name, k), None)
537 v = ui.config('alias', '%s:%s' % (name, k), None)
538 if v is None:
538 if v is None:
539 continue
539 continue
540 if not encoding.isasciistr(v):
540 if not encoding.isasciistr(v):
541 self.badalias = (_("non-ASCII character in alias definition "
541 self.badalias = (_("non-ASCII character in alias definition "
542 "'%s:%s'") % (name, k))
542 "'%s:%s'") % (name, k))
543 return
543 return
544 cfg[k] = v
544 cfg[k] = v
545
545
546 self.help = cfg.get('help', defaulthelp or '')
546 self.help = cfg.get('help', defaulthelp or '')
547 if self.help and self.help.startswith("hg " + cmd):
547 if self.help and self.help.startswith("hg " + cmd):
548 # drop prefix in old-style help lines so hg shows the alias
548 # drop prefix in old-style help lines so hg shows the alias
549 self.help = self.help[4 + len(cmd):]
549 self.help = self.help[4 + len(cmd):]
550
550
551 doc = cfg.get('doc', pycompat.getdoc(fn))
551 doc = cfg.get('doc', pycompat.getdoc(fn))
552 if doc is not None:
552 if doc is not None:
553 doc = pycompat.sysstr(doc)
553 doc = pycompat.sysstr(doc)
554 self.__doc__ = doc
554 self.__doc__ = doc
555
555
556 @property
556 @property
557 def args(self):
557 def args(self):
558 args = pycompat.maplist(util.expandpath, self.givenargs)
558 args = pycompat.maplist(util.expandpath, self.givenargs)
559 return aliasargs(self.fn, args)
559 return aliasargs(self.fn, args)
560
560
561 def __getattr__(self, name):
561 def __getattr__(self, name):
562 adefaults = {r'norepo': True, r'intents': set(),
562 adefaults = {r'norepo': True, r'intents': set(),
563 r'optionalrepo': False, r'inferrepo': False}
563 r'optionalrepo': False, r'inferrepo': False}
564 if name not in adefaults:
564 if name not in adefaults:
565 raise AttributeError(name)
565 raise AttributeError(name)
566 if self.badalias or util.safehasattr(self, 'shell'):
566 if self.badalias or util.safehasattr(self, 'shell'):
567 return adefaults[name]
567 return adefaults[name]
568 return getattr(self.fn, name)
568 return getattr(self.fn, name)
569
569
570 def __call__(self, ui, *args, **opts):
570 def __call__(self, ui, *args, **opts):
571 if self.badalias:
571 if self.badalias:
572 hint = None
572 hint = None
573 if self.unknowncmd:
573 if self.unknowncmd:
574 try:
574 try:
575 # check if the command is in a disabled extension
575 # check if the command is in a disabled extension
576 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
576 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
577 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
577 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
578 except error.UnknownCommand:
578 except error.UnknownCommand:
579 pass
579 pass
580 raise error.Abort(self.badalias, hint=hint)
580 raise error.Abort(self.badalias, hint=hint)
581 if self.shadows:
581 if self.shadows:
582 ui.debug("alias '%s' shadows command '%s'\n" %
582 ui.debug("alias '%s' shadows command '%s'\n" %
583 (self.name, self.cmdname))
583 (self.name, self.cmdname))
584
584
585 ui.log('commandalias', "alias '%s' expands to '%s'\n",
585 ui.log('commandalias', "alias '%s' expands to '%s'\n",
586 self.name, self.definition)
586 self.name, self.definition)
587 if util.safehasattr(self, 'shell'):
587 if util.safehasattr(self, 'shell'):
588 return self.fn(ui, *args, **opts)
588 return self.fn(ui, *args, **opts)
589 else:
589 else:
590 try:
590 try:
591 return util.checksignature(self.fn)(ui, *args, **opts)
591 return util.checksignature(self.fn)(ui, *args, **opts)
592 except error.SignatureError:
592 except error.SignatureError:
593 args = ' '.join([self.cmdname] + self.args)
593 args = ' '.join([self.cmdname] + self.args)
594 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
594 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
595 raise
595 raise
596
596
597 class lazyaliasentry(object):
597 class lazyaliasentry(object):
598 """like a typical command entry (func, opts, help), but is lazy"""
598 """like a typical command entry (func, opts, help), but is lazy"""
599
599
600 def __init__(self, ui, name, definition, cmdtable, source):
600 def __init__(self, ui, name, definition, cmdtable, source):
601 self.ui = ui
601 self.ui = ui
602 self.name = name
602 self.name = name
603 self.definition = definition
603 self.definition = definition
604 self.cmdtable = cmdtable.copy()
604 self.cmdtable = cmdtable.copy()
605 self.source = source
605 self.source = source
606
606
607 @util.propertycache
607 @util.propertycache
608 def _aliasdef(self):
608 def _aliasdef(self):
609 return cmdalias(self.ui, self.name, self.definition, self.cmdtable,
609 return cmdalias(self.ui, self.name, self.definition, self.cmdtable,
610 self.source)
610 self.source)
611
611
612 def __getitem__(self, n):
612 def __getitem__(self, n):
613 aliasdef = self._aliasdef
613 aliasdef = self._aliasdef
614 if n == 0:
614 if n == 0:
615 return aliasdef
615 return aliasdef
616 elif n == 1:
616 elif n == 1:
617 return aliasdef.opts
617 return aliasdef.opts
618 elif n == 2:
618 elif n == 2:
619 return aliasdef.help
619 return aliasdef.help
620 else:
620 else:
621 raise IndexError
621 raise IndexError
622
622
623 def __iter__(self):
623 def __iter__(self):
624 for i in range(3):
624 for i in range(3):
625 yield self[i]
625 yield self[i]
626
626
627 def __len__(self):
627 def __len__(self):
628 return 3
628 return 3
629
629
630 def addaliases(ui, cmdtable):
630 def addaliases(ui, cmdtable):
631 # aliases are processed after extensions have been loaded, so they
631 # aliases are processed after extensions have been loaded, so they
632 # may use extension commands. Aliases can also use other alias definitions,
632 # may use extension commands. Aliases can also use other alias definitions,
633 # but only if they have been defined prior to the current definition.
633 # but only if they have been defined prior to the current definition.
634 for alias, definition in ui.configitems('alias', ignoresub=True):
634 for alias, definition in ui.configitems('alias', ignoresub=True):
635 try:
635 try:
636 if cmdtable[alias].definition == definition:
636 if cmdtable[alias].definition == definition:
637 continue
637 continue
638 except (KeyError, AttributeError):
638 except (KeyError, AttributeError):
639 # definition might not exist or it might not be a cmdalias
639 # definition might not exist or it might not be a cmdalias
640 pass
640 pass
641
641
642 source = ui.configsource('alias', alias)
642 source = ui.configsource('alias', alias)
643 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
643 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
644 cmdtable[alias] = entry
644 cmdtable[alias] = entry
645
645
646 def _parse(ui, args):
646 def _parse(ui, args):
647 options = {}
647 options = {}
648 cmdoptions = {}
648 cmdoptions = {}
649
649
650 try:
650 try:
651 args = fancyopts.fancyopts(args, commands.globalopts, options)
651 args = fancyopts.fancyopts(args, commands.globalopts, options)
652 except getopt.GetoptError as inst:
652 except getopt.GetoptError as inst:
653 raise error.CommandError(None, stringutil.forcebytestr(inst))
653 raise error.CommandError(None, stringutil.forcebytestr(inst))
654
654
655 if args:
655 if args:
656 cmd, args = args[0], args[1:]
656 cmd, args = args[0], args[1:]
657 aliases, entry = cmdutil.findcmd(cmd, commands.table,
657 aliases, entry = cmdutil.findcmd(cmd, commands.table,
658 ui.configbool("ui", "strict"))
658 ui.configbool("ui", "strict"))
659 cmd = aliases[0]
659 cmd = aliases[0]
660 args = aliasargs(entry[0], args)
660 args = aliasargs(entry[0], args)
661 defaults = ui.config("defaults", cmd)
661 defaults = ui.config("defaults", cmd)
662 if defaults:
662 if defaults:
663 args = pycompat.maplist(
663 args = pycompat.maplist(
664 util.expandpath, pycompat.shlexsplit(defaults)) + args
664 util.expandpath, pycompat.shlexsplit(defaults)) + args
665 c = list(entry[1])
665 c = list(entry[1])
666 else:
666 else:
667 cmd = None
667 cmd = None
668 c = []
668 c = []
669
669
670 # combine global options into local
670 # combine global options into local
671 for o in commands.globalopts:
671 for o in commands.globalopts:
672 c.append((o[0], o[1], options[o[1]], o[3]))
672 c.append((o[0], o[1], options[o[1]], o[3]))
673
673
674 try:
674 try:
675 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
675 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
676 except getopt.GetoptError as inst:
676 except getopt.GetoptError as inst:
677 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
677 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
678
678
679 # separate global options back out
679 # separate global options back out
680 for o in commands.globalopts:
680 for o in commands.globalopts:
681 n = o[1]
681 n = o[1]
682 options[n] = cmdoptions[n]
682 options[n] = cmdoptions[n]
683 del cmdoptions[n]
683 del cmdoptions[n]
684
684
685 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
685 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
686
686
687 def _parseconfig(ui, config):
687 def _parseconfig(ui, config):
688 """parse the --config options from the command line"""
688 """parse the --config options from the command line"""
689 configs = []
689 configs = []
690
690
691 for cfg in config:
691 for cfg in config:
692 try:
692 try:
693 name, value = [cfgelem.strip()
693 name, value = [cfgelem.strip()
694 for cfgelem in cfg.split('=', 1)]
694 for cfgelem in cfg.split('=', 1)]
695 section, name = name.split('.', 1)
695 section, name = name.split('.', 1)
696 if not section or not name:
696 if not section or not name:
697 raise IndexError
697 raise IndexError
698 ui.setconfig(section, name, value, '--config')
698 ui.setconfig(section, name, value, '--config')
699 configs.append((section, name, value))
699 configs.append((section, name, value))
700 except (IndexError, ValueError):
700 except (IndexError, ValueError):
701 raise error.Abort(_('malformed --config option: %r '
701 raise error.Abort(_('malformed --config option: %r '
702 '(use --config section.name=value)')
702 '(use --config section.name=value)')
703 % pycompat.bytestr(cfg))
703 % pycompat.bytestr(cfg))
704
704
705 return configs
705 return configs
706
706
707 def _earlyparseopts(ui, args):
707 def _earlyparseopts(ui, args):
708 options = {}
708 options = {}
709 fancyopts.fancyopts(args, commands.globalopts, options,
709 fancyopts.fancyopts(args, commands.globalopts, options,
710 gnu=not ui.plain('strictflags'), early=True,
710 gnu=not ui.plain('strictflags'), early=True,
711 optaliases={'repository': ['repo']})
711 optaliases={'repository': ['repo']})
712 return options
712 return options
713
713
714 def _earlysplitopts(args):
714 def _earlysplitopts(args):
715 """Split args into a list of possible early options and remainder args"""
715 """Split args into a list of possible early options and remainder args"""
716 shortoptions = 'R:'
716 shortoptions = 'R:'
717 # TODO: perhaps 'debugger' should be included
717 # TODO: perhaps 'debugger' should be included
718 longoptions = ['cwd=', 'repository=', 'repo=', 'config=']
718 longoptions = ['cwd=', 'repository=', 'repo=', 'config=']
719 return fancyopts.earlygetopt(args, shortoptions, longoptions,
719 return fancyopts.earlygetopt(args, shortoptions, longoptions,
720 gnu=True, keepsep=True)
720 gnu=True, keepsep=True)
721
721
722 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
722 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
723 # run pre-hook, and abort if it fails
723 # run pre-hook, and abort if it fails
724 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
724 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
725 pats=cmdpats, opts=cmdoptions)
725 pats=cmdpats, opts=cmdoptions)
726 try:
726 try:
727 ret = _runcommand(ui, options, cmd, d)
727 ret = _runcommand(ui, options, cmd, d)
728 # run post-hook, passing command result
728 # run post-hook, passing command result
729 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
729 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
730 result=ret, pats=cmdpats, opts=cmdoptions)
730 result=ret, pats=cmdpats, opts=cmdoptions)
731 except Exception:
731 except Exception:
732 # run failure hook and re-raise
732 # run failure hook and re-raise
733 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
733 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
734 pats=cmdpats, opts=cmdoptions)
734 pats=cmdpats, opts=cmdoptions)
735 raise
735 raise
736 return ret
736 return ret
737
737
738 def _getlocal(ui, rpath, wd=None):
738 def _getlocal(ui, rpath, wd=None):
739 """Return (path, local ui object) for the given target path.
739 """Return (path, local ui object) for the given target path.
740
740
741 Takes paths in [cwd]/.hg/hgrc into account."
741 Takes paths in [cwd]/.hg/hgrc into account."
742 """
742 """
743 if wd is None:
743 if wd is None:
744 try:
744 try:
745 wd = pycompat.getcwd()
745 wd = pycompat.getcwd()
746 except OSError as e:
746 except OSError as e:
747 raise error.Abort(_("error getting current working directory: %s") %
747 raise error.Abort(_("error getting current working directory: %s") %
748 encoding.strtolocal(e.strerror))
748 encoding.strtolocal(e.strerror))
749 path = cmdutil.findrepo(wd) or ""
749 path = cmdutil.findrepo(wd) or ""
750 if not path:
750 if not path:
751 lui = ui
751 lui = ui
752 else:
752 else:
753 lui = ui.copy()
753 lui = ui.copy()
754 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
754 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
755
755
756 if rpath:
756 if rpath:
757 path = lui.expandpath(rpath)
757 path = lui.expandpath(rpath)
758 lui = ui.copy()
758 lui = ui.copy()
759 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
759 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
760
760
761 return path, lui
761 return path, lui
762
762
763 def _checkshellalias(lui, ui, args):
763 def _checkshellalias(lui, ui, args):
764 """Return the function to run the shell alias, if it is required"""
764 """Return the function to run the shell alias, if it is required"""
765 options = {}
765 options = {}
766
766
767 try:
767 try:
768 args = fancyopts.fancyopts(args, commands.globalopts, options)
768 args = fancyopts.fancyopts(args, commands.globalopts, options)
769 except getopt.GetoptError:
769 except getopt.GetoptError:
770 return
770 return
771
771
772 if not args:
772 if not args:
773 return
773 return
774
774
775 cmdtable = commands.table
775 cmdtable = commands.table
776
776
777 cmd = args[0]
777 cmd = args[0]
778 try:
778 try:
779 strict = ui.configbool("ui", "strict")
779 strict = ui.configbool("ui", "strict")
780 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
780 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
781 except (error.AmbiguousCommand, error.UnknownCommand):
781 except (error.AmbiguousCommand, error.UnknownCommand):
782 return
782 return
783
783
784 cmd = aliases[0]
784 cmd = aliases[0]
785 fn = entry[0]
785 fn = entry[0]
786
786
787 if cmd and util.safehasattr(fn, 'shell'):
787 if cmd and util.safehasattr(fn, 'shell'):
788 # shell alias shouldn't receive early options which are consumed by hg
788 # shell alias shouldn't receive early options which are consumed by hg
789 _earlyopts, args = _earlysplitopts(args)
789 _earlyopts, args = _earlysplitopts(args)
790 d = lambda: fn(ui, *args[1:])
790 d = lambda: fn(ui, *args[1:])
791 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
791 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
792 [], {})
792 [], {})
793
793
794 def _dispatch(req):
794 def _dispatch(req):
795 args = req.args
795 args = req.args
796 ui = req.ui
796 ui = req.ui
797
797
798 # check for cwd
798 # check for cwd
799 cwd = req.earlyoptions['cwd']
799 cwd = req.earlyoptions['cwd']
800 if cwd:
800 if cwd:
801 os.chdir(cwd)
801 os.chdir(cwd)
802
802
803 rpath = req.earlyoptions['repository']
803 rpath = req.earlyoptions['repository']
804 path, lui = _getlocal(ui, rpath)
804 path, lui = _getlocal(ui, rpath)
805
805
806 uis = {ui, lui}
806 uis = {ui, lui}
807
807
808 if req.repo:
808 if req.repo:
809 uis.add(req.repo.ui)
809 uis.add(req.repo.ui)
810
810
811 if req.earlyoptions['profile']:
811 if req.earlyoptions['profile']:
812 for ui_ in uis:
812 for ui_ in uis:
813 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
813 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
814
814
815 profile = lui.configbool('profiling', 'enabled')
815 profile = lui.configbool('profiling', 'enabled')
816 with profiling.profile(lui, enabled=profile) as profiler:
816 with profiling.profile(lui, enabled=profile) as profiler:
817 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
817 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
818 # reposetup
818 # reposetup
819 extensions.loadall(lui)
819 extensions.loadall(lui)
820 # Propagate any changes to lui.__class__ by extensions
820 # Propagate any changes to lui.__class__ by extensions
821 ui.__class__ = lui.__class__
821 ui.__class__ = lui.__class__
822
822
823 # (uisetup and extsetup are handled in extensions.loadall)
823 # (uisetup and extsetup are handled in extensions.loadall)
824
824
825 # (reposetup is handled in hg.repository)
825 # (reposetup is handled in hg.repository)
826
826
827 addaliases(lui, commands.table)
827 addaliases(lui, commands.table)
828
828
829 # All aliases and commands are completely defined, now.
829 # All aliases and commands are completely defined, now.
830 # Check abbreviation/ambiguity of shell alias.
830 # Check abbreviation/ambiguity of shell alias.
831 shellaliasfn = _checkshellalias(lui, ui, args)
831 shellaliasfn = _checkshellalias(lui, ui, args)
832 if shellaliasfn:
832 if shellaliasfn:
833 return shellaliasfn()
833 return shellaliasfn()
834
834
835 # check for fallback encoding
835 # check for fallback encoding
836 fallback = lui.config('ui', 'fallbackencoding')
836 fallback = lui.config('ui', 'fallbackencoding')
837 if fallback:
837 if fallback:
838 encoding.fallbackencoding = fallback
838 encoding.fallbackencoding = fallback
839
839
840 fullargs = args
840 fullargs = args
841 cmd, func, args, options, cmdoptions = _parse(lui, args)
841 cmd, func, args, options, cmdoptions = _parse(lui, args)
842
842
843 if options["config"] != req.earlyoptions["config"]:
843 if options["config"] != req.earlyoptions["config"]:
844 raise error.Abort(_("option --config may not be abbreviated!"))
844 raise error.Abort(_("option --config may not be abbreviated!"))
845 if options["cwd"] != req.earlyoptions["cwd"]:
845 if options["cwd"] != req.earlyoptions["cwd"]:
846 raise error.Abort(_("option --cwd may not be abbreviated!"))
846 raise error.Abort(_("option --cwd may not be abbreviated!"))
847 if options["repository"] != req.earlyoptions["repository"]:
847 if options["repository"] != req.earlyoptions["repository"]:
848 raise error.Abort(_(
848 raise error.Abort(_(
849 "option -R has to be separated from other options (e.g. not "
849 "option -R has to be separated from other options (e.g. not "
850 "-qR) and --repository may only be abbreviated as --repo!"))
850 "-qR) and --repository may only be abbreviated as --repo!"))
851 if options["debugger"] != req.earlyoptions["debugger"]:
851 if options["debugger"] != req.earlyoptions["debugger"]:
852 raise error.Abort(_("option --debugger may not be abbreviated!"))
852 raise error.Abort(_("option --debugger may not be abbreviated!"))
853 # don't validate --profile/--traceback, which can be enabled from now
853 # don't validate --profile/--traceback, which can be enabled from now
854
854
855 if options["encoding"]:
855 if options["encoding"]:
856 encoding.encoding = options["encoding"]
856 encoding.encoding = options["encoding"]
857 if options["encodingmode"]:
857 if options["encodingmode"]:
858 encoding.encodingmode = options["encodingmode"]
858 encoding.encodingmode = options["encodingmode"]
859 if options["time"]:
859 if options["time"]:
860 def get_times():
860 def get_times():
861 t = os.times()
861 t = os.times()
862 if t[4] == 0.0:
862 if t[4] == 0.0:
863 # Windows leaves this as zero, so use time.clock()
863 # Windows leaves this as zero, so use time.clock()
864 t = (t[0], t[1], t[2], t[3], time.clock())
864 t = (t[0], t[1], t[2], t[3], time.clock())
865 return t
865 return t
866 s = get_times()
866 s = get_times()
867 def print_time():
867 def print_time():
868 t = get_times()
868 t = get_times()
869 ui.warn(
869 ui.warn(
870 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
870 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
871 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
871 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
872 ui.atexit(print_time)
872 ui.atexit(print_time)
873 if options["profile"]:
873 if options["profile"]:
874 profiler.start()
874 profiler.start()
875
875
876 if options['verbose'] or options['debug'] or options['quiet']:
876 if options['verbose'] or options['debug'] or options['quiet']:
877 for opt in ('verbose', 'debug', 'quiet'):
877 for opt in ('verbose', 'debug', 'quiet'):
878 val = pycompat.bytestr(bool(options[opt]))
878 val = pycompat.bytestr(bool(options[opt]))
879 for ui_ in uis:
879 for ui_ in uis:
880 ui_.setconfig('ui', opt, val, '--' + opt)
880 ui_.setconfig('ui', opt, val, '--' + opt)
881
881
882 if options['traceback']:
882 if options['traceback']:
883 for ui_ in uis:
883 for ui_ in uis:
884 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
884 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
885
885
886 if options['noninteractive']:
886 if options['noninteractive']:
887 for ui_ in uis:
887 for ui_ in uis:
888 ui_.setconfig('ui', 'interactive', 'off', '-y')
888 ui_.setconfig('ui', 'interactive', 'off', '-y')
889
889
890 if cmdoptions.get('insecure', False):
890 if cmdoptions.get('insecure', False):
891 for ui_ in uis:
891 for ui_ in uis:
892 ui_.insecureconnections = True
892 ui_.insecureconnections = True
893
893
894 # setup color handling before pager, because setting up pager
894 # setup color handling before pager, because setting up pager
895 # might cause incorrect console information
895 # might cause incorrect console information
896 coloropt = options['color']
896 coloropt = options['color']
897 for ui_ in uis:
897 for ui_ in uis:
898 if coloropt:
898 if coloropt:
899 ui_.setconfig('ui', 'color', coloropt, '--color')
899 ui_.setconfig('ui', 'color', coloropt, '--color')
900 color.setup(ui_)
900 color.setup(ui_)
901
901
902 if stringutil.parsebool(options['pager']):
902 if stringutil.parsebool(options['pager']):
903 # ui.pager() expects 'internal-always-' prefix in this case
903 # ui.pager() expects 'internal-always-' prefix in this case
904 ui.pager('internal-always-' + cmd)
904 ui.pager('internal-always-' + cmd)
905 elif options['pager'] != 'auto':
905 elif options['pager'] != 'auto':
906 for ui_ in uis:
906 for ui_ in uis:
907 ui_.disablepager()
907 ui_.disablepager()
908
908
909 if options['version']:
909 if options['version']:
910 return commands.version_(ui)
910 return commands.version_(ui)
911 if options['help']:
911 if options['help']:
912 return commands.help_(ui, cmd, command=cmd is not None)
912 return commands.help_(ui, cmd, command=cmd is not None)
913 elif not cmd:
913 elif not cmd:
914 return commands.help_(ui, 'shortlist')
914 return commands.help_(ui, 'shortlist')
915
915
916 repo = None
916 repo = None
917 cmdpats = args[:]
917 cmdpats = args[:]
918 if not func.norepo:
918 if not func.norepo:
919 # use the repo from the request only if we don't have -R
919 # use the repo from the request only if we don't have -R
920 if not rpath and not cwd:
920 if not rpath and not cwd:
921 repo = req.repo
921 repo = req.repo
922
922
923 if repo:
923 if repo:
924 # set the descriptors of the repo ui to those of ui
924 # set the descriptors of the repo ui to those of ui
925 repo.ui.fin = ui.fin
925 repo.ui.fin = ui.fin
926 repo.ui.fout = ui.fout
926 repo.ui.fout = ui.fout
927 repo.ui.ferr = ui.ferr
927 repo.ui.ferr = ui.ferr
928 else:
928 else:
929 try:
929 try:
930 repo = hg.repository(ui, path=path,
930 repo = hg.repository(ui, path=path,
931 presetupfuncs=req.prereposetups,
931 presetupfuncs=req.prereposetups,
932 intents=func.intents)
932 intents=func.intents)
933 if not repo.local():
933 if not repo.local():
934 raise error.Abort(_("repository '%s' is not local")
934 raise error.Abort(_("repository '%s' is not local")
935 % path)
935 % path)
936 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
936 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
937 'repo')
937 'repo')
938 except error.RequirementError:
938 except error.RequirementError:
939 raise
939 raise
940 except error.RepoError:
940 except error.RepoError:
941 if rpath: # invalid -R path
941 if rpath: # invalid -R path
942 raise
942 raise
943 if not func.optionalrepo:
943 if not func.optionalrepo:
944 if func.inferrepo and args and not path:
944 if func.inferrepo and args and not path:
945 # try to infer -R from command args
945 # try to infer -R from command args
946 repos = pycompat.maplist(cmdutil.findrepo, args)
946 repos = pycompat.maplist(cmdutil.findrepo, args)
947 guess = repos[0]
947 guess = repos[0]
948 if guess and repos.count(guess) == len(repos):
948 if guess and repos.count(guess) == len(repos):
949 req.args = ['--repository', guess] + fullargs
949 req.args = ['--repository', guess] + fullargs
950 req.earlyoptions['repository'] = guess
950 req.earlyoptions['repository'] = guess
951 return _dispatch(req)
951 return _dispatch(req)
952 if not path:
952 if not path:
953 raise error.RepoError(_("no repository found in"
953 raise error.RepoError(_("no repository found in"
954 " '%s' (.hg not found)")
954 " '%s' (.hg not found)")
955 % pycompat.getcwd())
955 % pycompat.getcwd())
956 raise
956 raise
957 if repo:
957 if repo:
958 ui = repo.ui
958 ui = repo.ui
959 if options['hidden']:
959 if options['hidden']:
960 repo = repo.unfiltered()
960 repo = repo.unfiltered()
961 args.insert(0, repo)
961 args.insert(0, repo)
962 elif rpath:
962 elif rpath:
963 ui.warn(_("warning: --repository ignored\n"))
963 ui.warn(_("warning: --repository ignored\n"))
964
964
965 msg = _formatargs(fullargs)
965 msg = _formatargs(fullargs)
966 ui.log("command", '%s\n', msg)
966 ui.log("command", '%s\n', msg)
967 strcmdopt = pycompat.strkwargs(cmdoptions)
967 strcmdopt = pycompat.strkwargs(cmdoptions)
968 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
968 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
969 try:
969 try:
970 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
970 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
971 cmdpats, cmdoptions)
971 cmdpats, cmdoptions)
972 finally:
972 finally:
973 if repo and repo != req.repo:
973 if repo and repo != req.repo:
974 repo.close()
974 repo.close()
975
975
976 def _runcommand(ui, options, cmd, cmdfunc):
976 def _runcommand(ui, options, cmd, cmdfunc):
977 """Run a command function, possibly with profiling enabled."""
977 """Run a command function, possibly with profiling enabled."""
978 try:
978 try:
979 return cmdfunc()
979 return cmdfunc()
980 except error.SignatureError:
980 except error.SignatureError:
981 raise error.CommandError(cmd, _('invalid arguments'))
981 raise error.CommandError(cmd, _('invalid arguments'))
982
982
983 def _exceptionwarning(ui):
983 def _exceptionwarning(ui):
984 """Produce a warning message for the current active exception"""
984 """Produce a warning message for the current active exception"""
985
985
986 # For compatibility checking, we discard the portion of the hg
986 # For compatibility checking, we discard the portion of the hg
987 # version after the + on the assumption that if a "normal
987 # version after the + on the assumption that if a "normal
988 # user" is running a build with a + in it the packager
988 # user" is running a build with a + in it the packager
989 # probably built from fairly close to a tag and anyone with a
989 # probably built from fairly close to a tag and anyone with a
990 # 'make local' copy of hg (where the version number can be out
990 # 'make local' copy of hg (where the version number can be out
991 # of date) will be clueful enough to notice the implausible
991 # of date) will be clueful enough to notice the implausible
992 # version number and try updating.
992 # version number and try updating.
993 ct = util.versiontuple(n=2)
993 ct = util.versiontuple(n=2)
994 worst = None, ct, ''
994 worst = None, ct, ''
995 if ui.config('ui', 'supportcontact') is None:
995 if ui.config('ui', 'supportcontact') is None:
996 for name, mod in extensions.extensions():
996 for name, mod in extensions.extensions():
997 # 'testedwith' should be bytes, but not all extensions are ported
997 # 'testedwith' should be bytes, but not all extensions are ported
998 # to py3 and we don't want UnicodeException because of that.
998 # to py3 and we don't want UnicodeException because of that.
999 testedwith = stringutil.forcebytestr(getattr(mod, 'testedwith', ''))
999 testedwith = stringutil.forcebytestr(getattr(mod, 'testedwith', ''))
1000 report = getattr(mod, 'buglink', _('the extension author.'))
1000 report = getattr(mod, 'buglink', _('the extension author.'))
1001 if not testedwith.strip():
1001 if not testedwith.strip():
1002 # We found an untested extension. It's likely the culprit.
1002 # We found an untested extension. It's likely the culprit.
1003 worst = name, 'unknown', report
1003 worst = name, 'unknown', report
1004 break
1004 break
1005
1005
1006 # Never blame on extensions bundled with Mercurial.
1006 # Never blame on extensions bundled with Mercurial.
1007 if extensions.ismoduleinternal(mod):
1007 if extensions.ismoduleinternal(mod):
1008 continue
1008 continue
1009
1009
1010 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1010 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1011 if ct in tested:
1011 if ct in tested:
1012 continue
1012 continue
1013
1013
1014 lower = [t for t in tested if t < ct]
1014 lower = [t for t in tested if t < ct]
1015 nearest = max(lower or tested)
1015 nearest = max(lower or tested)
1016 if worst[0] is None or nearest < worst[1]:
1016 if worst[0] is None or nearest < worst[1]:
1017 worst = name, nearest, report
1017 worst = name, nearest, report
1018 if worst[0] is not None:
1018 if worst[0] is not None:
1019 name, testedwith, report = worst
1019 name, testedwith, report = worst
1020 if not isinstance(testedwith, (bytes, str)):
1020 if not isinstance(testedwith, (bytes, str)):
1021 testedwith = '.'.join([stringutil.forcebytestr(c)
1021 testedwith = '.'.join([stringutil.forcebytestr(c)
1022 for c in testedwith])
1022 for c in testedwith])
1023 warning = (_('** Unknown exception encountered with '
1023 warning = (_('** Unknown exception encountered with '
1024 'possibly-broken third-party extension %s\n'
1024 'possibly-broken third-party extension %s\n'
1025 '** which supports versions %s of Mercurial.\n'
1025 '** which supports versions %s of Mercurial.\n'
1026 '** Please disable %s and try your action again.\n'
1026 '** Please disable %s and try your action again.\n'
1027 '** If that fixes the bug please report it to %s\n')
1027 '** If that fixes the bug please report it to %s\n')
1028 % (name, testedwith, name, report))
1028 % (name, testedwith, name, report))
1029 else:
1029 else:
1030 bugtracker = ui.config('ui', 'supportcontact')
1030 bugtracker = ui.config('ui', 'supportcontact')
1031 if bugtracker is None:
1031 if bugtracker is None:
1032 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
1032 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
1033 warning = (_("** unknown exception encountered, "
1033 warning = (_("** unknown exception encountered, "
1034 "please report by visiting\n** ") + bugtracker + '\n')
1034 "please report by visiting\n** ") + bugtracker + '\n')
1035 sysversion = pycompat.sysbytes(sys.version).replace('\n', '')
1035 sysversion = pycompat.sysbytes(sys.version).replace('\n', '')
1036 warning += ((_("** Python %s\n") % sysversion) +
1036 warning += ((_("** Python %s\n") % sysversion) +
1037 (_("** Mercurial Distributed SCM (version %s)\n") %
1037 (_("** Mercurial Distributed SCM (version %s)\n") %
1038 util.version()) +
1038 util.version()) +
1039 (_("** Extensions loaded: %s\n") %
1039 (_("** Extensions loaded: %s\n") %
1040 ", ".join([x[0] for x in extensions.extensions()])))
1040 ", ".join([x[0] for x in extensions.extensions()])))
1041 return warning
1041 return warning
1042
1042
1043 def handlecommandexception(ui):
1043 def handlecommandexception(ui):
1044 """Produce a warning message for broken commands
1044 """Produce a warning message for broken commands
1045
1045
1046 Called when handling an exception; the exception is reraised if
1046 Called when handling an exception; the exception is reraised if
1047 this function returns False, ignored otherwise.
1047 this function returns False, ignored otherwise.
1048 """
1048 """
1049 warning = _exceptionwarning(ui)
1049 warning = _exceptionwarning(ui)
1050 ui.log("commandexception", "%s\n%s\n", warning,
1050 ui.log("commandexception", "%s\n%s\n", warning,
1051 pycompat.sysbytes(traceback.format_exc()))
1051 pycompat.sysbytes(traceback.format_exc()))
1052 ui.warn(warning)
1052 ui.warn(warning)
1053 return False # re-raise the exception
1053 return False # re-raise the exception
@@ -1,811 +1,807
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import contextlib
9 import contextlib
10 import struct
10 import struct
11 import sys
11 import sys
12 import threading
12 import threading
13
13
14 from .i18n import _
14 from .i18n import _
15 from .thirdparty import (
15 from .thirdparty import (
16 cbor,
16 cbor,
17 )
17 )
18 from . import (
18 from . import (
19 encoding,
19 encoding,
20 error,
20 error,
21 hook,
21 hook,
22 pycompat,
22 pycompat,
23 util,
23 util,
24 wireprototypes,
24 wireprototypes,
25 wireprotov1server,
25 wireprotov1server,
26 wireprotov2server,
26 wireprotov2server,
27 )
27 )
28 from .utils import (
28 from .utils import (
29 interfaceutil,
29 interfaceutil,
30 procutil,
30 procutil,
31 )
31 )
32
32
33 stringio = util.stringio
33 stringio = util.stringio
34
34
35 urlerr = util.urlerr
35 urlerr = util.urlerr
36 urlreq = util.urlreq
36 urlreq = util.urlreq
37
37
38 HTTP_OK = 200
38 HTTP_OK = 200
39
39
40 HGTYPE = 'application/mercurial-0.1'
40 HGTYPE = 'application/mercurial-0.1'
41 HGTYPE2 = 'application/mercurial-0.2'
41 HGTYPE2 = 'application/mercurial-0.2'
42 HGERRTYPE = 'application/hg-error'
42 HGERRTYPE = 'application/hg-error'
43
43
44 SSHV1 = wireprototypes.SSHV1
44 SSHV1 = wireprototypes.SSHV1
45 SSHV2 = wireprototypes.SSHV2
45 SSHV2 = wireprototypes.SSHV2
46
46
47 def decodevaluefromheaders(req, headerprefix):
47 def decodevaluefromheaders(req, headerprefix):
48 """Decode a long value from multiple HTTP request headers.
48 """Decode a long value from multiple HTTP request headers.
49
49
50 Returns the value as a bytes, not a str.
50 Returns the value as a bytes, not a str.
51 """
51 """
52 chunks = []
52 chunks = []
53 i = 1
53 i = 1
54 while True:
54 while True:
55 v = req.headers.get(b'%s-%d' % (headerprefix, i))
55 v = req.headers.get(b'%s-%d' % (headerprefix, i))
56 if v is None:
56 if v is None:
57 break
57 break
58 chunks.append(pycompat.bytesurl(v))
58 chunks.append(pycompat.bytesurl(v))
59 i += 1
59 i += 1
60
60
61 return ''.join(chunks)
61 return ''.join(chunks)
62
62
63 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
63 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
64 class httpv1protocolhandler(object):
64 class httpv1protocolhandler(object):
65 def __init__(self, req, ui, checkperm):
65 def __init__(self, req, ui, checkperm):
66 self._req = req
66 self._req = req
67 self._ui = ui
67 self._ui = ui
68 self._checkperm = checkperm
68 self._checkperm = checkperm
69 self._protocaps = None
69 self._protocaps = None
70
70
71 @property
71 @property
72 def name(self):
72 def name(self):
73 return 'http-v1'
73 return 'http-v1'
74
74
75 def getargs(self, args):
75 def getargs(self, args):
76 knownargs = self._args()
76 knownargs = self._args()
77 data = {}
77 data = {}
78 keys = args.split()
78 keys = args.split()
79 for k in keys:
79 for k in keys:
80 if k == '*':
80 if k == '*':
81 star = {}
81 star = {}
82 for key in knownargs.keys():
82 for key in knownargs.keys():
83 if key != 'cmd' and key not in keys:
83 if key != 'cmd' and key not in keys:
84 star[key] = knownargs[key][0]
84 star[key] = knownargs[key][0]
85 data['*'] = star
85 data['*'] = star
86 else:
86 else:
87 data[k] = knownargs[k][0]
87 data[k] = knownargs[k][0]
88 return [data[k] for k in keys]
88 return [data[k] for k in keys]
89
89
90 def _args(self):
90 def _args(self):
91 args = self._req.qsparams.asdictoflists()
91 args = self._req.qsparams.asdictoflists()
92 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
92 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
93 if postlen:
93 if postlen:
94 args.update(urlreq.parseqs(
94 args.update(urlreq.parseqs(
95 self._req.bodyfh.read(postlen), keep_blank_values=True))
95 self._req.bodyfh.read(postlen), keep_blank_values=True))
96 return args
96 return args
97
97
98 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
98 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
99 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
99 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
100 return args
100 return args
101
101
102 def getprotocaps(self):
102 def getprotocaps(self):
103 if self._protocaps is None:
103 if self._protocaps is None:
104 value = decodevaluefromheaders(self._req, b'X-HgProto')
104 value = decodevaluefromheaders(self._req, b'X-HgProto')
105 self._protocaps = set(value.split(' '))
105 self._protocaps = set(value.split(' '))
106 return self._protocaps
106 return self._protocaps
107
107
108 def getpayload(self):
108 def getpayload(self):
109 # Existing clients *always* send Content-Length.
109 # Existing clients *always* send Content-Length.
110 length = int(self._req.headers[b'Content-Length'])
110 length = int(self._req.headers[b'Content-Length'])
111
111
112 # If httppostargs is used, we need to read Content-Length
112 # If httppostargs is used, we need to read Content-Length
113 # minus the amount that was consumed by args.
113 # minus the amount that was consumed by args.
114 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
114 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
115 return util.filechunkiter(self._req.bodyfh, limit=length)
115 return util.filechunkiter(self._req.bodyfh, limit=length)
116
116
117 @contextlib.contextmanager
117 @contextlib.contextmanager
118 def mayberedirectstdio(self):
118 def mayberedirectstdio(self):
119 oldout = self._ui.fout
119 oldout = self._ui.fout
120 olderr = self._ui.ferr
120 olderr = self._ui.ferr
121
121
122 out = util.stringio()
122 out = util.stringio()
123
123
124 try:
124 try:
125 self._ui.fout = out
125 self._ui.fout = out
126 self._ui.ferr = out
126 self._ui.ferr = out
127 yield out
127 yield out
128 finally:
128 finally:
129 self._ui.fout = oldout
129 self._ui.fout = oldout
130 self._ui.ferr = olderr
130 self._ui.ferr = olderr
131
131
132 def client(self):
132 def client(self):
133 return 'remote:%s:%s:%s' % (
133 return 'remote:%s:%s:%s' % (
134 self._req.urlscheme,
134 self._req.urlscheme,
135 urlreq.quote(self._req.remotehost or ''),
135 urlreq.quote(self._req.remotehost or ''),
136 urlreq.quote(self._req.remoteuser or ''))
136 urlreq.quote(self._req.remoteuser or ''))
137
137
138 def addcapabilities(self, repo, caps):
138 def addcapabilities(self, repo, caps):
139 caps.append(b'batch')
139 caps.append(b'batch')
140
140
141 caps.append('httpheader=%d' %
141 caps.append('httpheader=%d' %
142 repo.ui.configint('server', 'maxhttpheaderlen'))
142 repo.ui.configint('server', 'maxhttpheaderlen'))
143 if repo.ui.configbool('experimental', 'httppostargs'):
143 if repo.ui.configbool('experimental', 'httppostargs'):
144 caps.append('httppostargs')
144 caps.append('httppostargs')
145
145
146 # FUTURE advertise 0.2rx once support is implemented
146 # FUTURE advertise 0.2rx once support is implemented
147 # FUTURE advertise minrx and mintx after consulting config option
147 # FUTURE advertise minrx and mintx after consulting config option
148 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
148 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
149
149
150 compengines = wireprototypes.supportedcompengines(repo.ui,
150 compengines = wireprototypes.supportedcompengines(repo.ui,
151 util.SERVERROLE)
151 util.SERVERROLE)
152 if compengines:
152 if compengines:
153 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
153 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
154 for e in compengines)
154 for e in compengines)
155 caps.append('compression=%s' % comptypes)
155 caps.append('compression=%s' % comptypes)
156
156
157 return caps
157 return caps
158
158
159 def checkperm(self, perm):
159 def checkperm(self, perm):
160 return self._checkperm(perm)
160 return self._checkperm(perm)
161
161
162 # This method exists mostly so that extensions like remotefilelog can
162 # This method exists mostly so that extensions like remotefilelog can
163 # disable a kludgey legacy method only over http. As of early 2018,
163 # disable a kludgey legacy method only over http. As of early 2018,
164 # there are no other known users, so with any luck we can discard this
164 # there are no other known users, so with any luck we can discard this
165 # hook if remotefilelog becomes a first-party extension.
165 # hook if remotefilelog becomes a first-party extension.
166 def iscmd(cmd):
166 def iscmd(cmd):
167 return cmd in wireprotov1server.commands
167 return cmd in wireprotov1server.commands
168
168
169 def handlewsgirequest(rctx, req, res, checkperm):
169 def handlewsgirequest(rctx, req, res, checkperm):
170 """Possibly process a wire protocol request.
170 """Possibly process a wire protocol request.
171
171
172 If the current request is a wire protocol request, the request is
172 If the current request is a wire protocol request, the request is
173 processed by this function.
173 processed by this function.
174
174
175 ``req`` is a ``parsedrequest`` instance.
175 ``req`` is a ``parsedrequest`` instance.
176 ``res`` is a ``wsgiresponse`` instance.
176 ``res`` is a ``wsgiresponse`` instance.
177
177
178 Returns a bool indicating if the request was serviced. If set, the caller
178 Returns a bool indicating if the request was serviced. If set, the caller
179 should stop processing the request, as a response has already been issued.
179 should stop processing the request, as a response has already been issued.
180 """
180 """
181 # Avoid cycle involving hg module.
181 # Avoid cycle involving hg module.
182 from .hgweb import common as hgwebcommon
182 from .hgweb import common as hgwebcommon
183
183
184 repo = rctx.repo
184 repo = rctx.repo
185
185
186 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
186 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
187 # string parameter. If it isn't present, this isn't a wire protocol
187 # string parameter. If it isn't present, this isn't a wire protocol
188 # request.
188 # request.
189 if 'cmd' not in req.qsparams:
189 if 'cmd' not in req.qsparams:
190 return False
190 return False
191
191
192 cmd = req.qsparams['cmd']
192 cmd = req.qsparams['cmd']
193
193
194 # The "cmd" request parameter is used by both the wire protocol and hgweb.
194 # The "cmd" request parameter is used by both the wire protocol and hgweb.
195 # While not all wire protocol commands are available for all transports,
195 # While not all wire protocol commands are available for all transports,
196 # if we see a "cmd" value that resembles a known wire protocol command, we
196 # if we see a "cmd" value that resembles a known wire protocol command, we
197 # route it to a protocol handler. This is better than routing possible
197 # route it to a protocol handler. This is better than routing possible
198 # wire protocol requests to hgweb because it prevents hgweb from using
198 # wire protocol requests to hgweb because it prevents hgweb from using
199 # known wire protocol commands and it is less confusing for machine
199 # known wire protocol commands and it is less confusing for machine
200 # clients.
200 # clients.
201 if not iscmd(cmd):
201 if not iscmd(cmd):
202 return False
202 return False
203
203
204 # The "cmd" query string argument is only valid on the root path of the
204 # The "cmd" query string argument is only valid on the root path of the
205 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
205 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
206 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
206 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
207 # in this case. We send an HTTP 404 for backwards compatibility reasons.
207 # in this case. We send an HTTP 404 for backwards compatibility reasons.
208 if req.dispatchpath:
208 if req.dispatchpath:
209 res.status = hgwebcommon.statusmessage(404)
209 res.status = hgwebcommon.statusmessage(404)
210 res.headers['Content-Type'] = HGTYPE
210 res.headers['Content-Type'] = HGTYPE
211 # TODO This is not a good response to issue for this request. This
211 # TODO This is not a good response to issue for this request. This
212 # is mostly for BC for now.
212 # is mostly for BC for now.
213 res.setbodybytes('0\n%s\n' % b'Not Found')
213 res.setbodybytes('0\n%s\n' % b'Not Found')
214 return True
214 return True
215
215
216 proto = httpv1protocolhandler(req, repo.ui,
216 proto = httpv1protocolhandler(req, repo.ui,
217 lambda perm: checkperm(rctx, req, perm))
217 lambda perm: checkperm(rctx, req, perm))
218
218
219 # The permissions checker should be the only thing that can raise an
219 # The permissions checker should be the only thing that can raise an
220 # ErrorResponse. It is kind of a layer violation to catch an hgweb
220 # ErrorResponse. It is kind of a layer violation to catch an hgweb
221 # exception here. So consider refactoring into a exception type that
221 # exception here. So consider refactoring into a exception type that
222 # is associated with the wire protocol.
222 # is associated with the wire protocol.
223 try:
223 try:
224 _callhttp(repo, req, res, proto, cmd)
224 _callhttp(repo, req, res, proto, cmd)
225 except hgwebcommon.ErrorResponse as e:
225 except hgwebcommon.ErrorResponse as e:
226 for k, v in e.headers:
226 for k, v in e.headers:
227 res.headers[k] = v
227 res.headers[k] = v
228 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
228 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
229 # TODO This response body assumes the failed command was
229 # TODO This response body assumes the failed command was
230 # "unbundle." That assumption is not always valid.
230 # "unbundle." That assumption is not always valid.
231 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
231 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
232
232
233 return True
233 return True
234
234
235 def _availableapis(repo):
235 def _availableapis(repo):
236 apis = set()
236 apis = set()
237
237
238 # Registered APIs are made available via config options of the name of
238 # Registered APIs are made available via config options of the name of
239 # the protocol.
239 # the protocol.
240 for k, v in API_HANDLERS.items():
240 for k, v in API_HANDLERS.items():
241 section, option = v['config']
241 section, option = v['config']
242 if repo.ui.configbool(section, option):
242 if repo.ui.configbool(section, option):
243 apis.add(k)
243 apis.add(k)
244
244
245 return apis
245 return apis
246
246
247 def handlewsgiapirequest(rctx, req, res, checkperm):
247 def handlewsgiapirequest(rctx, req, res, checkperm):
248 """Handle requests to /api/*."""
248 """Handle requests to /api/*."""
249 assert req.dispatchparts[0] == b'api'
249 assert req.dispatchparts[0] == b'api'
250
250
251 repo = rctx.repo
251 repo = rctx.repo
252
252
253 # This whole URL space is experimental for now. But we want to
253 # This whole URL space is experimental for now. But we want to
254 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
254 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
255 if not repo.ui.configbool('experimental', 'web.apiserver'):
255 if not repo.ui.configbool('experimental', 'web.apiserver'):
256 res.status = b'404 Not Found'
256 res.status = b'404 Not Found'
257 res.headers[b'Content-Type'] = b'text/plain'
257 res.headers[b'Content-Type'] = b'text/plain'
258 res.setbodybytes(_('Experimental API server endpoint not enabled'))
258 res.setbodybytes(_('Experimental API server endpoint not enabled'))
259 return
259 return
260
260
261 # The URL space is /api/<protocol>/*. The structure of URLs under varies
261 # The URL space is /api/<protocol>/*. The structure of URLs under varies
262 # by <protocol>.
262 # by <protocol>.
263
263
264 availableapis = _availableapis(repo)
264 availableapis = _availableapis(repo)
265
265
266 # Requests to /api/ list available APIs.
266 # Requests to /api/ list available APIs.
267 if req.dispatchparts == [b'api']:
267 if req.dispatchparts == [b'api']:
268 res.status = b'200 OK'
268 res.status = b'200 OK'
269 res.headers[b'Content-Type'] = b'text/plain'
269 res.headers[b'Content-Type'] = b'text/plain'
270 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
270 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
271 'one of the following:\n')]
271 'one of the following:\n')]
272 if availableapis:
272 if availableapis:
273 lines.extend(sorted(availableapis))
273 lines.extend(sorted(availableapis))
274 else:
274 else:
275 lines.append(_('(no available APIs)\n'))
275 lines.append(_('(no available APIs)\n'))
276 res.setbodybytes(b'\n'.join(lines))
276 res.setbodybytes(b'\n'.join(lines))
277 return
277 return
278
278
279 proto = req.dispatchparts[1]
279 proto = req.dispatchparts[1]
280
280
281 if proto not in API_HANDLERS:
281 if proto not in API_HANDLERS:
282 res.status = b'404 Not Found'
282 res.status = b'404 Not Found'
283 res.headers[b'Content-Type'] = b'text/plain'
283 res.headers[b'Content-Type'] = b'text/plain'
284 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
284 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
285 proto, b', '.join(sorted(availableapis))))
285 proto, b', '.join(sorted(availableapis))))
286 return
286 return
287
287
288 if proto not in availableapis:
288 if proto not in availableapis:
289 res.status = b'404 Not Found'
289 res.status = b'404 Not Found'
290 res.headers[b'Content-Type'] = b'text/plain'
290 res.headers[b'Content-Type'] = b'text/plain'
291 res.setbodybytes(_('API %s not enabled\n') % proto)
291 res.setbodybytes(_('API %s not enabled\n') % proto)
292 return
292 return
293
293
294 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
294 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
295 req.dispatchparts[2:])
295 req.dispatchparts[2:])
296
296
297 # Maps API name to metadata so custom API can be registered.
297 # Maps API name to metadata so custom API can be registered.
298 # Keys are:
298 # Keys are:
299 #
299 #
300 # config
300 # config
301 # Config option that controls whether service is enabled.
301 # Config option that controls whether service is enabled.
302 # handler
302 # handler
303 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
303 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
304 # when a request to this API is received.
304 # when a request to this API is received.
305 # apidescriptor
305 # apidescriptor
306 # Callable receiving (req, repo) that is called to obtain an API
306 # Callable receiving (req, repo) that is called to obtain an API
307 # descriptor for this service. The response must be serializable to CBOR.
307 # descriptor for this service. The response must be serializable to CBOR.
308 API_HANDLERS = {
308 API_HANDLERS = {
309 wireprotov2server.HTTP_WIREPROTO_V2: {
309 wireprotov2server.HTTP_WIREPROTO_V2: {
310 'config': ('experimental', 'web.api.http-v2'),
310 'config': ('experimental', 'web.api.http-v2'),
311 'handler': wireprotov2server.handlehttpv2request,
311 'handler': wireprotov2server.handlehttpv2request,
312 'apidescriptor': wireprotov2server.httpv2apidescriptor,
312 'apidescriptor': wireprotov2server.httpv2apidescriptor,
313 },
313 },
314 }
314 }
315
315
316 def _httpresponsetype(ui, proto, prefer_uncompressed):
316 def _httpresponsetype(ui, proto, prefer_uncompressed):
317 """Determine the appropriate response type and compression settings.
317 """Determine the appropriate response type and compression settings.
318
318
319 Returns a tuple of (mediatype, compengine, engineopts).
319 Returns a tuple of (mediatype, compengine, engineopts).
320 """
320 """
321 # Determine the response media type and compression engine based
321 # Determine the response media type and compression engine based
322 # on the request parameters.
322 # on the request parameters.
323
323
324 if '0.2' in proto.getprotocaps():
324 if '0.2' in proto.getprotocaps():
325 # All clients are expected to support uncompressed data.
325 # All clients are expected to support uncompressed data.
326 if prefer_uncompressed:
326 if prefer_uncompressed:
327 return HGTYPE2, util._noopengine(), {}
327 return HGTYPE2, util._noopengine(), {}
328
328
329 # Now find an agreed upon compression format.
329 # Now find an agreed upon compression format.
330 compformats = wireprotov1server.clientcompressionsupport(proto)
330 compformats = wireprotov1server.clientcompressionsupport(proto)
331 for engine in wireprototypes.supportedcompengines(ui, util.SERVERROLE):
331 for engine in wireprototypes.supportedcompengines(ui, util.SERVERROLE):
332 if engine.wireprotosupport().name in compformats:
332 if engine.wireprotosupport().name in compformats:
333 opts = {}
333 opts = {}
334 level = ui.configint('server', '%slevel' % engine.name())
334 level = ui.configint('server', '%slevel' % engine.name())
335 if level is not None:
335 if level is not None:
336 opts['level'] = level
336 opts['level'] = level
337
337
338 return HGTYPE2, engine, opts
338 return HGTYPE2, engine, opts
339
339
340 # No mutually supported compression format. Fall back to the
340 # No mutually supported compression format. Fall back to the
341 # legacy protocol.
341 # legacy protocol.
342
342
343 # Don't allow untrusted settings because disabling compression or
343 # Don't allow untrusted settings because disabling compression or
344 # setting a very high compression level could lead to flooding
344 # setting a very high compression level could lead to flooding
345 # the server's network or CPU.
345 # the server's network or CPU.
346 opts = {'level': ui.configint('server', 'zliblevel')}
346 opts = {'level': ui.configint('server', 'zliblevel')}
347 return HGTYPE, util.compengines['zlib'], opts
347 return HGTYPE, util.compengines['zlib'], opts
348
348
349 def processcapabilitieshandshake(repo, req, res, proto):
349 def processcapabilitieshandshake(repo, req, res, proto):
350 """Called during a ?cmd=capabilities request.
350 """Called during a ?cmd=capabilities request.
351
351
352 If the client is advertising support for a newer protocol, we send
352 If the client is advertising support for a newer protocol, we send
353 a CBOR response with information about available services. If no
353 a CBOR response with information about available services. If no
354 advertised services are available, we don't handle the request.
354 advertised services are available, we don't handle the request.
355 """
355 """
356 # Fall back to old behavior unless the API server is enabled.
356 # Fall back to old behavior unless the API server is enabled.
357 if not repo.ui.configbool('experimental', 'web.apiserver'):
357 if not repo.ui.configbool('experimental', 'web.apiserver'):
358 return False
358 return False
359
359
360 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
360 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
361 protocaps = decodevaluefromheaders(req, b'X-HgProto')
361 protocaps = decodevaluefromheaders(req, b'X-HgProto')
362 if not clientapis or not protocaps:
362 if not clientapis or not protocaps:
363 return False
363 return False
364
364
365 # We currently only support CBOR responses.
365 # We currently only support CBOR responses.
366 protocaps = set(protocaps.split(' '))
366 protocaps = set(protocaps.split(' '))
367 if b'cbor' not in protocaps:
367 if b'cbor' not in protocaps:
368 return False
368 return False
369
369
370 descriptors = {}
370 descriptors = {}
371
371
372 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
372 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
373 handler = API_HANDLERS[api]
373 handler = API_HANDLERS[api]
374
374
375 descriptorfn = handler.get('apidescriptor')
375 descriptorfn = handler.get('apidescriptor')
376 if not descriptorfn:
376 if not descriptorfn:
377 continue
377 continue
378
378
379 descriptors[api] = descriptorfn(req, repo)
379 descriptors[api] = descriptorfn(req, repo)
380
380
381 v1caps = wireprotov1server.dispatch(repo, proto, 'capabilities')
381 v1caps = wireprotov1server.dispatch(repo, proto, 'capabilities')
382 assert isinstance(v1caps, wireprototypes.bytesresponse)
382 assert isinstance(v1caps, wireprototypes.bytesresponse)
383
383
384 m = {
384 m = {
385 # TODO allow this to be configurable.
385 # TODO allow this to be configurable.
386 'apibase': 'api/',
386 'apibase': 'api/',
387 'apis': descriptors,
387 'apis': descriptors,
388 'v1capabilities': v1caps.data,
388 'v1capabilities': v1caps.data,
389 }
389 }
390
390
391 res.status = b'200 OK'
391 res.status = b'200 OK'
392 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
392 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
393 res.setbodybytes(cbor.dumps(m, canonical=True))
393 res.setbodybytes(cbor.dumps(m, canonical=True))
394
394
395 return True
395 return True
396
396
397 def _callhttp(repo, req, res, proto, cmd):
397 def _callhttp(repo, req, res, proto, cmd):
398 # Avoid cycle involving hg module.
398 # Avoid cycle involving hg module.
399 from .hgweb import common as hgwebcommon
399 from .hgweb import common as hgwebcommon
400
400
401 def genversion2(gen, engine, engineopts):
401 def genversion2(gen, engine, engineopts):
402 # application/mercurial-0.2 always sends a payload header
402 # application/mercurial-0.2 always sends a payload header
403 # identifying the compression engine.
403 # identifying the compression engine.
404 name = engine.wireprotosupport().name
404 name = engine.wireprotosupport().name
405 assert 0 < len(name) < 256
405 assert 0 < len(name) < 256
406 yield struct.pack('B', len(name))
406 yield struct.pack('B', len(name))
407 yield name
407 yield name
408
408
409 for chunk in gen:
409 for chunk in gen:
410 yield chunk
410 yield chunk
411
411
412 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
412 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
413 if code == HTTP_OK:
413 if code == HTTP_OK:
414 res.status = '200 Script output follows'
414 res.status = '200 Script output follows'
415 else:
415 else:
416 res.status = hgwebcommon.statusmessage(code)
416 res.status = hgwebcommon.statusmessage(code)
417
417
418 res.headers['Content-Type'] = contenttype
418 res.headers['Content-Type'] = contenttype
419
419
420 if bodybytes is not None:
420 if bodybytes is not None:
421 res.setbodybytes(bodybytes)
421 res.setbodybytes(bodybytes)
422 if bodygen is not None:
422 if bodygen is not None:
423 res.setbodygen(bodygen)
423 res.setbodygen(bodygen)
424
424
425 if not wireprotov1server.commands.commandavailable(cmd, proto):
425 if not wireprotov1server.commands.commandavailable(cmd, proto):
426 setresponse(HTTP_OK, HGERRTYPE,
426 setresponse(HTTP_OK, HGERRTYPE,
427 _('requested wire protocol command is not available over '
427 _('requested wire protocol command is not available over '
428 'HTTP'))
428 'HTTP'))
429 return
429 return
430
430
431 proto.checkperm(wireprotov1server.commands[cmd].permission)
431 proto.checkperm(wireprotov1server.commands[cmd].permission)
432
432
433 # Possibly handle a modern client wanting to switch protocols.
433 # Possibly handle a modern client wanting to switch protocols.
434 if (cmd == 'capabilities' and
434 if (cmd == 'capabilities' and
435 processcapabilitieshandshake(repo, req, res, proto)):
435 processcapabilitieshandshake(repo, req, res, proto)):
436
436
437 return
437 return
438
438
439 rsp = wireprotov1server.dispatch(repo, proto, cmd)
439 rsp = wireprotov1server.dispatch(repo, proto, cmd)
440
440
441 if isinstance(rsp, bytes):
441 if isinstance(rsp, bytes):
442 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
442 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
443 elif isinstance(rsp, wireprototypes.bytesresponse):
443 elif isinstance(rsp, wireprototypes.bytesresponse):
444 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
444 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
445 elif isinstance(rsp, wireprototypes.streamreslegacy):
445 elif isinstance(rsp, wireprototypes.streamreslegacy):
446 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
446 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
447 elif isinstance(rsp, wireprototypes.streamres):
447 elif isinstance(rsp, wireprototypes.streamres):
448 gen = rsp.gen
448 gen = rsp.gen
449
449
450 # This code for compression should not be streamres specific. It
450 # This code for compression should not be streamres specific. It
451 # is here because we only compress streamres at the moment.
451 # is here because we only compress streamres at the moment.
452 mediatype, engine, engineopts = _httpresponsetype(
452 mediatype, engine, engineopts = _httpresponsetype(
453 repo.ui, proto, rsp.prefer_uncompressed)
453 repo.ui, proto, rsp.prefer_uncompressed)
454 gen = engine.compressstream(gen, engineopts)
454 gen = engine.compressstream(gen, engineopts)
455
455
456 if mediatype == HGTYPE2:
456 if mediatype == HGTYPE2:
457 gen = genversion2(gen, engine, engineopts)
457 gen = genversion2(gen, engine, engineopts)
458
458
459 setresponse(HTTP_OK, mediatype, bodygen=gen)
459 setresponse(HTTP_OK, mediatype, bodygen=gen)
460 elif isinstance(rsp, wireprototypes.pushres):
460 elif isinstance(rsp, wireprototypes.pushres):
461 rsp = '%d\n%s' % (rsp.res, rsp.output)
461 rsp = '%d\n%s' % (rsp.res, rsp.output)
462 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
462 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
463 elif isinstance(rsp, wireprototypes.pusherr):
463 elif isinstance(rsp, wireprototypes.pusherr):
464 rsp = '0\n%s\n' % rsp.res
464 rsp = '0\n%s\n' % rsp.res
465 res.drain = True
465 res.drain = True
466 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
466 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
467 elif isinstance(rsp, wireprototypes.ooberror):
467 elif isinstance(rsp, wireprototypes.ooberror):
468 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
468 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
469 else:
469 else:
470 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
470 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
471
471
472 def _sshv1respondbytes(fout, value):
472 def _sshv1respondbytes(fout, value):
473 """Send a bytes response for protocol version 1."""
473 """Send a bytes response for protocol version 1."""
474 fout.write('%d\n' % len(value))
474 fout.write('%d\n' % len(value))
475 fout.write(value)
475 fout.write(value)
476 fout.flush()
476 fout.flush()
477
477
478 def _sshv1respondstream(fout, source):
478 def _sshv1respondstream(fout, source):
479 write = fout.write
479 write = fout.write
480 for chunk in source.gen:
480 for chunk in source.gen:
481 write(chunk)
481 write(chunk)
482 fout.flush()
482 fout.flush()
483
483
484 def _sshv1respondooberror(fout, ferr, rsp):
484 def _sshv1respondooberror(fout, ferr, rsp):
485 ferr.write(b'%s\n-\n' % rsp)
485 ferr.write(b'%s\n-\n' % rsp)
486 ferr.flush()
486 ferr.flush()
487 fout.write(b'\n')
487 fout.write(b'\n')
488 fout.flush()
488 fout.flush()
489
489
490 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
490 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
491 class sshv1protocolhandler(object):
491 class sshv1protocolhandler(object):
492 """Handler for requests services via version 1 of SSH protocol."""
492 """Handler for requests services via version 1 of SSH protocol."""
493 def __init__(self, ui, fin, fout):
493 def __init__(self, ui, fin, fout):
494 self._ui = ui
494 self._ui = ui
495 self._fin = fin
495 self._fin = fin
496 self._fout = fout
496 self._fout = fout
497 self._protocaps = set()
497 self._protocaps = set()
498
498
499 @property
499 @property
500 def name(self):
500 def name(self):
501 return wireprototypes.SSHV1
501 return wireprototypes.SSHV1
502
502
503 def getargs(self, args):
503 def getargs(self, args):
504 data = {}
504 data = {}
505 keys = args.split()
505 keys = args.split()
506 for n in xrange(len(keys)):
506 for n in xrange(len(keys)):
507 argline = self._fin.readline()[:-1]
507 argline = self._fin.readline()[:-1]
508 arg, l = argline.split()
508 arg, l = argline.split()
509 if arg not in keys:
509 if arg not in keys:
510 raise error.Abort(_("unexpected parameter %r") % arg)
510 raise error.Abort(_("unexpected parameter %r") % arg)
511 if arg == '*':
511 if arg == '*':
512 star = {}
512 star = {}
513 for k in xrange(int(l)):
513 for k in xrange(int(l)):
514 argline = self._fin.readline()[:-1]
514 argline = self._fin.readline()[:-1]
515 arg, l = argline.split()
515 arg, l = argline.split()
516 val = self._fin.read(int(l))
516 val = self._fin.read(int(l))
517 star[arg] = val
517 star[arg] = val
518 data['*'] = star
518 data['*'] = star
519 else:
519 else:
520 val = self._fin.read(int(l))
520 val = self._fin.read(int(l))
521 data[arg] = val
521 data[arg] = val
522 return [data[k] for k in keys]
522 return [data[k] for k in keys]
523
523
524 def getprotocaps(self):
524 def getprotocaps(self):
525 return self._protocaps
525 return self._protocaps
526
526
527 def getpayload(self):
527 def getpayload(self):
528 # We initially send an empty response. This tells the client it is
528 # We initially send an empty response. This tells the client it is
529 # OK to start sending data. If a client sees any other response, it
529 # OK to start sending data. If a client sees any other response, it
530 # interprets it as an error.
530 # interprets it as an error.
531 _sshv1respondbytes(self._fout, b'')
531 _sshv1respondbytes(self._fout, b'')
532
532
533 # The file is in the form:
533 # The file is in the form:
534 #
534 #
535 # <chunk size>\n<chunk>
535 # <chunk size>\n<chunk>
536 # ...
536 # ...
537 # 0\n
537 # 0\n
538 count = int(self._fin.readline())
538 count = int(self._fin.readline())
539 while count:
539 while count:
540 yield self._fin.read(count)
540 yield self._fin.read(count)
541 count = int(self._fin.readline())
541 count = int(self._fin.readline())
542
542
543 @contextlib.contextmanager
543 @contextlib.contextmanager
544 def mayberedirectstdio(self):
544 def mayberedirectstdio(self):
545 yield None
545 yield None
546
546
547 def client(self):
547 def client(self):
548 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
548 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
549 return 'remote:ssh:' + client
549 return 'remote:ssh:' + client
550
550
551 def addcapabilities(self, repo, caps):
551 def addcapabilities(self, repo, caps):
552 if self.name == wireprototypes.SSHV1:
552 if self.name == wireprototypes.SSHV1:
553 caps.append(b'protocaps')
553 caps.append(b'protocaps')
554 caps.append(b'batch')
554 caps.append(b'batch')
555 return caps
555 return caps
556
556
557 def checkperm(self, perm):
557 def checkperm(self, perm):
558 pass
558 pass
559
559
560 class sshv2protocolhandler(sshv1protocolhandler):
560 class sshv2protocolhandler(sshv1protocolhandler):
561 """Protocol handler for version 2 of the SSH protocol."""
561 """Protocol handler for version 2 of the SSH protocol."""
562
562
563 @property
563 @property
564 def name(self):
564 def name(self):
565 return wireprototypes.SSHV2
565 return wireprototypes.SSHV2
566
566
567 def addcapabilities(self, repo, caps):
567 def addcapabilities(self, repo, caps):
568 return caps
568 return caps
569
569
570 def _runsshserver(ui, repo, fin, fout, ev):
570 def _runsshserver(ui, repo, fin, fout, ev):
571 # This function operates like a state machine of sorts. The following
571 # This function operates like a state machine of sorts. The following
572 # states are defined:
572 # states are defined:
573 #
573 #
574 # protov1-serving
574 # protov1-serving
575 # Server is in protocol version 1 serving mode. Commands arrive on
575 # Server is in protocol version 1 serving mode. Commands arrive on
576 # new lines. These commands are processed in this state, one command
576 # new lines. These commands are processed in this state, one command
577 # after the other.
577 # after the other.
578 #
578 #
579 # protov2-serving
579 # protov2-serving
580 # Server is in protocol version 2 serving mode.
580 # Server is in protocol version 2 serving mode.
581 #
581 #
582 # upgrade-initial
582 # upgrade-initial
583 # The server is going to process an upgrade request.
583 # The server is going to process an upgrade request.
584 #
584 #
585 # upgrade-v2-filter-legacy-handshake
585 # upgrade-v2-filter-legacy-handshake
586 # The protocol is being upgraded to version 2. The server is expecting
586 # The protocol is being upgraded to version 2. The server is expecting
587 # the legacy handshake from version 1.
587 # the legacy handshake from version 1.
588 #
588 #
589 # upgrade-v2-finish
589 # upgrade-v2-finish
590 # The upgrade to version 2 of the protocol is imminent.
590 # The upgrade to version 2 of the protocol is imminent.
591 #
591 #
592 # shutdown
592 # shutdown
593 # The server is shutting down, possibly in reaction to a client event.
593 # The server is shutting down, possibly in reaction to a client event.
594 #
594 #
595 # And here are their transitions:
595 # And here are their transitions:
596 #
596 #
597 # protov1-serving -> shutdown
597 # protov1-serving -> shutdown
598 # When server receives an empty request or encounters another
598 # When server receives an empty request or encounters another
599 # error.
599 # error.
600 #
600 #
601 # protov1-serving -> upgrade-initial
601 # protov1-serving -> upgrade-initial
602 # An upgrade request line was seen.
602 # An upgrade request line was seen.
603 #
603 #
604 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
604 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
605 # Upgrade to version 2 in progress. Server is expecting to
605 # Upgrade to version 2 in progress. Server is expecting to
606 # process a legacy handshake.
606 # process a legacy handshake.
607 #
607 #
608 # upgrade-v2-filter-legacy-handshake -> shutdown
608 # upgrade-v2-filter-legacy-handshake -> shutdown
609 # Client did not fulfill upgrade handshake requirements.
609 # Client did not fulfill upgrade handshake requirements.
610 #
610 #
611 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
611 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
612 # Client fulfilled version 2 upgrade requirements. Finishing that
612 # Client fulfilled version 2 upgrade requirements. Finishing that
613 # upgrade.
613 # upgrade.
614 #
614 #
615 # upgrade-v2-finish -> protov2-serving
615 # upgrade-v2-finish -> protov2-serving
616 # Protocol upgrade to version 2 complete. Server can now speak protocol
616 # Protocol upgrade to version 2 complete. Server can now speak protocol
617 # version 2.
617 # version 2.
618 #
618 #
619 # protov2-serving -> protov1-serving
619 # protov2-serving -> protov1-serving
620 # Ths happens by default since protocol version 2 is the same as
620 # Ths happens by default since protocol version 2 is the same as
621 # version 1 except for the handshake.
621 # version 1 except for the handshake.
622
622
623 state = 'protov1-serving'
623 state = 'protov1-serving'
624 proto = sshv1protocolhandler(ui, fin, fout)
624 proto = sshv1protocolhandler(ui, fin, fout)
625 protoswitched = False
625 protoswitched = False
626
626
627 while not ev.is_set():
627 while not ev.is_set():
628 if state == 'protov1-serving':
628 if state == 'protov1-serving':
629 # Commands are issued on new lines.
629 # Commands are issued on new lines.
630 request = fin.readline()[:-1]
630 request = fin.readline()[:-1]
631
631
632 # Empty lines signal to terminate the connection.
632 # Empty lines signal to terminate the connection.
633 if not request:
633 if not request:
634 state = 'shutdown'
634 state = 'shutdown'
635 continue
635 continue
636
636
637 # It looks like a protocol upgrade request. Transition state to
637 # It looks like a protocol upgrade request. Transition state to
638 # handle it.
638 # handle it.
639 if request.startswith(b'upgrade '):
639 if request.startswith(b'upgrade '):
640 if protoswitched:
640 if protoswitched:
641 _sshv1respondooberror(fout, ui.ferr,
641 _sshv1respondooberror(fout, ui.ferr,
642 b'cannot upgrade protocols multiple '
642 b'cannot upgrade protocols multiple '
643 b'times')
643 b'times')
644 state = 'shutdown'
644 state = 'shutdown'
645 continue
645 continue
646
646
647 state = 'upgrade-initial'
647 state = 'upgrade-initial'
648 continue
648 continue
649
649
650 available = wireprotov1server.commands.commandavailable(
650 available = wireprotov1server.commands.commandavailable(
651 request, proto)
651 request, proto)
652
652
653 # This command isn't available. Send an empty response and go
653 # This command isn't available. Send an empty response and go
654 # back to waiting for a new command.
654 # back to waiting for a new command.
655 if not available:
655 if not available:
656 _sshv1respondbytes(fout, b'')
656 _sshv1respondbytes(fout, b'')
657 continue
657 continue
658
658
659 rsp = wireprotov1server.dispatch(repo, proto, request)
659 rsp = wireprotov1server.dispatch(repo, proto, request)
660
660
661 if isinstance(rsp, bytes):
661 if isinstance(rsp, bytes):
662 _sshv1respondbytes(fout, rsp)
662 _sshv1respondbytes(fout, rsp)
663 elif isinstance(rsp, wireprototypes.bytesresponse):
663 elif isinstance(rsp, wireprototypes.bytesresponse):
664 _sshv1respondbytes(fout, rsp.data)
664 _sshv1respondbytes(fout, rsp.data)
665 elif isinstance(rsp, wireprototypes.streamres):
665 elif isinstance(rsp, wireprototypes.streamres):
666 _sshv1respondstream(fout, rsp)
666 _sshv1respondstream(fout, rsp)
667 elif isinstance(rsp, wireprototypes.streamreslegacy):
667 elif isinstance(rsp, wireprototypes.streamreslegacy):
668 _sshv1respondstream(fout, rsp)
668 _sshv1respondstream(fout, rsp)
669 elif isinstance(rsp, wireprototypes.pushres):
669 elif isinstance(rsp, wireprototypes.pushres):
670 _sshv1respondbytes(fout, b'')
670 _sshv1respondbytes(fout, b'')
671 _sshv1respondbytes(fout, b'%d' % rsp.res)
671 _sshv1respondbytes(fout, b'%d' % rsp.res)
672 elif isinstance(rsp, wireprototypes.pusherr):
672 elif isinstance(rsp, wireprototypes.pusherr):
673 _sshv1respondbytes(fout, rsp.res)
673 _sshv1respondbytes(fout, rsp.res)
674 elif isinstance(rsp, wireprototypes.ooberror):
674 elif isinstance(rsp, wireprototypes.ooberror):
675 _sshv1respondooberror(fout, ui.ferr, rsp.message)
675 _sshv1respondooberror(fout, ui.ferr, rsp.message)
676 else:
676 else:
677 raise error.ProgrammingError('unhandled response type from '
677 raise error.ProgrammingError('unhandled response type from '
678 'wire protocol command: %s' % rsp)
678 'wire protocol command: %s' % rsp)
679
679
680 # For now, protocol version 2 serving just goes back to version 1.
680 # For now, protocol version 2 serving just goes back to version 1.
681 elif state == 'protov2-serving':
681 elif state == 'protov2-serving':
682 state = 'protov1-serving'
682 state = 'protov1-serving'
683 continue
683 continue
684
684
685 elif state == 'upgrade-initial':
685 elif state == 'upgrade-initial':
686 # We should never transition into this state if we've switched
686 # We should never transition into this state if we've switched
687 # protocols.
687 # protocols.
688 assert not protoswitched
688 assert not protoswitched
689 assert proto.name == wireprototypes.SSHV1
689 assert proto.name == wireprototypes.SSHV1
690
690
691 # Expected: upgrade <token> <capabilities>
691 # Expected: upgrade <token> <capabilities>
692 # If we get something else, the request is malformed. It could be
692 # If we get something else, the request is malformed. It could be
693 # from a future client that has altered the upgrade line content.
693 # from a future client that has altered the upgrade line content.
694 # We treat this as an unknown command.
694 # We treat this as an unknown command.
695 try:
695 try:
696 token, caps = request.split(b' ')[1:]
696 token, caps = request.split(b' ')[1:]
697 except ValueError:
697 except ValueError:
698 _sshv1respondbytes(fout, b'')
698 _sshv1respondbytes(fout, b'')
699 state = 'protov1-serving'
699 state = 'protov1-serving'
700 continue
700 continue
701
701
702 # Send empty response if we don't support upgrading protocols.
702 # Send empty response if we don't support upgrading protocols.
703 if not ui.configbool('experimental', 'sshserver.support-v2'):
703 if not ui.configbool('experimental', 'sshserver.support-v2'):
704 _sshv1respondbytes(fout, b'')
704 _sshv1respondbytes(fout, b'')
705 state = 'protov1-serving'
705 state = 'protov1-serving'
706 continue
706 continue
707
707
708 try:
708 try:
709 caps = urlreq.parseqs(caps)
709 caps = urlreq.parseqs(caps)
710 except ValueError:
710 except ValueError:
711 _sshv1respondbytes(fout, b'')
711 _sshv1respondbytes(fout, b'')
712 state = 'protov1-serving'
712 state = 'protov1-serving'
713 continue
713 continue
714
714
715 # We don't see an upgrade request to protocol version 2. Ignore
715 # We don't see an upgrade request to protocol version 2. Ignore
716 # the upgrade request.
716 # the upgrade request.
717 wantedprotos = caps.get(b'proto', [b''])[0]
717 wantedprotos = caps.get(b'proto', [b''])[0]
718 if SSHV2 not in wantedprotos:
718 if SSHV2 not in wantedprotos:
719 _sshv1respondbytes(fout, b'')
719 _sshv1respondbytes(fout, b'')
720 state = 'protov1-serving'
720 state = 'protov1-serving'
721 continue
721 continue
722
722
723 # It looks like we can honor this upgrade request to protocol 2.
723 # It looks like we can honor this upgrade request to protocol 2.
724 # Filter the rest of the handshake protocol request lines.
724 # Filter the rest of the handshake protocol request lines.
725 state = 'upgrade-v2-filter-legacy-handshake'
725 state = 'upgrade-v2-filter-legacy-handshake'
726 continue
726 continue
727
727
728 elif state == 'upgrade-v2-filter-legacy-handshake':
728 elif state == 'upgrade-v2-filter-legacy-handshake':
729 # Client should have sent legacy handshake after an ``upgrade``
729 # Client should have sent legacy handshake after an ``upgrade``
730 # request. Expected lines:
730 # request. Expected lines:
731 #
731 #
732 # hello
732 # hello
733 # between
733 # between
734 # pairs 81
734 # pairs 81
735 # 0000...-0000...
735 # 0000...-0000...
736
736
737 ok = True
737 ok = True
738 for line in (b'hello', b'between', b'pairs 81'):
738 for line in (b'hello', b'between', b'pairs 81'):
739 request = fin.readline()[:-1]
739 request = fin.readline()[:-1]
740
740
741 if request != line:
741 if request != line:
742 _sshv1respondooberror(fout, ui.ferr,
742 _sshv1respondooberror(fout, ui.ferr,
743 b'malformed handshake protocol: '
743 b'malformed handshake protocol: '
744 b'missing %s' % line)
744 b'missing %s' % line)
745 ok = False
745 ok = False
746 state = 'shutdown'
746 state = 'shutdown'
747 break
747 break
748
748
749 if not ok:
749 if not ok:
750 continue
750 continue
751
751
752 request = fin.read(81)
752 request = fin.read(81)
753 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
753 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
754 _sshv1respondooberror(fout, ui.ferr,
754 _sshv1respondooberror(fout, ui.ferr,
755 b'malformed handshake protocol: '
755 b'malformed handshake protocol: '
756 b'missing between argument value')
756 b'missing between argument value')
757 state = 'shutdown'
757 state = 'shutdown'
758 continue
758 continue
759
759
760 state = 'upgrade-v2-finish'
760 state = 'upgrade-v2-finish'
761 continue
761 continue
762
762
763 elif state == 'upgrade-v2-finish':
763 elif state == 'upgrade-v2-finish':
764 # Send the upgrade response.
764 # Send the upgrade response.
765 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
765 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
766 servercaps = wireprotov1server.capabilities(repo, proto)
766 servercaps = wireprotov1server.capabilities(repo, proto)
767 rsp = b'capabilities: %s' % servercaps.data
767 rsp = b'capabilities: %s' % servercaps.data
768 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
768 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
769 fout.flush()
769 fout.flush()
770
770
771 proto = sshv2protocolhandler(ui, fin, fout)
771 proto = sshv2protocolhandler(ui, fin, fout)
772 protoswitched = True
772 protoswitched = True
773
773
774 state = 'protov2-serving'
774 state = 'protov2-serving'
775 continue
775 continue
776
776
777 elif state == 'shutdown':
777 elif state == 'shutdown':
778 break
778 break
779
779
780 else:
780 else:
781 raise error.ProgrammingError('unhandled ssh server state: %s' %
781 raise error.ProgrammingError('unhandled ssh server state: %s' %
782 state)
782 state)
783
783
784 class sshserver(object):
784 class sshserver(object):
785 def __init__(self, ui, repo, logfh=None):
785 def __init__(self, ui, repo, logfh=None):
786 self._ui = ui
786 self._ui = ui
787 self._repo = repo
787 self._repo = repo
788 self._fin = ui.fin
788 self._fin = ui.fin
789 self._fout = ui.fout
789 self._fout = ui.fout
790
790
791 # Log write I/O to stdout and stderr if configured.
791 # Log write I/O to stdout and stderr if configured.
792 if logfh:
792 if logfh:
793 self._fout = util.makeloggingfileobject(
793 self._fout = util.makeloggingfileobject(
794 logfh, self._fout, 'o', logdata=True)
794 logfh, self._fout, 'o', logdata=True)
795 ui.ferr = util.makeloggingfileobject(
795 ui.ferr = util.makeloggingfileobject(
796 logfh, ui.ferr, 'e', logdata=True)
796 logfh, ui.ferr, 'e', logdata=True)
797
797
798 hook.redirect(True)
798 hook.redirect(True)
799 ui.fout = repo.ui.fout = ui.ferr
799 ui.fout = repo.ui.fout = ui.ferr
800
800
801 # Prevent insertion/deletion of CRs
802 procutil.setbinary(self._fin)
803 procutil.setbinary(self._fout)
804
805 def serve_forever(self):
801 def serve_forever(self):
806 self.serveuntil(threading.Event())
802 self.serveuntil(threading.Event())
807 sys.exit(0)
803 sys.exit(0)
808
804
809 def serveuntil(self, ev):
805 def serveuntil(self, ev):
810 """Serve until a threading.Event is set."""
806 """Serve until a threading.Event is set."""
811 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
807 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now