##// END OF EJS Templates
procutil: add a shim for translating shell commands to native commands
Matt Harbison -
r38510:72286f9e default
parent child Browse files
Show More
@@ -1,283 +1,281 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, encoding.strtolocal(str(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 pycompat.iswindows:
142 cmd = procutil.shelltonative(cmd, env)
143 environ = procutil.shellenviron(env)
144 cmd = util.platform.shelltocmdexe(cmd, environ)
145
143
146 ui.note(_("running hook %s: %s\n") % (name, cmd))
144 ui.note(_("running hook %s: %s\n") % (name, cmd))
147
145
148 if repo:
146 if repo:
149 cwd = repo.root
147 cwd = repo.root
150 else:
148 else:
151 cwd = pycompat.getcwd()
149 cwd = pycompat.getcwd()
152 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag='exthook-%s' % (name,))
150 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag='exthook-%s' % (name,))
153
151
154 duration = util.timer() - starttime
152 duration = util.timer() - starttime
155 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
153 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
156 name, cmd, duration)
154 name, cmd, duration)
157 if r:
155 if r:
158 desc = procutil.explainexit(r)
156 desc = procutil.explainexit(r)
159 if throw:
157 if throw:
160 raise error.HookAbort(_('%s hook %s') % (name, desc))
158 raise error.HookAbort(_('%s hook %s') % (name, desc))
161 ui.warn(_('warning: %s hook %s\n') % (name, desc))
159 ui.warn(_('warning: %s hook %s\n') % (name, desc))
162 return r
160 return r
163
161
164 # represent an untrusted hook command
162 # represent an untrusted hook command
165 _fromuntrusted = object()
163 _fromuntrusted = object()
166
164
167 def _allhooks(ui):
165 def _allhooks(ui):
168 """return a list of (hook-id, cmd) pairs sorted by priority"""
166 """return a list of (hook-id, cmd) pairs sorted by priority"""
169 hooks = _hookitems(ui)
167 hooks = _hookitems(ui)
170 # Be careful in this section, propagating the real commands from untrusted
168 # Be careful in this section, propagating the real commands from untrusted
171 # sources would create a security vulnerability, make sure anything altered
169 # sources would create a security vulnerability, make sure anything altered
172 # in that section uses "_fromuntrusted" as its command.
170 # in that section uses "_fromuntrusted" as its command.
173 untrustedhooks = _hookitems(ui, _untrusted=True)
171 untrustedhooks = _hookitems(ui, _untrusted=True)
174 for name, value in untrustedhooks.items():
172 for name, value in untrustedhooks.items():
175 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
173 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
176 if value != trustedvalue:
174 if value != trustedvalue:
177 (lp, lo, lk, lv) = trustedvalue
175 (lp, lo, lk, lv) = trustedvalue
178 hooks[name] = (lp, lo, lk, _fromuntrusted)
176 hooks[name] = (lp, lo, lk, _fromuntrusted)
179 # (end of the security sensitive section)
177 # (end of the security sensitive section)
180 return [(k, v) for p, o, k, v in sorted(hooks.values())]
178 return [(k, v) for p, o, k, v in sorted(hooks.values())]
181
179
182 def _hookitems(ui, _untrusted=False):
180 def _hookitems(ui, _untrusted=False):
183 """return all hooks items ready to be sorted"""
181 """return all hooks items ready to be sorted"""
184 hooks = {}
182 hooks = {}
185 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
183 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
186 if not name.startswith('priority'):
184 if not name.startswith('priority'):
187 priority = ui.configint('hooks', 'priority.%s' % name, 0)
185 priority = ui.configint('hooks', 'priority.%s' % name, 0)
188 hooks[name] = (-priority, len(hooks), name, cmd)
186 hooks[name] = (-priority, len(hooks), name, cmd)
189 return hooks
187 return hooks
190
188
191 _redirect = False
189 _redirect = False
192 def redirect(state):
190 def redirect(state):
193 global _redirect
191 global _redirect
194 _redirect = state
192 _redirect = state
195
193
196 def hashook(ui, htype):
194 def hashook(ui, htype):
197 """return True if a hook is configured for 'htype'"""
195 """return True if a hook is configured for 'htype'"""
198 if not ui.callhooks:
196 if not ui.callhooks:
199 return False
197 return False
200 for hname, cmd in _allhooks(ui):
198 for hname, cmd in _allhooks(ui):
201 if hname.split('.')[0] == htype and cmd:
199 if hname.split('.')[0] == htype and cmd:
202 return True
200 return True
203 return False
201 return False
204
202
205 def hook(ui, repo, htype, throw=False, **args):
203 def hook(ui, repo, htype, throw=False, **args):
206 if not ui.callhooks:
204 if not ui.callhooks:
207 return False
205 return False
208
206
209 hooks = []
207 hooks = []
210 for hname, cmd in _allhooks(ui):
208 for hname, cmd in _allhooks(ui):
211 if hname.split('.')[0] == htype and cmd:
209 if hname.split('.')[0] == htype and cmd:
212 hooks.append((hname, cmd))
210 hooks.append((hname, cmd))
213
211
214 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
212 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
215 r = False
213 r = False
216 for hname, cmd in hooks:
214 for hname, cmd in hooks:
217 r = res[hname][0] or r
215 r = res[hname][0] or r
218 return r
216 return r
219
217
220 def runhooks(ui, repo, htype, hooks, throw=False, **args):
218 def runhooks(ui, repo, htype, hooks, throw=False, **args):
221 args = pycompat.byteskwargs(args)
219 args = pycompat.byteskwargs(args)
222 res = {}
220 res = {}
223 oldstdout = -1
221 oldstdout = -1
224
222
225 try:
223 try:
226 for hname, cmd in hooks:
224 for hname, cmd in hooks:
227 if oldstdout == -1 and _redirect:
225 if oldstdout == -1 and _redirect:
228 try:
226 try:
229 stdoutno = procutil.stdout.fileno()
227 stdoutno = procutil.stdout.fileno()
230 stderrno = procutil.stderr.fileno()
228 stderrno = procutil.stderr.fileno()
231 # temporarily redirect stdout to stderr, if possible
229 # temporarily redirect stdout to stderr, if possible
232 if stdoutno >= 0 and stderrno >= 0:
230 if stdoutno >= 0 and stderrno >= 0:
233 procutil.stdout.flush()
231 procutil.stdout.flush()
234 oldstdout = os.dup(stdoutno)
232 oldstdout = os.dup(stdoutno)
235 os.dup2(stderrno, stdoutno)
233 os.dup2(stderrno, stdoutno)
236 except (OSError, AttributeError):
234 except (OSError, AttributeError):
237 # files seem to be bogus, give up on redirecting (WSGI, etc)
235 # files seem to be bogus, give up on redirecting (WSGI, etc)
238 pass
236 pass
239
237
240 if cmd is _fromuntrusted:
238 if cmd is _fromuntrusted:
241 if throw:
239 if throw:
242 raise error.HookAbort(
240 raise error.HookAbort(
243 _('untrusted hook %s not executed') % hname,
241 _('untrusted hook %s not executed') % hname,
244 hint = _("see 'hg help config.trusted'"))
242 hint = _("see 'hg help config.trusted'"))
245 ui.warn(_('warning: untrusted hook %s not executed\n') % hname)
243 ui.warn(_('warning: untrusted hook %s not executed\n') % hname)
246 r = 1
244 r = 1
247 raised = False
245 raised = False
248 elif callable(cmd):
246 elif callable(cmd):
249 r, raised = pythonhook(ui, repo, htype, hname, cmd, args,
247 r, raised = pythonhook(ui, repo, htype, hname, cmd, args,
250 throw)
248 throw)
251 elif cmd.startswith('python:'):
249 elif cmd.startswith('python:'):
252 if cmd.count(':') >= 2:
250 if cmd.count(':') >= 2:
253 path, cmd = cmd[7:].rsplit(':', 1)
251 path, cmd = cmd[7:].rsplit(':', 1)
254 path = util.expandpath(path)
252 path = util.expandpath(path)
255 if repo:
253 if repo:
256 path = os.path.join(repo.root, path)
254 path = os.path.join(repo.root, path)
257 try:
255 try:
258 mod = extensions.loadpath(path, 'hghook.%s' % hname)
256 mod = extensions.loadpath(path, 'hghook.%s' % hname)
259 except Exception:
257 except Exception:
260 ui.write(_("loading %s hook failed:\n") % hname)
258 ui.write(_("loading %s hook failed:\n") % hname)
261 raise
259 raise
262 hookfn = getattr(mod, cmd)
260 hookfn = getattr(mod, cmd)
263 else:
261 else:
264 hookfn = cmd[7:].strip()
262 hookfn = cmd[7:].strip()
265 r, raised = pythonhook(ui, repo, htype, hname, hookfn, args,
263 r, raised = pythonhook(ui, repo, htype, hname, hookfn, args,
266 throw)
264 throw)
267 else:
265 else:
268 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
266 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
269 raised = False
267 raised = False
270
268
271 res[hname] = r, raised
269 res[hname] = r, raised
272 finally:
270 finally:
273 # The stderr is fully buffered on Windows when connected to a pipe.
271 # The stderr is fully buffered on Windows when connected to a pipe.
274 # A forcible flush is required to make small stderr data in the
272 # A forcible flush is required to make small stderr data in the
275 # remote side available to the client immediately.
273 # remote side available to the client immediately.
276 procutil.stderr.flush()
274 procutil.stderr.flush()
277
275
278 if _redirect and oldstdout >= 0:
276 if _redirect and oldstdout >= 0:
279 procutil.stdout.flush() # write hook output to stderr fd
277 procutil.stdout.flush() # write hook output to stderr fd
280 os.dup2(oldstdout, stdoutno)
278 os.dup2(oldstdout, stdoutno)
281 os.close(oldstdout)
279 os.close(oldstdout)
282
280
283 return res
281 return res
@@ -1,410 +1,417 b''
1 # procutil.py - utility for managing processes and executable environment
1 # procutil.py - utility for managing processes and executable environment
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import contextlib
12 import contextlib
13 import imp
13 import imp
14 import io
14 import io
15 import os
15 import os
16 import signal
16 import signal
17 import subprocess
17 import subprocess
18 import sys
18 import sys
19 import time
19 import time
20
20
21 from ..i18n import _
21 from ..i18n import _
22
22
23 from .. import (
23 from .. import (
24 encoding,
24 encoding,
25 error,
25 error,
26 policy,
26 policy,
27 pycompat,
27 pycompat,
28 )
28 )
29
29
30 osutil = policy.importmod(r'osutil')
30 osutil = policy.importmod(r'osutil')
31
31
32 stderr = pycompat.stderr
32 stderr = pycompat.stderr
33 stdin = pycompat.stdin
33 stdin = pycompat.stdin
34 stdout = pycompat.stdout
34 stdout = pycompat.stdout
35
35
36 def isatty(fp):
36 def isatty(fp):
37 try:
37 try:
38 return fp.isatty()
38 return fp.isatty()
39 except AttributeError:
39 except AttributeError:
40 return False
40 return False
41
41
42 # glibc determines buffering on first write to stdout - if we replace a TTY
42 # glibc determines buffering on first write to stdout - if we replace a TTY
43 # destined stdout with a pipe destined stdout (e.g. pager), we want line
43 # destined stdout with a pipe destined stdout (e.g. pager), we want line
44 # buffering (or unbuffered, on Windows)
44 # buffering (or unbuffered, on Windows)
45 if isatty(stdout):
45 if isatty(stdout):
46 if pycompat.iswindows:
46 if pycompat.iswindows:
47 # Windows doesn't support line buffering
47 # Windows doesn't support line buffering
48 stdout = os.fdopen(stdout.fileno(), r'wb', 0)
48 stdout = os.fdopen(stdout.fileno(), r'wb', 0)
49 else:
49 else:
50 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
50 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
51
51
52 if pycompat.iswindows:
52 if pycompat.iswindows:
53 from .. import windows as platform
53 from .. import windows as platform
54 stdout = platform.winstdout(stdout)
54 stdout = platform.winstdout(stdout)
55 else:
55 else:
56 from .. import posix as platform
56 from .. import posix as platform
57
57
58 findexe = platform.findexe
58 findexe = platform.findexe
59 _gethgcmd = platform.gethgcmd
59 _gethgcmd = platform.gethgcmd
60 getuser = platform.getuser
60 getuser = platform.getuser
61 getpid = os.getpid
61 getpid = os.getpid
62 hidewindow = platform.hidewindow
62 hidewindow = platform.hidewindow
63 quotecommand = platform.quotecommand
63 quotecommand = platform.quotecommand
64 readpipe = platform.readpipe
64 readpipe = platform.readpipe
65 setbinary = platform.setbinary
65 setbinary = platform.setbinary
66 setsignalhandler = platform.setsignalhandler
66 setsignalhandler = platform.setsignalhandler
67 shellquote = platform.shellquote
67 shellquote = platform.shellquote
68 shellsplit = platform.shellsplit
68 shellsplit = platform.shellsplit
69 spawndetached = platform.spawndetached
69 spawndetached = platform.spawndetached
70 sshargs = platform.sshargs
70 sshargs = platform.sshargs
71 testpid = platform.testpid
71 testpid = platform.testpid
72
72
73 try:
73 try:
74 setprocname = osutil.setprocname
74 setprocname = osutil.setprocname
75 except AttributeError:
75 except AttributeError:
76 pass
76 pass
77 try:
77 try:
78 unblocksignal = osutil.unblocksignal
78 unblocksignal = osutil.unblocksignal
79 except AttributeError:
79 except AttributeError:
80 pass
80 pass
81
81
82 closefds = pycompat.isposix
82 closefds = pycompat.isposix
83
83
84 def explainexit(code):
84 def explainexit(code):
85 """return a message describing a subprocess status
85 """return a message describing a subprocess status
86 (codes from kill are negative - not os.system/wait encoding)"""
86 (codes from kill are negative - not os.system/wait encoding)"""
87 if code >= 0:
87 if code >= 0:
88 return _("exited with status %d") % code
88 return _("exited with status %d") % code
89 return _("killed by signal %d") % -code
89 return _("killed by signal %d") % -code
90
90
91 class _pfile(object):
91 class _pfile(object):
92 """File-like wrapper for a stream opened by subprocess.Popen()"""
92 """File-like wrapper for a stream opened by subprocess.Popen()"""
93
93
94 def __init__(self, proc, fp):
94 def __init__(self, proc, fp):
95 self._proc = proc
95 self._proc = proc
96 self._fp = fp
96 self._fp = fp
97
97
98 def close(self):
98 def close(self):
99 # unlike os.popen(), this returns an integer in subprocess coding
99 # unlike os.popen(), this returns an integer in subprocess coding
100 self._fp.close()
100 self._fp.close()
101 return self._proc.wait()
101 return self._proc.wait()
102
102
103 def __iter__(self):
103 def __iter__(self):
104 return iter(self._fp)
104 return iter(self._fp)
105
105
106 def __getattr__(self, attr):
106 def __getattr__(self, attr):
107 return getattr(self._fp, attr)
107 return getattr(self._fp, attr)
108
108
109 def __enter__(self):
109 def __enter__(self):
110 return self
110 return self
111
111
112 def __exit__(self, exc_type, exc_value, exc_tb):
112 def __exit__(self, exc_type, exc_value, exc_tb):
113 self.close()
113 self.close()
114
114
115 def popen(cmd, mode='rb', bufsize=-1):
115 def popen(cmd, mode='rb', bufsize=-1):
116 if mode == 'rb':
116 if mode == 'rb':
117 return _popenreader(cmd, bufsize)
117 return _popenreader(cmd, bufsize)
118 elif mode == 'wb':
118 elif mode == 'wb':
119 return _popenwriter(cmd, bufsize)
119 return _popenwriter(cmd, bufsize)
120 raise error.ProgrammingError('unsupported mode: %r' % mode)
120 raise error.ProgrammingError('unsupported mode: %r' % mode)
121
121
122 def _popenreader(cmd, bufsize):
122 def _popenreader(cmd, bufsize):
123 p = subprocess.Popen(quotecommand(cmd), shell=True, bufsize=bufsize,
123 p = subprocess.Popen(quotecommand(cmd), shell=True, bufsize=bufsize,
124 close_fds=closefds,
124 close_fds=closefds,
125 stdout=subprocess.PIPE)
125 stdout=subprocess.PIPE)
126 return _pfile(p, p.stdout)
126 return _pfile(p, p.stdout)
127
127
128 def _popenwriter(cmd, bufsize):
128 def _popenwriter(cmd, bufsize):
129 p = subprocess.Popen(quotecommand(cmd), shell=True, bufsize=bufsize,
129 p = subprocess.Popen(quotecommand(cmd), shell=True, bufsize=bufsize,
130 close_fds=closefds,
130 close_fds=closefds,
131 stdin=subprocess.PIPE)
131 stdin=subprocess.PIPE)
132 return _pfile(p, p.stdin)
132 return _pfile(p, p.stdin)
133
133
134 def popen2(cmd, env=None):
134 def popen2(cmd, env=None):
135 # Setting bufsize to -1 lets the system decide the buffer size.
135 # Setting bufsize to -1 lets the system decide the buffer size.
136 # The default for bufsize is 0, meaning unbuffered. This leads to
136 # The default for bufsize is 0, meaning unbuffered. This leads to
137 # poor performance on Mac OS X: http://bugs.python.org/issue4194
137 # poor performance on Mac OS X: http://bugs.python.org/issue4194
138 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
138 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
139 close_fds=closefds,
139 close_fds=closefds,
140 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
140 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
141 env=env)
141 env=env)
142 return p.stdin, p.stdout
142 return p.stdin, p.stdout
143
143
144 def popen3(cmd, env=None):
144 def popen3(cmd, env=None):
145 stdin, stdout, stderr, p = popen4(cmd, env)
145 stdin, stdout, stderr, p = popen4(cmd, env)
146 return stdin, stdout, stderr
146 return stdin, stdout, stderr
147
147
148 def popen4(cmd, env=None, bufsize=-1):
148 def popen4(cmd, env=None, bufsize=-1):
149 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
149 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
150 close_fds=closefds,
150 close_fds=closefds,
151 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
151 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
152 stderr=subprocess.PIPE,
152 stderr=subprocess.PIPE,
153 env=env)
153 env=env)
154 return p.stdin, p.stdout, p.stderr, p
154 return p.stdin, p.stdout, p.stderr, p
155
155
156 def pipefilter(s, cmd):
156 def pipefilter(s, cmd):
157 '''filter string S through command CMD, returning its output'''
157 '''filter string S through command CMD, returning its output'''
158 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
158 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
159 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
159 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
160 pout, perr = p.communicate(s)
160 pout, perr = p.communicate(s)
161 return pout
161 return pout
162
162
163 def tempfilter(s, cmd):
163 def tempfilter(s, cmd):
164 '''filter string S through a pair of temporary files with CMD.
164 '''filter string S through a pair of temporary files with CMD.
165 CMD is used as a template to create the real command to be run,
165 CMD is used as a template to create the real command to be run,
166 with the strings INFILE and OUTFILE replaced by the real names of
166 with the strings INFILE and OUTFILE replaced by the real names of
167 the temporary files generated.'''
167 the temporary files generated.'''
168 inname, outname = None, None
168 inname, outname = None, None
169 try:
169 try:
170 infd, inname = pycompat.mkstemp(prefix='hg-filter-in-')
170 infd, inname = pycompat.mkstemp(prefix='hg-filter-in-')
171 fp = os.fdopen(infd, r'wb')
171 fp = os.fdopen(infd, r'wb')
172 fp.write(s)
172 fp.write(s)
173 fp.close()
173 fp.close()
174 outfd, outname = pycompat.mkstemp(prefix='hg-filter-out-')
174 outfd, outname = pycompat.mkstemp(prefix='hg-filter-out-')
175 os.close(outfd)
175 os.close(outfd)
176 cmd = cmd.replace('INFILE', inname)
176 cmd = cmd.replace('INFILE', inname)
177 cmd = cmd.replace('OUTFILE', outname)
177 cmd = cmd.replace('OUTFILE', outname)
178 code = system(cmd)
178 code = system(cmd)
179 if pycompat.sysplatform == 'OpenVMS' and code & 1:
179 if pycompat.sysplatform == 'OpenVMS' and code & 1:
180 code = 0
180 code = 0
181 if code:
181 if code:
182 raise error.Abort(_("command '%s' failed: %s") %
182 raise error.Abort(_("command '%s' failed: %s") %
183 (cmd, explainexit(code)))
183 (cmd, explainexit(code)))
184 with open(outname, 'rb') as fp:
184 with open(outname, 'rb') as fp:
185 return fp.read()
185 return fp.read()
186 finally:
186 finally:
187 try:
187 try:
188 if inname:
188 if inname:
189 os.unlink(inname)
189 os.unlink(inname)
190 except OSError:
190 except OSError:
191 pass
191 pass
192 try:
192 try:
193 if outname:
193 if outname:
194 os.unlink(outname)
194 os.unlink(outname)
195 except OSError:
195 except OSError:
196 pass
196 pass
197
197
198 _filtertable = {
198 _filtertable = {
199 'tempfile:': tempfilter,
199 'tempfile:': tempfilter,
200 'pipe:': pipefilter,
200 'pipe:': pipefilter,
201 }
201 }
202
202
203 def filter(s, cmd):
203 def filter(s, cmd):
204 "filter a string through a command that transforms its input to its output"
204 "filter a string through a command that transforms its input to its output"
205 for name, fn in _filtertable.iteritems():
205 for name, fn in _filtertable.iteritems():
206 if cmd.startswith(name):
206 if cmd.startswith(name):
207 return fn(s, cmd[len(name):].lstrip())
207 return fn(s, cmd[len(name):].lstrip())
208 return pipefilter(s, cmd)
208 return pipefilter(s, cmd)
209
209
210 def mainfrozen():
210 def mainfrozen():
211 """return True if we are a frozen executable.
211 """return True if we are a frozen executable.
212
212
213 The code supports py2exe (most common, Windows only) and tools/freeze
213 The code supports py2exe (most common, Windows only) and tools/freeze
214 (portable, not much used).
214 (portable, not much used).
215 """
215 """
216 return (pycompat.safehasattr(sys, "frozen") or # new py2exe
216 return (pycompat.safehasattr(sys, "frozen") or # new py2exe
217 pycompat.safehasattr(sys, "importers") or # old py2exe
217 pycompat.safehasattr(sys, "importers") or # old py2exe
218 imp.is_frozen(u"__main__")) # tools/freeze
218 imp.is_frozen(u"__main__")) # tools/freeze
219
219
220 _hgexecutable = None
220 _hgexecutable = None
221
221
222 def hgexecutable():
222 def hgexecutable():
223 """return location of the 'hg' executable.
223 """return location of the 'hg' executable.
224
224
225 Defaults to $HG or 'hg' in the search path.
225 Defaults to $HG or 'hg' in the search path.
226 """
226 """
227 if _hgexecutable is None:
227 if _hgexecutable is None:
228 hg = encoding.environ.get('HG')
228 hg = encoding.environ.get('HG')
229 mainmod = sys.modules[r'__main__']
229 mainmod = sys.modules[r'__main__']
230 if hg:
230 if hg:
231 _sethgexecutable(hg)
231 _sethgexecutable(hg)
232 elif mainfrozen():
232 elif mainfrozen():
233 if getattr(sys, 'frozen', None) == 'macosx_app':
233 if getattr(sys, 'frozen', None) == 'macosx_app':
234 # Env variable set by py2app
234 # Env variable set by py2app
235 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
235 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
236 else:
236 else:
237 _sethgexecutable(pycompat.sysexecutable)
237 _sethgexecutable(pycompat.sysexecutable)
238 elif (os.path.basename(
238 elif (os.path.basename(
239 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
239 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
240 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
240 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
241 else:
241 else:
242 exe = findexe('hg') or os.path.basename(sys.argv[0])
242 exe = findexe('hg') or os.path.basename(sys.argv[0])
243 _sethgexecutable(exe)
243 _sethgexecutable(exe)
244 return _hgexecutable
244 return _hgexecutable
245
245
246 def _sethgexecutable(path):
246 def _sethgexecutable(path):
247 """set location of the 'hg' executable"""
247 """set location of the 'hg' executable"""
248 global _hgexecutable
248 global _hgexecutable
249 _hgexecutable = path
249 _hgexecutable = path
250
250
251 def _testfileno(f, stdf):
251 def _testfileno(f, stdf):
252 fileno = getattr(f, 'fileno', None)
252 fileno = getattr(f, 'fileno', None)
253 try:
253 try:
254 return fileno and fileno() == stdf.fileno()
254 return fileno and fileno() == stdf.fileno()
255 except io.UnsupportedOperation:
255 except io.UnsupportedOperation:
256 return False # fileno() raised UnsupportedOperation
256 return False # fileno() raised UnsupportedOperation
257
257
258 def isstdin(f):
258 def isstdin(f):
259 return _testfileno(f, sys.__stdin__)
259 return _testfileno(f, sys.__stdin__)
260
260
261 def isstdout(f):
261 def isstdout(f):
262 return _testfileno(f, sys.__stdout__)
262 return _testfileno(f, sys.__stdout__)
263
263
264 def protectstdio(uin, uout):
264 def protectstdio(uin, uout):
265 """Duplicate streams and redirect original if (uin, uout) are stdio
265 """Duplicate streams and redirect original if (uin, uout) are stdio
266
266
267 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
267 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
268 redirected to stderr so the output is still readable.
268 redirected to stderr so the output is still readable.
269
269
270 Returns (fin, fout) which point to the original (uin, uout) fds, but
270 Returns (fin, fout) which point to the original (uin, uout) fds, but
271 may be copy of (uin, uout). The returned streams can be considered
271 may be copy of (uin, uout). The returned streams can be considered
272 "owned" in that print(), exec(), etc. never reach to them.
272 "owned" in that print(), exec(), etc. never reach to them.
273 """
273 """
274 uout.flush()
274 uout.flush()
275 fin, fout = uin, uout
275 fin, fout = uin, uout
276 if uin is stdin:
276 if uin is stdin:
277 newfd = os.dup(uin.fileno())
277 newfd = os.dup(uin.fileno())
278 nullfd = os.open(os.devnull, os.O_RDONLY)
278 nullfd = os.open(os.devnull, os.O_RDONLY)
279 os.dup2(nullfd, uin.fileno())
279 os.dup2(nullfd, uin.fileno())
280 os.close(nullfd)
280 os.close(nullfd)
281 fin = os.fdopen(newfd, r'rb')
281 fin = os.fdopen(newfd, r'rb')
282 if uout is stdout:
282 if uout is stdout:
283 newfd = os.dup(uout.fileno())
283 newfd = os.dup(uout.fileno())
284 os.dup2(stderr.fileno(), uout.fileno())
284 os.dup2(stderr.fileno(), uout.fileno())
285 fout = os.fdopen(newfd, r'wb')
285 fout = os.fdopen(newfd, r'wb')
286 return fin, fout
286 return fin, fout
287
287
288 def restorestdio(uin, uout, fin, fout):
288 def restorestdio(uin, uout, fin, fout):
289 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
289 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
290 uout.flush()
290 uout.flush()
291 for f, uif in [(fin, uin), (fout, uout)]:
291 for f, uif in [(fin, uin), (fout, uout)]:
292 if f is not uif:
292 if f is not uif:
293 os.dup2(f.fileno(), uif.fileno())
293 os.dup2(f.fileno(), uif.fileno())
294 f.close()
294 f.close()
295
295
296 @contextlib.contextmanager
296 @contextlib.contextmanager
297 def protectedstdio(uin, uout):
297 def protectedstdio(uin, uout):
298 """Run code block with protected standard streams"""
298 """Run code block with protected standard streams"""
299 fin, fout = protectstdio(uin, uout)
299 fin, fout = protectstdio(uin, uout)
300 try:
300 try:
301 yield fin, fout
301 yield fin, fout
302 finally:
302 finally:
303 restorestdio(uin, uout, fin, fout)
303 restorestdio(uin, uout, fin, fout)
304
304
305 def shellenviron(environ=None):
305 def shellenviron(environ=None):
306 """return environ with optional override, useful for shelling out"""
306 """return environ with optional override, useful for shelling out"""
307 def py2shell(val):
307 def py2shell(val):
308 'convert python object into string that is useful to shell'
308 'convert python object into string that is useful to shell'
309 if val is None or val is False:
309 if val is None or val is False:
310 return '0'
310 return '0'
311 if val is True:
311 if val is True:
312 return '1'
312 return '1'
313 return pycompat.bytestr(val)
313 return pycompat.bytestr(val)
314 env = dict(encoding.environ)
314 env = dict(encoding.environ)
315 if environ:
315 if environ:
316 env.update((k, py2shell(v)) for k, v in environ.iteritems())
316 env.update((k, py2shell(v)) for k, v in environ.iteritems())
317 env['HG'] = hgexecutable()
317 env['HG'] = hgexecutable()
318 return env
318 return env
319
319
320 if pycompat.iswindows:
321 def shelltonative(cmd, env):
322 return platform.shelltocmdexe(cmd, shellenviron(env))
323 else:
324 def shelltonative(cmd, env):
325 return cmd
326
320 def system(cmd, environ=None, cwd=None, out=None):
327 def system(cmd, environ=None, cwd=None, out=None):
321 '''enhanced shell command execution.
328 '''enhanced shell command execution.
322 run with environment maybe modified, maybe in different dir.
329 run with environment maybe modified, maybe in different dir.
323
330
324 if out is specified, it is assumed to be a file-like object that has a
331 if out is specified, it is assumed to be a file-like object that has a
325 write() method. stdout and stderr will be redirected to out.'''
332 write() method. stdout and stderr will be redirected to out.'''
326 try:
333 try:
327 stdout.flush()
334 stdout.flush()
328 except Exception:
335 except Exception:
329 pass
336 pass
330 cmd = quotecommand(cmd)
337 cmd = quotecommand(cmd)
331 env = shellenviron(environ)
338 env = shellenviron(environ)
332 if out is None or isstdout(out):
339 if out is None or isstdout(out):
333 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
340 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
334 env=env, cwd=cwd)
341 env=env, cwd=cwd)
335 else:
342 else:
336 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
343 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
337 env=env, cwd=cwd, stdout=subprocess.PIPE,
344 env=env, cwd=cwd, stdout=subprocess.PIPE,
338 stderr=subprocess.STDOUT)
345 stderr=subprocess.STDOUT)
339 for line in iter(proc.stdout.readline, ''):
346 for line in iter(proc.stdout.readline, ''):
340 out.write(line)
347 out.write(line)
341 proc.wait()
348 proc.wait()
342 rc = proc.returncode
349 rc = proc.returncode
343 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
350 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
344 rc = 0
351 rc = 0
345 return rc
352 return rc
346
353
347 def gui():
354 def gui():
348 '''Are we running in a GUI?'''
355 '''Are we running in a GUI?'''
349 if pycompat.isdarwin:
356 if pycompat.isdarwin:
350 if 'SSH_CONNECTION' in encoding.environ:
357 if 'SSH_CONNECTION' in encoding.environ:
351 # handle SSH access to a box where the user is logged in
358 # handle SSH access to a box where the user is logged in
352 return False
359 return False
353 elif getattr(osutil, 'isgui', None):
360 elif getattr(osutil, 'isgui', None):
354 # check if a CoreGraphics session is available
361 # check if a CoreGraphics session is available
355 return osutil.isgui()
362 return osutil.isgui()
356 else:
363 else:
357 # pure build; use a safe default
364 # pure build; use a safe default
358 return True
365 return True
359 else:
366 else:
360 return pycompat.iswindows or encoding.environ.get("DISPLAY")
367 return pycompat.iswindows or encoding.environ.get("DISPLAY")
361
368
362 def hgcmd():
369 def hgcmd():
363 """Return the command used to execute current hg
370 """Return the command used to execute current hg
364
371
365 This is different from hgexecutable() because on Windows we want
372 This is different from hgexecutable() because on Windows we want
366 to avoid things opening new shell windows like batch files, so we
373 to avoid things opening new shell windows like batch files, so we
367 get either the python call or current executable.
374 get either the python call or current executable.
368 """
375 """
369 if mainfrozen():
376 if mainfrozen():
370 if getattr(sys, 'frozen', None) == 'macosx_app':
377 if getattr(sys, 'frozen', None) == 'macosx_app':
371 # Env variable set by py2app
378 # Env variable set by py2app
372 return [encoding.environ['EXECUTABLEPATH']]
379 return [encoding.environ['EXECUTABLEPATH']]
373 else:
380 else:
374 return [pycompat.sysexecutable]
381 return [pycompat.sysexecutable]
375 return _gethgcmd()
382 return _gethgcmd()
376
383
377 def rundetached(args, condfn):
384 def rundetached(args, condfn):
378 """Execute the argument list in a detached process.
385 """Execute the argument list in a detached process.
379
386
380 condfn is a callable which is called repeatedly and should return
387 condfn is a callable which is called repeatedly and should return
381 True once the child process is known to have started successfully.
388 True once the child process is known to have started successfully.
382 At this point, the child process PID is returned. If the child
389 At this point, the child process PID is returned. If the child
383 process fails to start or finishes before condfn() evaluates to
390 process fails to start or finishes before condfn() evaluates to
384 True, return -1.
391 True, return -1.
385 """
392 """
386 # Windows case is easier because the child process is either
393 # Windows case is easier because the child process is either
387 # successfully starting and validating the condition or exiting
394 # successfully starting and validating the condition or exiting
388 # on failure. We just poll on its PID. On Unix, if the child
395 # on failure. We just poll on its PID. On Unix, if the child
389 # process fails to start, it will be left in a zombie state until
396 # process fails to start, it will be left in a zombie state until
390 # the parent wait on it, which we cannot do since we expect a long
397 # the parent wait on it, which we cannot do since we expect a long
391 # running process on success. Instead we listen for SIGCHLD telling
398 # running process on success. Instead we listen for SIGCHLD telling
392 # us our child process terminated.
399 # us our child process terminated.
393 terminated = set()
400 terminated = set()
394 def handler(signum, frame):
401 def handler(signum, frame):
395 terminated.add(os.wait())
402 terminated.add(os.wait())
396 prevhandler = None
403 prevhandler = None
397 SIGCHLD = getattr(signal, 'SIGCHLD', None)
404 SIGCHLD = getattr(signal, 'SIGCHLD', None)
398 if SIGCHLD is not None:
405 if SIGCHLD is not None:
399 prevhandler = signal.signal(SIGCHLD, handler)
406 prevhandler = signal.signal(SIGCHLD, handler)
400 try:
407 try:
401 pid = spawndetached(args)
408 pid = spawndetached(args)
402 while not condfn():
409 while not condfn():
403 if ((pid in terminated or not testpid(pid))
410 if ((pid in terminated or not testpid(pid))
404 and not condfn()):
411 and not condfn()):
405 return -1
412 return -1
406 time.sleep(0.1)
413 time.sleep(0.1)
407 return pid
414 return pid
408 finally:
415 finally:
409 if prevhandler is not None:
416 if prevhandler is not None:
410 signal.signal(signal.SIGCHLD, prevhandler)
417 signal.signal(signal.SIGCHLD, prevhandler)
General Comments 0
You need to be logged in to leave comments. Login now