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