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