##// END OF EJS Templates
py3: use forcebytestr() to stringify hook exception...
Yuya Nishihara -
r41007:8c8fcb38 default
parent child Browse files
Show More
@@ -1,287 +1,287 b''
1 # hook.py - hook support for mercurial
1 # hook.py - hook support for mercurial
2 #
2 #
3 # Copyright 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 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
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import sys
11 import sys
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 demandimport,
15 demandimport,
16 encoding,
16 encoding,
17 error,
17 error,
18 extensions,
18 extensions,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 )
21 )
22 from .utils import (
22 from .utils import (
23 procutil,
23 procutil,
24 stringutil,
24 stringutil,
25 )
25 )
26
26
27 def pythonhook(ui, repo, htype, hname, funcname, args, throw):
27 def pythonhook(ui, repo, htype, hname, funcname, args, throw):
28 '''call python hook. hook is callable object, looked up as
28 '''call python hook. hook is callable object, looked up as
29 name in python module. if callable returns "true", hook
29 name in python module. if callable returns "true", hook
30 fails, else passes. if hook raises exception, treated as
30 fails, else passes. if hook raises exception, treated as
31 hook failure. exception propagates if throw is "true".
31 hook failure. exception propagates if throw is "true".
32
32
33 reason for "true" meaning "hook failed" is so that
33 reason for "true" meaning "hook failed" is so that
34 unmodified commands (e.g. mercurial.commands.update) can
34 unmodified commands (e.g. mercurial.commands.update) can
35 be run as hooks without wrappers to convert return values.'''
35 be run as hooks without wrappers to convert return values.'''
36
36
37 if callable(funcname):
37 if callable(funcname):
38 obj = funcname
38 obj = funcname
39 funcname = pycompat.sysbytes(obj.__module__ + r"." + obj.__name__)
39 funcname = pycompat.sysbytes(obj.__module__ + r"." + obj.__name__)
40 else:
40 else:
41 d = funcname.rfind('.')
41 d = funcname.rfind('.')
42 if d == -1:
42 if d == -1:
43 raise error.HookLoadError(
43 raise error.HookLoadError(
44 _('%s hook is invalid: "%s" not in a module')
44 _('%s hook is invalid: "%s" not in a module')
45 % (hname, funcname))
45 % (hname, funcname))
46 modname = funcname[:d]
46 modname = funcname[:d]
47 oldpaths = sys.path
47 oldpaths = sys.path
48 if procutil.mainfrozen():
48 if procutil.mainfrozen():
49 # binary installs require sys.path manipulation
49 # binary installs require sys.path manipulation
50 modpath, modfile = os.path.split(modname)
50 modpath, modfile = os.path.split(modname)
51 if modpath and modfile:
51 if modpath and modfile:
52 sys.path = sys.path[:] + [modpath]
52 sys.path = sys.path[:] + [modpath]
53 modname = modfile
53 modname = modfile
54 with demandimport.deactivated():
54 with demandimport.deactivated():
55 try:
55 try:
56 obj = __import__(pycompat.sysstr(modname))
56 obj = __import__(pycompat.sysstr(modname))
57 except (ImportError, SyntaxError):
57 except (ImportError, SyntaxError):
58 e1 = sys.exc_info()
58 e1 = sys.exc_info()
59 try:
59 try:
60 # extensions are loaded with hgext_ prefix
60 # extensions are loaded with hgext_ prefix
61 obj = __import__(r"hgext_%s" % pycompat.sysstr(modname))
61 obj = __import__(r"hgext_%s" % pycompat.sysstr(modname))
62 except (ImportError, SyntaxError):
62 except (ImportError, SyntaxError):
63 e2 = sys.exc_info()
63 e2 = sys.exc_info()
64 if ui.tracebackflag:
64 if ui.tracebackflag:
65 ui.warn(_('exception from first failed import '
65 ui.warn(_('exception from first failed import '
66 'attempt:\n'))
66 'attempt:\n'))
67 ui.traceback(e1)
67 ui.traceback(e1)
68 if ui.tracebackflag:
68 if ui.tracebackflag:
69 ui.warn(_('exception from second failed import '
69 ui.warn(_('exception from second failed import '
70 'attempt:\n'))
70 'attempt:\n'))
71 ui.traceback(e2)
71 ui.traceback(e2)
72
72
73 if not ui.tracebackflag:
73 if not ui.tracebackflag:
74 tracebackhint = _(
74 tracebackhint = _(
75 'run with --traceback for stack trace')
75 'run with --traceback for stack trace')
76 else:
76 else:
77 tracebackhint = None
77 tracebackhint = None
78 raise error.HookLoadError(
78 raise error.HookLoadError(
79 _('%s hook is invalid: import of "%s" failed') %
79 _('%s hook is invalid: import of "%s" failed') %
80 (hname, modname), hint=tracebackhint)
80 (hname, modname), hint=tracebackhint)
81 sys.path = oldpaths
81 sys.path = oldpaths
82 try:
82 try:
83 for p in funcname.split('.')[1:]:
83 for p in funcname.split('.')[1:]:
84 obj = getattr(obj, p)
84 obj = getattr(obj, p)
85 except AttributeError:
85 except AttributeError:
86 raise error.HookLoadError(
86 raise error.HookLoadError(
87 _('%s hook is invalid: "%s" is not defined')
87 _('%s hook is invalid: "%s" is not defined')
88 % (hname, funcname))
88 % (hname, funcname))
89 if not callable(obj):
89 if not callable(obj):
90 raise error.HookLoadError(
90 raise error.HookLoadError(
91 _('%s hook is invalid: "%s" is not callable')
91 _('%s hook is invalid: "%s" is not callable')
92 % (hname, funcname))
92 % (hname, funcname))
93
93
94 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
94 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
95 starttime = util.timer()
95 starttime = util.timer()
96
96
97 try:
97 try:
98 r = obj(ui=ui, repo=repo, hooktype=htype, **pycompat.strkwargs(args))
98 r = obj(ui=ui, repo=repo, hooktype=htype, **pycompat.strkwargs(args))
99 except Exception as exc:
99 except Exception as exc:
100 if isinstance(exc, error.Abort):
100 if isinstance(exc, error.Abort):
101 ui.warn(_('error: %s hook failed: %s\n') %
101 ui.warn(_('error: %s hook failed: %s\n') %
102 (hname, exc.args[0]))
102 (hname, exc.args[0]))
103 else:
103 else:
104 ui.warn(_('error: %s hook raised an exception: '
104 ui.warn(_('error: %s hook raised an exception: '
105 '%s\n') % (hname, encoding.strtolocal(str(exc))))
105 '%s\n') % (hname, stringutil.forcebytestr(exc)))
106 if throw:
106 if throw:
107 raise
107 raise
108 if not ui.tracebackflag:
108 if not ui.tracebackflag:
109 ui.warn(_('(run with --traceback for stack trace)\n'))
109 ui.warn(_('(run with --traceback for stack trace)\n'))
110 ui.traceback()
110 ui.traceback()
111 return True, True
111 return True, True
112 finally:
112 finally:
113 duration = util.timer() - starttime
113 duration = util.timer() - starttime
114 ui.log('pythonhook', 'pythonhook-%s: %s finished in %0.2f seconds\n',
114 ui.log('pythonhook', 'pythonhook-%s: %s finished in %0.2f seconds\n',
115 htype, funcname, duration)
115 htype, funcname, duration)
116 if r:
116 if r:
117 if throw:
117 if throw:
118 raise error.HookAbort(_('%s hook failed') % hname)
118 raise error.HookAbort(_('%s hook failed') % hname)
119 ui.warn(_('warning: %s hook failed\n') % hname)
119 ui.warn(_('warning: %s hook failed\n') % hname)
120 return r, False
120 return r, False
121
121
122 def _exthook(ui, repo, htype, name, cmd, args, throw):
122 def _exthook(ui, repo, htype, name, cmd, args, throw):
123 starttime = util.timer()
123 starttime = util.timer()
124 env = {}
124 env = {}
125
125
126 # make in-memory changes visible to external process
126 # make in-memory changes visible to external process
127 if repo is not None:
127 if repo is not None:
128 tr = repo.currenttransaction()
128 tr = repo.currenttransaction()
129 repo.dirstate.write(tr)
129 repo.dirstate.write(tr)
130 if tr and tr.writepending():
130 if tr and tr.writepending():
131 env['HG_PENDING'] = repo.root
131 env['HG_PENDING'] = repo.root
132 env['HG_HOOKTYPE'] = htype
132 env['HG_HOOKTYPE'] = htype
133 env['HG_HOOKNAME'] = name
133 env['HG_HOOKNAME'] = name
134
134
135 for k, v in args.iteritems():
135 for k, v in args.iteritems():
136 if callable(v):
136 if callable(v):
137 v = v()
137 v = v()
138 if isinstance(v, (dict, list)):
138 if isinstance(v, (dict, list)):
139 v = stringutil.pprint(v)
139 v = stringutil.pprint(v)
140 env['HG_' + k.upper()] = v
140 env['HG_' + k.upper()] = v
141
141
142 if ui.configbool('hooks', 'tonative.%s' % name, False):
142 if ui.configbool('hooks', 'tonative.%s' % name, False):
143 oldcmd = cmd
143 oldcmd = cmd
144 cmd = procutil.shelltonative(cmd, env)
144 cmd = procutil.shelltonative(cmd, env)
145 if cmd != oldcmd:
145 if cmd != oldcmd:
146 ui.note(_('converting hook "%s" to native\n') % name)
146 ui.note(_('converting hook "%s" to native\n') % name)
147
147
148 ui.note(_("running hook %s: %s\n") % (name, cmd))
148 ui.note(_("running hook %s: %s\n") % (name, cmd))
149
149
150 if repo:
150 if repo:
151 cwd = repo.root
151 cwd = repo.root
152 else:
152 else:
153 cwd = encoding.getcwd()
153 cwd = encoding.getcwd()
154 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag='exthook-%s' % (name,))
154 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag='exthook-%s' % (name,))
155
155
156 duration = util.timer() - starttime
156 duration = util.timer() - starttime
157 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
157 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
158 name, cmd, duration)
158 name, cmd, duration)
159 if r:
159 if r:
160 desc = procutil.explainexit(r)
160 desc = procutil.explainexit(r)
161 if throw:
161 if throw:
162 raise error.HookAbort(_('%s hook %s') % (name, desc))
162 raise error.HookAbort(_('%s hook %s') % (name, desc))
163 ui.warn(_('warning: %s hook %s\n') % (name, desc))
163 ui.warn(_('warning: %s hook %s\n') % (name, desc))
164 return r
164 return r
165
165
166 # represent an untrusted hook command
166 # represent an untrusted hook command
167 _fromuntrusted = object()
167 _fromuntrusted = object()
168
168
169 def _allhooks(ui):
169 def _allhooks(ui):
170 """return a list of (hook-id, cmd) pairs sorted by priority"""
170 """return a list of (hook-id, cmd) pairs sorted by priority"""
171 hooks = _hookitems(ui)
171 hooks = _hookitems(ui)
172 # Be careful in this section, propagating the real commands from untrusted
172 # Be careful in this section, propagating the real commands from untrusted
173 # sources would create a security vulnerability, make sure anything altered
173 # sources would create a security vulnerability, make sure anything altered
174 # in that section uses "_fromuntrusted" as its command.
174 # in that section uses "_fromuntrusted" as its command.
175 untrustedhooks = _hookitems(ui, _untrusted=True)
175 untrustedhooks = _hookitems(ui, _untrusted=True)
176 for name, value in untrustedhooks.items():
176 for name, value in untrustedhooks.items():
177 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
177 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
178 if value != trustedvalue:
178 if value != trustedvalue:
179 (lp, lo, lk, lv) = trustedvalue
179 (lp, lo, lk, lv) = trustedvalue
180 hooks[name] = (lp, lo, lk, _fromuntrusted)
180 hooks[name] = (lp, lo, lk, _fromuntrusted)
181 # (end of the security sensitive section)
181 # (end of the security sensitive section)
182 return [(k, v) for p, o, k, v in sorted(hooks.values())]
182 return [(k, v) for p, o, k, v in sorted(hooks.values())]
183
183
184 def _hookitems(ui, _untrusted=False):
184 def _hookitems(ui, _untrusted=False):
185 """return all hooks items ready to be sorted"""
185 """return all hooks items ready to be sorted"""
186 hooks = {}
186 hooks = {}
187 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
187 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
188 if name.startswith('priority.') or name.startswith('tonative.'):
188 if name.startswith('priority.') or name.startswith('tonative.'):
189 continue
189 continue
190
190
191 priority = ui.configint('hooks', 'priority.%s' % name, 0)
191 priority = ui.configint('hooks', 'priority.%s' % name, 0)
192 hooks[name] = (-priority, len(hooks), name, cmd)
192 hooks[name] = (-priority, len(hooks), name, cmd)
193 return hooks
193 return hooks
194
194
195 _redirect = False
195 _redirect = False
196 def redirect(state):
196 def redirect(state):
197 global _redirect
197 global _redirect
198 _redirect = state
198 _redirect = state
199
199
200 def hashook(ui, htype):
200 def hashook(ui, htype):
201 """return True if a hook is configured for 'htype'"""
201 """return True if a hook is configured for 'htype'"""
202 if not ui.callhooks:
202 if not ui.callhooks:
203 return False
203 return False
204 for hname, cmd in _allhooks(ui):
204 for hname, cmd in _allhooks(ui):
205 if hname.split('.')[0] == htype and cmd:
205 if hname.split('.')[0] == htype and cmd:
206 return True
206 return True
207 return False
207 return False
208
208
209 def hook(ui, repo, htype, throw=False, **args):
209 def hook(ui, repo, htype, throw=False, **args):
210 if not ui.callhooks:
210 if not ui.callhooks:
211 return False
211 return False
212
212
213 hooks = []
213 hooks = []
214 for hname, cmd in _allhooks(ui):
214 for hname, cmd in _allhooks(ui):
215 if hname.split('.')[0] == htype and cmd:
215 if hname.split('.')[0] == htype and cmd:
216 hooks.append((hname, cmd))
216 hooks.append((hname, cmd))
217
217
218 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
218 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
219 r = False
219 r = False
220 for hname, cmd in hooks:
220 for hname, cmd in hooks:
221 r = res[hname][0] or r
221 r = res[hname][0] or r
222 return r
222 return r
223
223
224 def runhooks(ui, repo, htype, hooks, throw=False, **args):
224 def runhooks(ui, repo, htype, hooks, throw=False, **args):
225 args = pycompat.byteskwargs(args)
225 args = pycompat.byteskwargs(args)
226 res = {}
226 res = {}
227 oldstdout = -1
227 oldstdout = -1
228
228
229 try:
229 try:
230 for hname, cmd in hooks:
230 for hname, cmd in hooks:
231 if oldstdout == -1 and _redirect:
231 if oldstdout == -1 and _redirect:
232 try:
232 try:
233 stdoutno = procutil.stdout.fileno()
233 stdoutno = procutil.stdout.fileno()
234 stderrno = procutil.stderr.fileno()
234 stderrno = procutil.stderr.fileno()
235 # temporarily redirect stdout to stderr, if possible
235 # temporarily redirect stdout to stderr, if possible
236 if stdoutno >= 0 and stderrno >= 0:
236 if stdoutno >= 0 and stderrno >= 0:
237 procutil.stdout.flush()
237 procutil.stdout.flush()
238 oldstdout = os.dup(stdoutno)
238 oldstdout = os.dup(stdoutno)
239 os.dup2(stderrno, stdoutno)
239 os.dup2(stderrno, stdoutno)
240 except (OSError, AttributeError):
240 except (OSError, AttributeError):
241 # files seem to be bogus, give up on redirecting (WSGI, etc)
241 # files seem to be bogus, give up on redirecting (WSGI, etc)
242 pass
242 pass
243
243
244 if cmd is _fromuntrusted:
244 if cmd is _fromuntrusted:
245 if throw:
245 if throw:
246 raise error.HookAbort(
246 raise error.HookAbort(
247 _('untrusted hook %s not executed') % hname,
247 _('untrusted hook %s not executed') % hname,
248 hint = _("see 'hg help config.trusted'"))
248 hint = _("see 'hg help config.trusted'"))
249 ui.warn(_('warning: untrusted hook %s not executed\n') % hname)
249 ui.warn(_('warning: untrusted hook %s not executed\n') % hname)
250 r = 1
250 r = 1
251 raised = False
251 raised = False
252 elif callable(cmd):
252 elif callable(cmd):
253 r, raised = pythonhook(ui, repo, htype, hname, cmd, args,
253 r, raised = pythonhook(ui, repo, htype, hname, cmd, args,
254 throw)
254 throw)
255 elif cmd.startswith('python:'):
255 elif cmd.startswith('python:'):
256 if cmd.count(':') >= 2:
256 if cmd.count(':') >= 2:
257 path, cmd = cmd[7:].rsplit(':', 1)
257 path, cmd = cmd[7:].rsplit(':', 1)
258 path = util.expandpath(path)
258 path = util.expandpath(path)
259 if repo:
259 if repo:
260 path = os.path.join(repo.root, path)
260 path = os.path.join(repo.root, path)
261 try:
261 try:
262 mod = extensions.loadpath(path, 'hghook.%s' % hname)
262 mod = extensions.loadpath(path, 'hghook.%s' % hname)
263 except Exception:
263 except Exception:
264 ui.write(_("loading %s hook failed:\n") % hname)
264 ui.write(_("loading %s hook failed:\n") % hname)
265 raise
265 raise
266 hookfn = getattr(mod, cmd)
266 hookfn = getattr(mod, cmd)
267 else:
267 else:
268 hookfn = cmd[7:].strip()
268 hookfn = cmd[7:].strip()
269 r, raised = pythonhook(ui, repo, htype, hname, hookfn, args,
269 r, raised = pythonhook(ui, repo, htype, hname, hookfn, args,
270 throw)
270 throw)
271 else:
271 else:
272 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
272 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
273 raised = False
273 raised = False
274
274
275 res[hname] = r, raised
275 res[hname] = r, raised
276 finally:
276 finally:
277 # The stderr is fully buffered on Windows when connected to a pipe.
277 # The stderr is fully buffered on Windows when connected to a pipe.
278 # A forcible flush is required to make small stderr data in the
278 # A forcible flush is required to make small stderr data in the
279 # remote side available to the client immediately.
279 # remote side available to the client immediately.
280 procutil.stderr.flush()
280 procutil.stderr.flush()
281
281
282 if _redirect and oldstdout >= 0:
282 if _redirect and oldstdout >= 0:
283 procutil.stdout.flush() # write hook output to stderr fd
283 procutil.stdout.flush() # write hook output to stderr fd
284 os.dup2(oldstdout, stdoutno)
284 os.dup2(oldstdout, stdoutno)
285 os.close(oldstdout)
285 os.close(oldstdout)
286
286
287 return res
287 return res
General Comments 0
You need to be logged in to leave comments. Login now