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