##// END OF EJS Templates
chgserver: backport py3 buffered I/O workarounds from procutil...
Yuya Nishihara -
r46452:b56feaa9 default
parent child Browse files
Show More
@@ -1,741 +1,753 b''
1 # chgserver.py - command server extension for cHg
1 # chgserver.py - command server extension for cHg
2 #
2 #
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
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 """command server extension for cHg
8 """command server extension for cHg
9
9
10 'S' channel (read/write)
10 'S' channel (read/write)
11 propagate ui.system() request to client
11 propagate ui.system() request to client
12
12
13 'attachio' command
13 'attachio' command
14 attach client's stdio passed by sendmsg()
14 attach client's stdio passed by sendmsg()
15
15
16 'chdir' command
16 'chdir' command
17 change current directory
17 change current directory
18
18
19 'setenv' command
19 'setenv' command
20 replace os.environ completely
20 replace os.environ completely
21
21
22 'setumask' command (DEPRECATED)
22 'setumask' command (DEPRECATED)
23 'setumask2' command
23 'setumask2' command
24 set umask
24 set umask
25
25
26 'validate' command
26 'validate' command
27 reload the config and check if the server is up to date
27 reload the config and check if the server is up to date
28
28
29 Config
29 Config
30 ------
30 ------
31
31
32 ::
32 ::
33
33
34 [chgserver]
34 [chgserver]
35 # how long (in seconds) should an idle chg server exit
35 # how long (in seconds) should an idle chg server exit
36 idletimeout = 3600
36 idletimeout = 3600
37
37
38 # whether to skip config or env change checks
38 # whether to skip config or env change checks
39 skiphash = False
39 skiphash = False
40 """
40 """
41
41
42 from __future__ import absolute_import
42 from __future__ import absolute_import
43
43
44 import inspect
44 import inspect
45 import os
45 import os
46 import re
46 import re
47 import socket
47 import socket
48 import stat
48 import stat
49 import struct
49 import struct
50 import time
50 import time
51
51
52 from .i18n import _
52 from .i18n import _
53 from .pycompat import (
53 from .pycompat import (
54 getattr,
54 getattr,
55 setattr,
55 setattr,
56 )
56 )
57
57
58 from . import (
58 from . import (
59 commandserver,
59 commandserver,
60 encoding,
60 encoding,
61 error,
61 error,
62 extensions,
62 extensions,
63 node,
63 node,
64 pycompat,
64 pycompat,
65 util,
65 util,
66 )
66 )
67
67
68 from .utils import (
68 from .utils import (
69 hashutil,
69 hashutil,
70 procutil,
70 procutil,
71 stringutil,
71 stringutil,
72 )
72 )
73
73
74
74
75 def _hashlist(items):
75 def _hashlist(items):
76 """return sha1 hexdigest for a list"""
76 """return sha1 hexdigest for a list"""
77 return node.hex(hashutil.sha1(stringutil.pprint(items)).digest())
77 return node.hex(hashutil.sha1(stringutil.pprint(items)).digest())
78
78
79
79
80 # sensitive config sections affecting confighash
80 # sensitive config sections affecting confighash
81 _configsections = [
81 _configsections = [
82 b'alias', # affects global state commands.table
82 b'alias', # affects global state commands.table
83 b'diff-tools', # affects whether gui or not in extdiff's uisetup
83 b'diff-tools', # affects whether gui or not in extdiff's uisetup
84 b'eol', # uses setconfig('eol', ...)
84 b'eol', # uses setconfig('eol', ...)
85 b'extdiff', # uisetup will register new commands
85 b'extdiff', # uisetup will register new commands
86 b'extensions',
86 b'extensions',
87 b'fastannotate', # affects annotate command and adds fastannonate cmd
87 b'fastannotate', # affects annotate command and adds fastannonate cmd
88 b'merge-tools', # affects whether gui or not in extdiff's uisetup
88 b'merge-tools', # affects whether gui or not in extdiff's uisetup
89 b'schemes', # extsetup will update global hg.schemes
89 b'schemes', # extsetup will update global hg.schemes
90 ]
90 ]
91
91
92 _configsectionitems = [
92 _configsectionitems = [
93 (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup
93 (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup
94 ]
94 ]
95
95
96 # sensitive environment variables affecting confighash
96 # sensitive environment variables affecting confighash
97 _envre = re.compile(
97 _envre = re.compile(
98 br'''\A(?:
98 br'''\A(?:
99 CHGHG
99 CHGHG
100 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
100 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
101 |HG(?:ENCODING|PLAIN).*
101 |HG(?:ENCODING|PLAIN).*
102 |LANG(?:UAGE)?
102 |LANG(?:UAGE)?
103 |LC_.*
103 |LC_.*
104 |LD_.*
104 |LD_.*
105 |PATH
105 |PATH
106 |PYTHON.*
106 |PYTHON.*
107 |TERM(?:INFO)?
107 |TERM(?:INFO)?
108 |TZ
108 |TZ
109 )\Z''',
109 )\Z''',
110 re.X,
110 re.X,
111 )
111 )
112
112
113
113
114 def _confighash(ui):
114 def _confighash(ui):
115 """return a quick hash for detecting config/env changes
115 """return a quick hash for detecting config/env changes
116
116
117 confighash is the hash of sensitive config items and environment variables.
117 confighash is the hash of sensitive config items and environment variables.
118
118
119 for chgserver, it is designed that once confighash changes, the server is
119 for chgserver, it is designed that once confighash changes, the server is
120 not qualified to serve its client and should redirect the client to a new
120 not qualified to serve its client and should redirect the client to a new
121 server. different from mtimehash, confighash change will not mark the
121 server. different from mtimehash, confighash change will not mark the
122 server outdated and exit since the user can have different configs at the
122 server outdated and exit since the user can have different configs at the
123 same time.
123 same time.
124 """
124 """
125 sectionitems = []
125 sectionitems = []
126 for section in _configsections:
126 for section in _configsections:
127 sectionitems.append(ui.configitems(section))
127 sectionitems.append(ui.configitems(section))
128 for section, item in _configsectionitems:
128 for section, item in _configsectionitems:
129 sectionitems.append(ui.config(section, item))
129 sectionitems.append(ui.config(section, item))
130 sectionhash = _hashlist(sectionitems)
130 sectionhash = _hashlist(sectionitems)
131 # If $CHGHG is set, the change to $HG should not trigger a new chg server
131 # If $CHGHG is set, the change to $HG should not trigger a new chg server
132 if b'CHGHG' in encoding.environ:
132 if b'CHGHG' in encoding.environ:
133 ignored = {b'HG'}
133 ignored = {b'HG'}
134 else:
134 else:
135 ignored = set()
135 ignored = set()
136 envitems = [
136 envitems = [
137 (k, v)
137 (k, v)
138 for k, v in pycompat.iteritems(encoding.environ)
138 for k, v in pycompat.iteritems(encoding.environ)
139 if _envre.match(k) and k not in ignored
139 if _envre.match(k) and k not in ignored
140 ]
140 ]
141 envhash = _hashlist(sorted(envitems))
141 envhash = _hashlist(sorted(envitems))
142 return sectionhash[:6] + envhash[:6]
142 return sectionhash[:6] + envhash[:6]
143
143
144
144
145 def _getmtimepaths(ui):
145 def _getmtimepaths(ui):
146 """get a list of paths that should be checked to detect change
146 """get a list of paths that should be checked to detect change
147
147
148 The list will include:
148 The list will include:
149 - extensions (will not cover all files for complex extensions)
149 - extensions (will not cover all files for complex extensions)
150 - mercurial/__version__.py
150 - mercurial/__version__.py
151 - python binary
151 - python binary
152 """
152 """
153 modules = [m for n, m in extensions.extensions(ui)]
153 modules = [m for n, m in extensions.extensions(ui)]
154 try:
154 try:
155 from . import __version__
155 from . import __version__
156
156
157 modules.append(__version__)
157 modules.append(__version__)
158 except ImportError:
158 except ImportError:
159 pass
159 pass
160 files = []
160 files = []
161 if pycompat.sysexecutable:
161 if pycompat.sysexecutable:
162 files.append(pycompat.sysexecutable)
162 files.append(pycompat.sysexecutable)
163 for m in modules:
163 for m in modules:
164 try:
164 try:
165 files.append(pycompat.fsencode(inspect.getabsfile(m)))
165 files.append(pycompat.fsencode(inspect.getabsfile(m)))
166 except TypeError:
166 except TypeError:
167 pass
167 pass
168 return sorted(set(files))
168 return sorted(set(files))
169
169
170
170
171 def _mtimehash(paths):
171 def _mtimehash(paths):
172 """return a quick hash for detecting file changes
172 """return a quick hash for detecting file changes
173
173
174 mtimehash calls stat on given paths and calculate a hash based on size and
174 mtimehash calls stat on given paths and calculate a hash based on size and
175 mtime of each file. mtimehash does not read file content because reading is
175 mtime of each file. mtimehash does not read file content because reading is
176 expensive. therefore it's not 100% reliable for detecting content changes.
176 expensive. therefore it's not 100% reliable for detecting content changes.
177 it's possible to return different hashes for same file contents.
177 it's possible to return different hashes for same file contents.
178 it's also possible to return a same hash for different file contents for
178 it's also possible to return a same hash for different file contents for
179 some carefully crafted situation.
179 some carefully crafted situation.
180
180
181 for chgserver, it is designed that once mtimehash changes, the server is
181 for chgserver, it is designed that once mtimehash changes, the server is
182 considered outdated immediately and should no longer provide service.
182 considered outdated immediately and should no longer provide service.
183
183
184 mtimehash is not included in confighash because we only know the paths of
184 mtimehash is not included in confighash because we only know the paths of
185 extensions after importing them (there is imp.find_module but that faces
185 extensions after importing them (there is imp.find_module but that faces
186 race conditions). We need to calculate confighash without importing.
186 race conditions). We need to calculate confighash without importing.
187 """
187 """
188
188
189 def trystat(path):
189 def trystat(path):
190 try:
190 try:
191 st = os.stat(path)
191 st = os.stat(path)
192 return (st[stat.ST_MTIME], st.st_size)
192 return (st[stat.ST_MTIME], st.st_size)
193 except OSError:
193 except OSError:
194 # could be ENOENT, EPERM etc. not fatal in any case
194 # could be ENOENT, EPERM etc. not fatal in any case
195 pass
195 pass
196
196
197 return _hashlist(pycompat.maplist(trystat, paths))[:12]
197 return _hashlist(pycompat.maplist(trystat, paths))[:12]
198
198
199
199
200 class hashstate(object):
200 class hashstate(object):
201 """a structure storing confighash, mtimehash, paths used for mtimehash"""
201 """a structure storing confighash, mtimehash, paths used for mtimehash"""
202
202
203 def __init__(self, confighash, mtimehash, mtimepaths):
203 def __init__(self, confighash, mtimehash, mtimepaths):
204 self.confighash = confighash
204 self.confighash = confighash
205 self.mtimehash = mtimehash
205 self.mtimehash = mtimehash
206 self.mtimepaths = mtimepaths
206 self.mtimepaths = mtimepaths
207
207
208 @staticmethod
208 @staticmethod
209 def fromui(ui, mtimepaths=None):
209 def fromui(ui, mtimepaths=None):
210 if mtimepaths is None:
210 if mtimepaths is None:
211 mtimepaths = _getmtimepaths(ui)
211 mtimepaths = _getmtimepaths(ui)
212 confighash = _confighash(ui)
212 confighash = _confighash(ui)
213 mtimehash = _mtimehash(mtimepaths)
213 mtimehash = _mtimehash(mtimepaths)
214 ui.log(
214 ui.log(
215 b'cmdserver',
215 b'cmdserver',
216 b'confighash = %s mtimehash = %s\n',
216 b'confighash = %s mtimehash = %s\n',
217 confighash,
217 confighash,
218 mtimehash,
218 mtimehash,
219 )
219 )
220 return hashstate(confighash, mtimehash, mtimepaths)
220 return hashstate(confighash, mtimehash, mtimepaths)
221
221
222
222
223 def _newchgui(srcui, csystem, attachio):
223 def _newchgui(srcui, csystem, attachio):
224 class chgui(srcui.__class__):
224 class chgui(srcui.__class__):
225 def __init__(self, src=None):
225 def __init__(self, src=None):
226 super(chgui, self).__init__(src)
226 super(chgui, self).__init__(src)
227 if src:
227 if src:
228 self._csystem = getattr(src, '_csystem', csystem)
228 self._csystem = getattr(src, '_csystem', csystem)
229 else:
229 else:
230 self._csystem = csystem
230 self._csystem = csystem
231
231
232 def _runsystem(self, cmd, environ, cwd, out):
232 def _runsystem(self, cmd, environ, cwd, out):
233 # fallback to the original system method if
233 # fallback to the original system method if
234 # a. the output stream is not stdout (e.g. stderr, cStringIO),
234 # a. the output stream is not stdout (e.g. stderr, cStringIO),
235 # b. or stdout is redirected by protectfinout(),
235 # b. or stdout is redirected by protectfinout(),
236 # because the chg client is not aware of these situations and
236 # because the chg client is not aware of these situations and
237 # will behave differently (i.e. write to stdout).
237 # will behave differently (i.e. write to stdout).
238 if (
238 if (
239 out is not self.fout
239 out is not self.fout
240 or not util.safehasattr(self.fout, b'fileno')
240 or not util.safehasattr(self.fout, b'fileno')
241 or self.fout.fileno() != procutil.stdout.fileno()
241 or self.fout.fileno() != procutil.stdout.fileno()
242 or self._finoutredirected
242 or self._finoutredirected
243 ):
243 ):
244 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
244 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
245 self.flush()
245 self.flush()
246 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
246 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
247
247
248 def _runpager(self, cmd, env=None):
248 def _runpager(self, cmd, env=None):
249 self._csystem(
249 self._csystem(
250 cmd,
250 cmd,
251 procutil.shellenviron(env),
251 procutil.shellenviron(env),
252 type=b'pager',
252 type=b'pager',
253 cmdtable={b'attachio': attachio},
253 cmdtable={b'attachio': attachio},
254 )
254 )
255 return True
255 return True
256
256
257 return chgui(srcui)
257 return chgui(srcui)
258
258
259
259
260 def _loadnewui(srcui, args, cdebug):
260 def _loadnewui(srcui, args, cdebug):
261 from . import dispatch # avoid cycle
261 from . import dispatch # avoid cycle
262
262
263 newui = srcui.__class__.load()
263 newui = srcui.__class__.load()
264 for a in [b'fin', b'fout', b'ferr', b'environ']:
264 for a in [b'fin', b'fout', b'ferr', b'environ']:
265 setattr(newui, a, getattr(srcui, a))
265 setattr(newui, a, getattr(srcui, a))
266 if util.safehasattr(srcui, b'_csystem'):
266 if util.safehasattr(srcui, b'_csystem'):
267 newui._csystem = srcui._csystem
267 newui._csystem = srcui._csystem
268
268
269 # command line args
269 # command line args
270 options = dispatch._earlyparseopts(newui, args)
270 options = dispatch._earlyparseopts(newui, args)
271 dispatch._parseconfig(newui, options[b'config'])
271 dispatch._parseconfig(newui, options[b'config'])
272
272
273 # stolen from tortoisehg.util.copydynamicconfig()
273 # stolen from tortoisehg.util.copydynamicconfig()
274 for section, name, value in srcui.walkconfig():
274 for section, name, value in srcui.walkconfig():
275 source = srcui.configsource(section, name)
275 source = srcui.configsource(section, name)
276 if b':' in source or source == b'--config' or source.startswith(b'$'):
276 if b':' in source or source == b'--config' or source.startswith(b'$'):
277 # path:line or command line, or environ
277 # path:line or command line, or environ
278 continue
278 continue
279 newui.setconfig(section, name, value, source)
279 newui.setconfig(section, name, value, source)
280
280
281 # load wd and repo config, copied from dispatch.py
281 # load wd and repo config, copied from dispatch.py
282 cwd = options[b'cwd']
282 cwd = options[b'cwd']
283 cwd = cwd and os.path.realpath(cwd) or None
283 cwd = cwd and os.path.realpath(cwd) or None
284 rpath = options[b'repository']
284 rpath = options[b'repository']
285 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
285 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
286
286
287 extensions.populateui(newui)
287 extensions.populateui(newui)
288 commandserver.setuplogging(newui, fp=cdebug)
288 commandserver.setuplogging(newui, fp=cdebug)
289 if newui is not newlui:
289 if newui is not newlui:
290 extensions.populateui(newlui)
290 extensions.populateui(newlui)
291 commandserver.setuplogging(newlui, fp=cdebug)
291 commandserver.setuplogging(newlui, fp=cdebug)
292
292
293 return (newui, newlui)
293 return (newui, newlui)
294
294
295
295
296 class channeledsystem(object):
296 class channeledsystem(object):
297 """Propagate ui.system() request in the following format:
297 """Propagate ui.system() request in the following format:
298
298
299 payload length (unsigned int),
299 payload length (unsigned int),
300 type, '\0',
300 type, '\0',
301 cmd, '\0',
301 cmd, '\0',
302 cwd, '\0',
302 cwd, '\0',
303 envkey, '=', val, '\0',
303 envkey, '=', val, '\0',
304 ...
304 ...
305 envkey, '=', val
305 envkey, '=', val
306
306
307 if type == 'system', waits for:
307 if type == 'system', waits for:
308
308
309 exitcode length (unsigned int),
309 exitcode length (unsigned int),
310 exitcode (int)
310 exitcode (int)
311
311
312 if type == 'pager', repetitively waits for a command name ending with '\n'
312 if type == 'pager', repetitively waits for a command name ending with '\n'
313 and executes it defined by cmdtable, or exits the loop if the command name
313 and executes it defined by cmdtable, or exits the loop if the command name
314 is empty.
314 is empty.
315 """
315 """
316
316
317 def __init__(self, in_, out, channel):
317 def __init__(self, in_, out, channel):
318 self.in_ = in_
318 self.in_ = in_
319 self.out = out
319 self.out = out
320 self.channel = channel
320 self.channel = channel
321
321
322 def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
322 def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
323 args = [type, cmd, os.path.abspath(cwd or b'.')]
323 args = [type, cmd, os.path.abspath(cwd or b'.')]
324 args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
324 args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
325 data = b'\0'.join(args)
325 data = b'\0'.join(args)
326 self.out.write(struct.pack(b'>cI', self.channel, len(data)))
326 self.out.write(struct.pack(b'>cI', self.channel, len(data)))
327 self.out.write(data)
327 self.out.write(data)
328 self.out.flush()
328 self.out.flush()
329
329
330 if type == b'system':
330 if type == b'system':
331 length = self.in_.read(4)
331 length = self.in_.read(4)
332 (length,) = struct.unpack(b'>I', length)
332 (length,) = struct.unpack(b'>I', length)
333 if length != 4:
333 if length != 4:
334 raise error.Abort(_(b'invalid response'))
334 raise error.Abort(_(b'invalid response'))
335 (rc,) = struct.unpack(b'>i', self.in_.read(4))
335 (rc,) = struct.unpack(b'>i', self.in_.read(4))
336 return rc
336 return rc
337 elif type == b'pager':
337 elif type == b'pager':
338 while True:
338 while True:
339 cmd = self.in_.readline()[:-1]
339 cmd = self.in_.readline()[:-1]
340 if not cmd:
340 if not cmd:
341 break
341 break
342 if cmdtable and cmd in cmdtable:
342 if cmdtable and cmd in cmdtable:
343 cmdtable[cmd]()
343 cmdtable[cmd]()
344 else:
344 else:
345 raise error.Abort(_(b'unexpected command: %s') % cmd)
345 raise error.Abort(_(b'unexpected command: %s') % cmd)
346 else:
346 else:
347 raise error.ProgrammingError(b'invalid S channel type: %s' % type)
347 raise error.ProgrammingError(b'invalid S channel type: %s' % type)
348
348
349
349
350 _iochannels = [
350 _iochannels = [
351 # server.ch, ui.fp, mode
351 # server.ch, ui.fp, mode
352 (b'cin', b'fin', 'rb'),
352 (b'cin', b'fin', 'rb'),
353 (b'cout', b'fout', 'wb'),
353 (b'cout', b'fout', 'wb'),
354 (b'cerr', b'ferr', 'wb'),
354 (b'cerr', b'ferr', 'wb'),
355 ]
355 ]
356
356
357
357
358 class chgcmdserver(commandserver.server):
358 class chgcmdserver(commandserver.server):
359 def __init__(
359 def __init__(
360 self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
360 self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
361 ):
361 ):
362 super(chgcmdserver, self).__init__(
362 super(chgcmdserver, self).__init__(
363 _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
363 _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
364 repo,
364 repo,
365 fin,
365 fin,
366 fout,
366 fout,
367 prereposetups,
367 prereposetups,
368 )
368 )
369 self.clientsock = sock
369 self.clientsock = sock
370 self._ioattached = False
370 self._ioattached = False
371 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
371 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
372 self.hashstate = hashstate
372 self.hashstate = hashstate
373 self.baseaddress = baseaddress
373 self.baseaddress = baseaddress
374 if hashstate is not None:
374 if hashstate is not None:
375 self.capabilities = self.capabilities.copy()
375 self.capabilities = self.capabilities.copy()
376 self.capabilities[b'validate'] = chgcmdserver.validate
376 self.capabilities[b'validate'] = chgcmdserver.validate
377
377
378 def cleanup(self):
378 def cleanup(self):
379 super(chgcmdserver, self).cleanup()
379 super(chgcmdserver, self).cleanup()
380 # dispatch._runcatch() does not flush outputs if exception is not
380 # dispatch._runcatch() does not flush outputs if exception is not
381 # handled by dispatch._dispatch()
381 # handled by dispatch._dispatch()
382 self.ui.flush()
382 self.ui.flush()
383 self._restoreio()
383 self._restoreio()
384 self._ioattached = False
384 self._ioattached = False
385
385
386 def attachio(self):
386 def attachio(self):
387 """Attach to client's stdio passed via unix domain socket; all
387 """Attach to client's stdio passed via unix domain socket; all
388 channels except cresult will no longer be used
388 channels except cresult will no longer be used
389 """
389 """
390 # tell client to sendmsg() with 1-byte payload, which makes it
390 # tell client to sendmsg() with 1-byte payload, which makes it
391 # distinctive from "attachio\n" command consumed by client.read()
391 # distinctive from "attachio\n" command consumed by client.read()
392 self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
392 self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
393 clientfds = util.recvfds(self.clientsock.fileno())
393 clientfds = util.recvfds(self.clientsock.fileno())
394 self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
394 self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
395
395
396 ui = self.ui
396 ui = self.ui
397 ui.flush()
397 ui.flush()
398 self._saveio()
398 self._saveio()
399 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
399 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
400 assert fd > 0
400 assert fd > 0
401 fp = getattr(ui, fn)
401 fp = getattr(ui, fn)
402 os.dup2(fd, fp.fileno())
402 os.dup2(fd, fp.fileno())
403 os.close(fd)
403 os.close(fd)
404 if self._ioattached:
404 if self._ioattached:
405 continue
405 continue
406 # reset buffering mode when client is first attached. as we want
406 # reset buffering mode when client is first attached. as we want
407 # to see output immediately on pager, the mode stays unchanged
407 # to see output immediately on pager, the mode stays unchanged
408 # when client re-attached. ferr is unchanged because it should
408 # when client re-attached. ferr is unchanged because it should
409 # be unbuffered no matter if it is a tty or not.
409 # be unbuffered no matter if it is a tty or not.
410 if fn == b'ferr':
410 if fn == b'ferr':
411 newfp = fp
411 newfp = fp
412 elif pycompat.ispy3:
413 # On Python 3, the standard library doesn't offer line-buffered
414 # binary streams, so wrap/unwrap it.
415 if fp.isatty():
416 newfp = procutil.make_line_buffered(fp)
417 else:
418 newfp = procutil.unwrap_line_buffered(fp)
412 else:
419 else:
413 # make it line buffered explicitly because the default is
420 # Python 2 uses the I/O streams provided by the C library, so
414 # decided on first write(), where fout could be a pager.
421 # make it line-buffered explicitly. Otherwise the default would
422 # be decided on first write(), where fout could be a pager.
415 if fp.isatty():
423 if fp.isatty():
416 bufsize = 1 # line buffered
424 bufsize = 1 # line buffered
417 else:
425 else:
418 bufsize = -1 # system default
426 bufsize = -1 # system default
419 newfp = os.fdopen(fp.fileno(), mode, bufsize)
427 newfp = os.fdopen(fp.fileno(), mode, bufsize)
428 if newfp is not fp:
420 setattr(ui, fn, newfp)
429 setattr(ui, fn, newfp)
421 setattr(self, cn, newfp)
430 setattr(self, cn, newfp)
422
431
423 self._ioattached = True
432 self._ioattached = True
424 self.cresult.write(struct.pack(b'>i', len(clientfds)))
433 self.cresult.write(struct.pack(b'>i', len(clientfds)))
425
434
426 def _saveio(self):
435 def _saveio(self):
427 if self._oldios:
436 if self._oldios:
428 return
437 return
429 ui = self.ui
438 ui = self.ui
430 for cn, fn, _mode in _iochannels:
439 for cn, fn, _mode in _iochannels:
431 ch = getattr(self, cn)
440 ch = getattr(self, cn)
432 fp = getattr(ui, fn)
441 fp = getattr(ui, fn)
433 fd = os.dup(fp.fileno())
442 fd = os.dup(fp.fileno())
434 self._oldios.append((ch, fp, fd))
443 self._oldios.append((ch, fp, fd))
435
444
436 def _restoreio(self):
445 def _restoreio(self):
437 if not self._oldios:
446 if not self._oldios:
438 return
447 return
439 nullfd = os.open(os.devnull, os.O_WRONLY)
448 nullfd = os.open(os.devnull, os.O_WRONLY)
440 ui = self.ui
449 ui = self.ui
441 for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
450 for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
442 newfp = getattr(ui, fn)
451 newfp = getattr(ui, fn)
443 # close newfp while it's associated with client; otherwise it
452 # On Python 2, newfp and fp may be separate file objects associated
444 # would be closed when newfp is deleted
453 # with the same fd, so we must close newfp while it's associated
445 if newfp is not fp:
454 # with the client. Otherwise the new associated fd would be closed
455 # when newfp gets deleted. On Python 3, newfp is just a wrapper
456 # around fp even if newfp is not fp, so deleting newfp is safe.
457 if not (pycompat.ispy3 or newfp is fp):
446 newfp.close()
458 newfp.close()
447 # restore original fd: fp is open again
459 # restore original fd: fp is open again
448 try:
460 try:
449 if newfp is fp and 'w' in mode:
461 if (pycompat.ispy3 or newfp is fp) and 'w' in mode:
450 # Discard buffered data which couldn't be flushed because
462 # Discard buffered data which couldn't be flushed because
451 # of EPIPE. The data should belong to the current session
463 # of EPIPE. The data should belong to the current session
452 # and should never persist.
464 # and should never persist.
453 os.dup2(nullfd, fp.fileno())
465 os.dup2(nullfd, fp.fileno())
454 fp.flush()
466 fp.flush()
455 os.dup2(fd, fp.fileno())
467 os.dup2(fd, fp.fileno())
456 except OSError as err:
468 except OSError as err:
457 # According to issue6330, running chg on heavy loaded systems
469 # According to issue6330, running chg on heavy loaded systems
458 # can lead to EBUSY. [man dup2] indicates that, on Linux,
470 # can lead to EBUSY. [man dup2] indicates that, on Linux,
459 # EBUSY comes from a race condition between open() and dup2().
471 # EBUSY comes from a race condition between open() and dup2().
460 # However it's not clear why open() race occurred for
472 # However it's not clear why open() race occurred for
461 # newfd=stdin/out/err.
473 # newfd=stdin/out/err.
462 self.ui.log(
474 self.ui.log(
463 b'chgserver',
475 b'chgserver',
464 b'got %s while duplicating %s\n',
476 b'got %s while duplicating %s\n',
465 stringutil.forcebytestr(err),
477 stringutil.forcebytestr(err),
466 fn,
478 fn,
467 )
479 )
468 os.close(fd)
480 os.close(fd)
469 setattr(self, cn, ch)
481 setattr(self, cn, ch)
470 setattr(ui, fn, fp)
482 setattr(ui, fn, fp)
471 os.close(nullfd)
483 os.close(nullfd)
472 del self._oldios[:]
484 del self._oldios[:]
473
485
474 def validate(self):
486 def validate(self):
475 """Reload the config and check if the server is up to date
487 """Reload the config and check if the server is up to date
476
488
477 Read a list of '\0' separated arguments.
489 Read a list of '\0' separated arguments.
478 Write a non-empty list of '\0' separated instruction strings or '\0'
490 Write a non-empty list of '\0' separated instruction strings or '\0'
479 if the list is empty.
491 if the list is empty.
480 An instruction string could be either:
492 An instruction string could be either:
481 - "unlink $path", the client should unlink the path to stop the
493 - "unlink $path", the client should unlink the path to stop the
482 outdated server.
494 outdated server.
483 - "redirect $path", the client should attempt to connect to $path
495 - "redirect $path", the client should attempt to connect to $path
484 first. If it does not work, start a new server. It implies
496 first. If it does not work, start a new server. It implies
485 "reconnect".
497 "reconnect".
486 - "exit $n", the client should exit directly with code n.
498 - "exit $n", the client should exit directly with code n.
487 This may happen if we cannot parse the config.
499 This may happen if we cannot parse the config.
488 - "reconnect", the client should close the connection and
500 - "reconnect", the client should close the connection and
489 reconnect.
501 reconnect.
490 If neither "reconnect" nor "redirect" is included in the instruction
502 If neither "reconnect" nor "redirect" is included in the instruction
491 list, the client can continue with this server after completing all
503 list, the client can continue with this server after completing all
492 the instructions.
504 the instructions.
493 """
505 """
494 from . import dispatch # avoid cycle
506 from . import dispatch # avoid cycle
495
507
496 args = self._readlist()
508 args = self._readlist()
497 try:
509 try:
498 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
510 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
499 except error.ParseError as inst:
511 except error.ParseError as inst:
500 dispatch._formatparse(self.ui.warn, inst)
512 dispatch._formatparse(self.ui.warn, inst)
501 self.ui.flush()
513 self.ui.flush()
502 self.cresult.write(b'exit 255')
514 self.cresult.write(b'exit 255')
503 return
515 return
504 except error.Abort as inst:
516 except error.Abort as inst:
505 self.ui.error(_(b"abort: %s\n") % inst.message)
517 self.ui.error(_(b"abort: %s\n") % inst.message)
506 if inst.hint:
518 if inst.hint:
507 self.ui.error(_(b"(%s)\n") % inst.hint)
519 self.ui.error(_(b"(%s)\n") % inst.hint)
508 self.ui.flush()
520 self.ui.flush()
509 self.cresult.write(b'exit 255')
521 self.cresult.write(b'exit 255')
510 return
522 return
511 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
523 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
512 insts = []
524 insts = []
513 if newhash.mtimehash != self.hashstate.mtimehash:
525 if newhash.mtimehash != self.hashstate.mtimehash:
514 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
526 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
515 insts.append(b'unlink %s' % addr)
527 insts.append(b'unlink %s' % addr)
516 # mtimehash is empty if one or more extensions fail to load.
528 # mtimehash is empty if one or more extensions fail to load.
517 # to be compatible with hg, still serve the client this time.
529 # to be compatible with hg, still serve the client this time.
518 if self.hashstate.mtimehash:
530 if self.hashstate.mtimehash:
519 insts.append(b'reconnect')
531 insts.append(b'reconnect')
520 if newhash.confighash != self.hashstate.confighash:
532 if newhash.confighash != self.hashstate.confighash:
521 addr = _hashaddress(self.baseaddress, newhash.confighash)
533 addr = _hashaddress(self.baseaddress, newhash.confighash)
522 insts.append(b'redirect %s' % addr)
534 insts.append(b'redirect %s' % addr)
523 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
535 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
524 self.cresult.write(b'\0'.join(insts) or b'\0')
536 self.cresult.write(b'\0'.join(insts) or b'\0')
525
537
526 def chdir(self):
538 def chdir(self):
527 """Change current directory
539 """Change current directory
528
540
529 Note that the behavior of --cwd option is bit different from this.
541 Note that the behavior of --cwd option is bit different from this.
530 It does not affect --config parameter.
542 It does not affect --config parameter.
531 """
543 """
532 path = self._readstr()
544 path = self._readstr()
533 if not path:
545 if not path:
534 return
546 return
535 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
547 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
536 os.chdir(path)
548 os.chdir(path)
537
549
538 def setumask(self):
550 def setumask(self):
539 """Change umask (DEPRECATED)"""
551 """Change umask (DEPRECATED)"""
540 # BUG: this does not follow the message frame structure, but kept for
552 # BUG: this does not follow the message frame structure, but kept for
541 # backward compatibility with old chg clients for some time
553 # backward compatibility with old chg clients for some time
542 self._setumask(self._read(4))
554 self._setumask(self._read(4))
543
555
544 def setumask2(self):
556 def setumask2(self):
545 """Change umask"""
557 """Change umask"""
546 data = self._readstr()
558 data = self._readstr()
547 if len(data) != 4:
559 if len(data) != 4:
548 raise ValueError(b'invalid mask length in setumask2 request')
560 raise ValueError(b'invalid mask length in setumask2 request')
549 self._setumask(data)
561 self._setumask(data)
550
562
551 def _setumask(self, data):
563 def _setumask(self, data):
552 mask = struct.unpack(b'>I', data)[0]
564 mask = struct.unpack(b'>I', data)[0]
553 self.ui.log(b'chgserver', b'setumask %r\n', mask)
565 self.ui.log(b'chgserver', b'setumask %r\n', mask)
554 util.setumask(mask)
566 util.setumask(mask)
555
567
556 def runcommand(self):
568 def runcommand(self):
557 # pager may be attached within the runcommand session, which should
569 # pager may be attached within the runcommand session, which should
558 # be detached at the end of the session. otherwise the pager wouldn't
570 # be detached at the end of the session. otherwise the pager wouldn't
559 # receive EOF.
571 # receive EOF.
560 globaloldios = self._oldios
572 globaloldios = self._oldios
561 self._oldios = []
573 self._oldios = []
562 try:
574 try:
563 return super(chgcmdserver, self).runcommand()
575 return super(chgcmdserver, self).runcommand()
564 finally:
576 finally:
565 self._restoreio()
577 self._restoreio()
566 self._oldios = globaloldios
578 self._oldios = globaloldios
567
579
568 def setenv(self):
580 def setenv(self):
569 """Clear and update os.environ
581 """Clear and update os.environ
570
582
571 Note that not all variables can make an effect on the running process.
583 Note that not all variables can make an effect on the running process.
572 """
584 """
573 l = self._readlist()
585 l = self._readlist()
574 try:
586 try:
575 newenv = dict(s.split(b'=', 1) for s in l)
587 newenv = dict(s.split(b'=', 1) for s in l)
576 except ValueError:
588 except ValueError:
577 raise ValueError(b'unexpected value in setenv request')
589 raise ValueError(b'unexpected value in setenv request')
578 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
590 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
579
591
580 encoding.environ.clear()
592 encoding.environ.clear()
581 encoding.environ.update(newenv)
593 encoding.environ.update(newenv)
582
594
583 capabilities = commandserver.server.capabilities.copy()
595 capabilities = commandserver.server.capabilities.copy()
584 capabilities.update(
596 capabilities.update(
585 {
597 {
586 b'attachio': attachio,
598 b'attachio': attachio,
587 b'chdir': chdir,
599 b'chdir': chdir,
588 b'runcommand': runcommand,
600 b'runcommand': runcommand,
589 b'setenv': setenv,
601 b'setenv': setenv,
590 b'setumask': setumask,
602 b'setumask': setumask,
591 b'setumask2': setumask2,
603 b'setumask2': setumask2,
592 }
604 }
593 )
605 )
594
606
595 if util.safehasattr(procutil, b'setprocname'):
607 if util.safehasattr(procutil, b'setprocname'):
596
608
597 def setprocname(self):
609 def setprocname(self):
598 """Change process title"""
610 """Change process title"""
599 name = self._readstr()
611 name = self._readstr()
600 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
612 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
601 procutil.setprocname(name)
613 procutil.setprocname(name)
602
614
603 capabilities[b'setprocname'] = setprocname
615 capabilities[b'setprocname'] = setprocname
604
616
605
617
606 def _tempaddress(address):
618 def _tempaddress(address):
607 return b'%s.%d.tmp' % (address, os.getpid())
619 return b'%s.%d.tmp' % (address, os.getpid())
608
620
609
621
610 def _hashaddress(address, hashstr):
622 def _hashaddress(address, hashstr):
611 # if the basename of address contains '.', use only the left part. this
623 # if the basename of address contains '.', use only the left part. this
612 # makes it possible for the client to pass 'server.tmp$PID' and follow by
624 # makes it possible for the client to pass 'server.tmp$PID' and follow by
613 # an atomic rename to avoid locking when spawning new servers.
625 # an atomic rename to avoid locking when spawning new servers.
614 dirname, basename = os.path.split(address)
626 dirname, basename = os.path.split(address)
615 basename = basename.split(b'.', 1)[0]
627 basename = basename.split(b'.', 1)[0]
616 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
628 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
617
629
618
630
619 class chgunixservicehandler(object):
631 class chgunixservicehandler(object):
620 """Set of operations for chg services"""
632 """Set of operations for chg services"""
621
633
622 pollinterval = 1 # [sec]
634 pollinterval = 1 # [sec]
623
635
624 def __init__(self, ui):
636 def __init__(self, ui):
625 self.ui = ui
637 self.ui = ui
626 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
638 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
627 self._lastactive = time.time()
639 self._lastactive = time.time()
628
640
629 def bindsocket(self, sock, address):
641 def bindsocket(self, sock, address):
630 self._inithashstate(address)
642 self._inithashstate(address)
631 self._checkextensions()
643 self._checkextensions()
632 self._bind(sock)
644 self._bind(sock)
633 self._createsymlink()
645 self._createsymlink()
634 # no "listening at" message should be printed to simulate hg behavior
646 # no "listening at" message should be printed to simulate hg behavior
635
647
636 def _inithashstate(self, address):
648 def _inithashstate(self, address):
637 self._baseaddress = address
649 self._baseaddress = address
638 if self.ui.configbool(b'chgserver', b'skiphash'):
650 if self.ui.configbool(b'chgserver', b'skiphash'):
639 self._hashstate = None
651 self._hashstate = None
640 self._realaddress = address
652 self._realaddress = address
641 return
653 return
642 self._hashstate = hashstate.fromui(self.ui)
654 self._hashstate = hashstate.fromui(self.ui)
643 self._realaddress = _hashaddress(address, self._hashstate.confighash)
655 self._realaddress = _hashaddress(address, self._hashstate.confighash)
644
656
645 def _checkextensions(self):
657 def _checkextensions(self):
646 if not self._hashstate:
658 if not self._hashstate:
647 return
659 return
648 if extensions.notloaded():
660 if extensions.notloaded():
649 # one or more extensions failed to load. mtimehash becomes
661 # one or more extensions failed to load. mtimehash becomes
650 # meaningless because we do not know the paths of those extensions.
662 # meaningless because we do not know the paths of those extensions.
651 # set mtimehash to an illegal hash value to invalidate the server.
663 # set mtimehash to an illegal hash value to invalidate the server.
652 self._hashstate.mtimehash = b''
664 self._hashstate.mtimehash = b''
653
665
654 def _bind(self, sock):
666 def _bind(self, sock):
655 # use a unique temp address so we can stat the file and do ownership
667 # use a unique temp address so we can stat the file and do ownership
656 # check later
668 # check later
657 tempaddress = _tempaddress(self._realaddress)
669 tempaddress = _tempaddress(self._realaddress)
658 util.bindunixsocket(sock, tempaddress)
670 util.bindunixsocket(sock, tempaddress)
659 self._socketstat = os.stat(tempaddress)
671 self._socketstat = os.stat(tempaddress)
660 sock.listen(socket.SOMAXCONN)
672 sock.listen(socket.SOMAXCONN)
661 # rename will replace the old socket file if exists atomically. the
673 # rename will replace the old socket file if exists atomically. the
662 # old server will detect ownership change and exit.
674 # old server will detect ownership change and exit.
663 util.rename(tempaddress, self._realaddress)
675 util.rename(tempaddress, self._realaddress)
664
676
665 def _createsymlink(self):
677 def _createsymlink(self):
666 if self._baseaddress == self._realaddress:
678 if self._baseaddress == self._realaddress:
667 return
679 return
668 tempaddress = _tempaddress(self._baseaddress)
680 tempaddress = _tempaddress(self._baseaddress)
669 os.symlink(os.path.basename(self._realaddress), tempaddress)
681 os.symlink(os.path.basename(self._realaddress), tempaddress)
670 util.rename(tempaddress, self._baseaddress)
682 util.rename(tempaddress, self._baseaddress)
671
683
672 def _issocketowner(self):
684 def _issocketowner(self):
673 try:
685 try:
674 st = os.stat(self._realaddress)
686 st = os.stat(self._realaddress)
675 return (
687 return (
676 st.st_ino == self._socketstat.st_ino
688 st.st_ino == self._socketstat.st_ino
677 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
689 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
678 )
690 )
679 except OSError:
691 except OSError:
680 return False
692 return False
681
693
682 def unlinksocket(self, address):
694 def unlinksocket(self, address):
683 if not self._issocketowner():
695 if not self._issocketowner():
684 return
696 return
685 # it is possible to have a race condition here that we may
697 # it is possible to have a race condition here that we may
686 # remove another server's socket file. but that's okay
698 # remove another server's socket file. but that's okay
687 # since that server will detect and exit automatically and
699 # since that server will detect and exit automatically and
688 # the client will start a new server on demand.
700 # the client will start a new server on demand.
689 util.tryunlink(self._realaddress)
701 util.tryunlink(self._realaddress)
690
702
691 def shouldexit(self):
703 def shouldexit(self):
692 if not self._issocketowner():
704 if not self._issocketowner():
693 self.ui.log(
705 self.ui.log(
694 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
706 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
695 )
707 )
696 return True
708 return True
697 if time.time() - self._lastactive > self._idletimeout:
709 if time.time() - self._lastactive > self._idletimeout:
698 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
710 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
699 return True
711 return True
700 return False
712 return False
701
713
702 def newconnection(self):
714 def newconnection(self):
703 self._lastactive = time.time()
715 self._lastactive = time.time()
704
716
705 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
717 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
706 return chgcmdserver(
718 return chgcmdserver(
707 self.ui,
719 self.ui,
708 repo,
720 repo,
709 fin,
721 fin,
710 fout,
722 fout,
711 conn,
723 conn,
712 prereposetups,
724 prereposetups,
713 self._hashstate,
725 self._hashstate,
714 self._baseaddress,
726 self._baseaddress,
715 )
727 )
716
728
717
729
718 def chgunixservice(ui, repo, opts):
730 def chgunixservice(ui, repo, opts):
719 # CHGINTERNALMARK is set by chg client. It is an indication of things are
731 # CHGINTERNALMARK is set by chg client. It is an indication of things are
720 # started by chg so other code can do things accordingly, like disabling
732 # started by chg so other code can do things accordingly, like disabling
721 # demandimport or detecting chg client started by chg client. When executed
733 # demandimport or detecting chg client started by chg client. When executed
722 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
734 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
723 # environ cleaner.
735 # environ cleaner.
724 if b'CHGINTERNALMARK' in encoding.environ:
736 if b'CHGINTERNALMARK' in encoding.environ:
725 del encoding.environ[b'CHGINTERNALMARK']
737 del encoding.environ[b'CHGINTERNALMARK']
726 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
738 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
727 # it thinks the current value is "C". This breaks the hash computation and
739 # it thinks the current value is "C". This breaks the hash computation and
728 # causes chg to restart loop.
740 # causes chg to restart loop.
729 if b'CHGORIG_LC_CTYPE' in encoding.environ:
741 if b'CHGORIG_LC_CTYPE' in encoding.environ:
730 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
742 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
731 del encoding.environ[b'CHGORIG_LC_CTYPE']
743 del encoding.environ[b'CHGORIG_LC_CTYPE']
732 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
744 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
733 if b'LC_CTYPE' in encoding.environ:
745 if b'LC_CTYPE' in encoding.environ:
734 del encoding.environ[b'LC_CTYPE']
746 del encoding.environ[b'LC_CTYPE']
735 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
747 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
736
748
737 if repo:
749 if repo:
738 # one chgserver can serve multiple repos. drop repo information
750 # one chgserver can serve multiple repos. drop repo information
739 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
751 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
740 h = chgunixservicehandler(ui)
752 h = chgunixservicehandler(ui)
741 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
753 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,780 +1,787 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 errno
13 import errno
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 threading
19 import threading
20 import time
20 import time
21
21
22 from ..i18n import _
22 from ..i18n import _
23 from ..pycompat import (
23 from ..pycompat import (
24 getattr,
24 getattr,
25 open,
25 open,
26 )
26 )
27
27
28 from .. import (
28 from .. import (
29 encoding,
29 encoding,
30 error,
30 error,
31 policy,
31 policy,
32 pycompat,
32 pycompat,
33 )
33 )
34
34
35 # Import like this to keep import-checker happy
35 # Import like this to keep import-checker happy
36 from ..utils import resourceutil
36 from ..utils import resourceutil
37
37
38 osutil = policy.importmod('osutil')
38 osutil = policy.importmod('osutil')
39
39
40 if pycompat.iswindows:
40 if pycompat.iswindows:
41 from .. import windows as platform
41 from .. import windows as platform
42 else:
42 else:
43 from .. import posix as platform
43 from .. import posix as platform
44
44
45
45
46 def isatty(fp):
46 def isatty(fp):
47 try:
47 try:
48 return fp.isatty()
48 return fp.isatty()
49 except AttributeError:
49 except AttributeError:
50 return False
50 return False
51
51
52
52
53 class LineBufferedWrapper(object):
53 class LineBufferedWrapper(object):
54 def __init__(self, orig):
54 def __init__(self, orig):
55 self.orig = orig
55 self.orig = orig
56
56
57 def __getattr__(self, attr):
57 def __getattr__(self, attr):
58 return getattr(self.orig, attr)
58 return getattr(self.orig, attr)
59
59
60 def write(self, s):
60 def write(self, s):
61 orig = self.orig
61 orig = self.orig
62 res = orig.write(s)
62 res = orig.write(s)
63 if s.endswith(b'\n'):
63 if s.endswith(b'\n'):
64 orig.flush()
64 orig.flush()
65 return res
65 return res
66
66
67
67
68 io.BufferedIOBase.register(LineBufferedWrapper)
68 io.BufferedIOBase.register(LineBufferedWrapper)
69
69
70
70
71 def make_line_buffered(stream):
71 def make_line_buffered(stream):
72 if pycompat.ispy3 and not isinstance(stream, io.BufferedIOBase):
72 if pycompat.ispy3 and not isinstance(stream, io.BufferedIOBase):
73 # On Python 3, buffered streams can be expected to subclass
73 # On Python 3, buffered streams can be expected to subclass
74 # BufferedIOBase. This is definitively the case for the streams
74 # BufferedIOBase. This is definitively the case for the streams
75 # initialized by the interpreter. For unbuffered streams, we don't need
75 # initialized by the interpreter. For unbuffered streams, we don't need
76 # to emulate line buffering.
76 # to emulate line buffering.
77 return stream
77 return stream
78 if isinstance(stream, LineBufferedWrapper):
78 if isinstance(stream, LineBufferedWrapper):
79 return stream
79 return stream
80 return LineBufferedWrapper(stream)
80 return LineBufferedWrapper(stream)
81
81
82
82
83 def unwrap_line_buffered(stream):
84 if isinstance(stream, LineBufferedWrapper):
85 assert not isinstance(stream.orig, LineBufferedWrapper)
86 return stream.orig
87 return stream
88
89
83 class WriteAllWrapper(object):
90 class WriteAllWrapper(object):
84 def __init__(self, orig):
91 def __init__(self, orig):
85 self.orig = orig
92 self.orig = orig
86
93
87 def __getattr__(self, attr):
94 def __getattr__(self, attr):
88 return getattr(self.orig, attr)
95 return getattr(self.orig, attr)
89
96
90 def write(self, s):
97 def write(self, s):
91 write1 = self.orig.write
98 write1 = self.orig.write
92 m = memoryview(s)
99 m = memoryview(s)
93 total_to_write = len(s)
100 total_to_write = len(s)
94 total_written = 0
101 total_written = 0
95 while total_written < total_to_write:
102 while total_written < total_to_write:
96 total_written += write1(m[total_written:])
103 total_written += write1(m[total_written:])
97 return total_written
104 return total_written
98
105
99
106
100 io.IOBase.register(WriteAllWrapper)
107 io.IOBase.register(WriteAllWrapper)
101
108
102
109
103 def _make_write_all(stream):
110 def _make_write_all(stream):
104 assert pycompat.ispy3
111 assert pycompat.ispy3
105 if isinstance(stream, WriteAllWrapper):
112 if isinstance(stream, WriteAllWrapper):
106 return stream
113 return stream
107 if isinstance(stream, io.BufferedIOBase):
114 if isinstance(stream, io.BufferedIOBase):
108 # The io.BufferedIOBase.write() contract guarantees that all data is
115 # The io.BufferedIOBase.write() contract guarantees that all data is
109 # written.
116 # written.
110 return stream
117 return stream
111 # In general, the write() method of streams is free to write only part of
118 # In general, the write() method of streams is free to write only part of
112 # the data.
119 # the data.
113 return WriteAllWrapper(stream)
120 return WriteAllWrapper(stream)
114
121
115
122
116 if pycompat.ispy3:
123 if pycompat.ispy3:
117 # Python 3 implements its own I/O streams.
124 # Python 3 implements its own I/O streams.
118 # TODO: .buffer might not exist if std streams were replaced; we'll need
125 # TODO: .buffer might not exist if std streams were replaced; we'll need
119 # a silly wrapper to make a bytes stream backed by a unicode one.
126 # a silly wrapper to make a bytes stream backed by a unicode one.
120 stdin = sys.stdin.buffer
127 stdin = sys.stdin.buffer
121 stdout = _make_write_all(sys.stdout.buffer)
128 stdout = _make_write_all(sys.stdout.buffer)
122 stderr = _make_write_all(sys.stderr.buffer)
129 stderr = _make_write_all(sys.stderr.buffer)
123 if pycompat.iswindows:
130 if pycompat.iswindows:
124 # Work around Windows bugs.
131 # Work around Windows bugs.
125 stdout = platform.winstdout(stdout)
132 stdout = platform.winstdout(stdout)
126 stderr = platform.winstdout(stderr)
133 stderr = platform.winstdout(stderr)
127 if isatty(stdout):
134 if isatty(stdout):
128 # The standard library doesn't offer line-buffered binary streams.
135 # The standard library doesn't offer line-buffered binary streams.
129 stdout = make_line_buffered(stdout)
136 stdout = make_line_buffered(stdout)
130 else:
137 else:
131 # Python 2 uses the I/O streams provided by the C library.
138 # Python 2 uses the I/O streams provided by the C library.
132 stdin = sys.stdin
139 stdin = sys.stdin
133 stdout = sys.stdout
140 stdout = sys.stdout
134 stderr = sys.stderr
141 stderr = sys.stderr
135 if pycompat.iswindows:
142 if pycompat.iswindows:
136 # Work around Windows bugs.
143 # Work around Windows bugs.
137 stdout = platform.winstdout(stdout)
144 stdout = platform.winstdout(stdout)
138 stderr = platform.winstdout(stderr)
145 stderr = platform.winstdout(stderr)
139 if isatty(stdout):
146 if isatty(stdout):
140 if pycompat.iswindows:
147 if pycompat.iswindows:
141 # The Windows C runtime library doesn't support line buffering.
148 # The Windows C runtime library doesn't support line buffering.
142 stdout = make_line_buffered(stdout)
149 stdout = make_line_buffered(stdout)
143 else:
150 else:
144 # glibc determines buffering on first write to stdout - if we
151 # glibc determines buffering on first write to stdout - if we
145 # replace a TTY destined stdout with a pipe destined stdout (e.g.
152 # replace a TTY destined stdout with a pipe destined stdout (e.g.
146 # pager), we want line buffering.
153 # pager), we want line buffering.
147 stdout = os.fdopen(stdout.fileno(), 'wb', 1)
154 stdout = os.fdopen(stdout.fileno(), 'wb', 1)
148
155
149
156
150 findexe = platform.findexe
157 findexe = platform.findexe
151 _gethgcmd = platform.gethgcmd
158 _gethgcmd = platform.gethgcmd
152 getuser = platform.getuser
159 getuser = platform.getuser
153 getpid = os.getpid
160 getpid = os.getpid
154 hidewindow = platform.hidewindow
161 hidewindow = platform.hidewindow
155 readpipe = platform.readpipe
162 readpipe = platform.readpipe
156 setbinary = platform.setbinary
163 setbinary = platform.setbinary
157 setsignalhandler = platform.setsignalhandler
164 setsignalhandler = platform.setsignalhandler
158 shellquote = platform.shellquote
165 shellquote = platform.shellquote
159 shellsplit = platform.shellsplit
166 shellsplit = platform.shellsplit
160 spawndetached = platform.spawndetached
167 spawndetached = platform.spawndetached
161 sshargs = platform.sshargs
168 sshargs = platform.sshargs
162 testpid = platform.testpid
169 testpid = platform.testpid
163
170
164 try:
171 try:
165 setprocname = osutil.setprocname
172 setprocname = osutil.setprocname
166 except AttributeError:
173 except AttributeError:
167 pass
174 pass
168 try:
175 try:
169 unblocksignal = osutil.unblocksignal
176 unblocksignal = osutil.unblocksignal
170 except AttributeError:
177 except AttributeError:
171 pass
178 pass
172
179
173 closefds = pycompat.isposix
180 closefds = pycompat.isposix
174
181
175
182
176 def explainexit(code):
183 def explainexit(code):
177 """return a message describing a subprocess status
184 """return a message describing a subprocess status
178 (codes from kill are negative - not os.system/wait encoding)"""
185 (codes from kill are negative - not os.system/wait encoding)"""
179 if code >= 0:
186 if code >= 0:
180 return _(b"exited with status %d") % code
187 return _(b"exited with status %d") % code
181 return _(b"killed by signal %d") % -code
188 return _(b"killed by signal %d") % -code
182
189
183
190
184 class _pfile(object):
191 class _pfile(object):
185 """File-like wrapper for a stream opened by subprocess.Popen()"""
192 """File-like wrapper for a stream opened by subprocess.Popen()"""
186
193
187 def __init__(self, proc, fp):
194 def __init__(self, proc, fp):
188 self._proc = proc
195 self._proc = proc
189 self._fp = fp
196 self._fp = fp
190
197
191 def close(self):
198 def close(self):
192 # unlike os.popen(), this returns an integer in subprocess coding
199 # unlike os.popen(), this returns an integer in subprocess coding
193 self._fp.close()
200 self._fp.close()
194 return self._proc.wait()
201 return self._proc.wait()
195
202
196 def __iter__(self):
203 def __iter__(self):
197 return iter(self._fp)
204 return iter(self._fp)
198
205
199 def __getattr__(self, attr):
206 def __getattr__(self, attr):
200 return getattr(self._fp, attr)
207 return getattr(self._fp, attr)
201
208
202 def __enter__(self):
209 def __enter__(self):
203 return self
210 return self
204
211
205 def __exit__(self, exc_type, exc_value, exc_tb):
212 def __exit__(self, exc_type, exc_value, exc_tb):
206 self.close()
213 self.close()
207
214
208
215
209 def popen(cmd, mode=b'rb', bufsize=-1):
216 def popen(cmd, mode=b'rb', bufsize=-1):
210 if mode == b'rb':
217 if mode == b'rb':
211 return _popenreader(cmd, bufsize)
218 return _popenreader(cmd, bufsize)
212 elif mode == b'wb':
219 elif mode == b'wb':
213 return _popenwriter(cmd, bufsize)
220 return _popenwriter(cmd, bufsize)
214 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
221 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
215
222
216
223
217 def _popenreader(cmd, bufsize):
224 def _popenreader(cmd, bufsize):
218 p = subprocess.Popen(
225 p = subprocess.Popen(
219 tonativestr(cmd),
226 tonativestr(cmd),
220 shell=True,
227 shell=True,
221 bufsize=bufsize,
228 bufsize=bufsize,
222 close_fds=closefds,
229 close_fds=closefds,
223 stdout=subprocess.PIPE,
230 stdout=subprocess.PIPE,
224 )
231 )
225 return _pfile(p, p.stdout)
232 return _pfile(p, p.stdout)
226
233
227
234
228 def _popenwriter(cmd, bufsize):
235 def _popenwriter(cmd, bufsize):
229 p = subprocess.Popen(
236 p = subprocess.Popen(
230 tonativestr(cmd),
237 tonativestr(cmd),
231 shell=True,
238 shell=True,
232 bufsize=bufsize,
239 bufsize=bufsize,
233 close_fds=closefds,
240 close_fds=closefds,
234 stdin=subprocess.PIPE,
241 stdin=subprocess.PIPE,
235 )
242 )
236 return _pfile(p, p.stdin)
243 return _pfile(p, p.stdin)
237
244
238
245
239 def popen2(cmd, env=None):
246 def popen2(cmd, env=None):
240 # Setting bufsize to -1 lets the system decide the buffer size.
247 # Setting bufsize to -1 lets the system decide the buffer size.
241 # The default for bufsize is 0, meaning unbuffered. This leads to
248 # The default for bufsize is 0, meaning unbuffered. This leads to
242 # poor performance on Mac OS X: http://bugs.python.org/issue4194
249 # poor performance on Mac OS X: http://bugs.python.org/issue4194
243 p = subprocess.Popen(
250 p = subprocess.Popen(
244 tonativestr(cmd),
251 tonativestr(cmd),
245 shell=True,
252 shell=True,
246 bufsize=-1,
253 bufsize=-1,
247 close_fds=closefds,
254 close_fds=closefds,
248 stdin=subprocess.PIPE,
255 stdin=subprocess.PIPE,
249 stdout=subprocess.PIPE,
256 stdout=subprocess.PIPE,
250 env=tonativeenv(env),
257 env=tonativeenv(env),
251 )
258 )
252 return p.stdin, p.stdout
259 return p.stdin, p.stdout
253
260
254
261
255 def popen3(cmd, env=None):
262 def popen3(cmd, env=None):
256 stdin, stdout, stderr, p = popen4(cmd, env)
263 stdin, stdout, stderr, p = popen4(cmd, env)
257 return stdin, stdout, stderr
264 return stdin, stdout, stderr
258
265
259
266
260 def popen4(cmd, env=None, bufsize=-1):
267 def popen4(cmd, env=None, bufsize=-1):
261 p = subprocess.Popen(
268 p = subprocess.Popen(
262 tonativestr(cmd),
269 tonativestr(cmd),
263 shell=True,
270 shell=True,
264 bufsize=bufsize,
271 bufsize=bufsize,
265 close_fds=closefds,
272 close_fds=closefds,
266 stdin=subprocess.PIPE,
273 stdin=subprocess.PIPE,
267 stdout=subprocess.PIPE,
274 stdout=subprocess.PIPE,
268 stderr=subprocess.PIPE,
275 stderr=subprocess.PIPE,
269 env=tonativeenv(env),
276 env=tonativeenv(env),
270 )
277 )
271 return p.stdin, p.stdout, p.stderr, p
278 return p.stdin, p.stdout, p.stderr, p
272
279
273
280
274 def pipefilter(s, cmd):
281 def pipefilter(s, cmd):
275 '''filter string S through command CMD, returning its output'''
282 '''filter string S through command CMD, returning its output'''
276 p = subprocess.Popen(
283 p = subprocess.Popen(
277 tonativestr(cmd),
284 tonativestr(cmd),
278 shell=True,
285 shell=True,
279 close_fds=closefds,
286 close_fds=closefds,
280 stdin=subprocess.PIPE,
287 stdin=subprocess.PIPE,
281 stdout=subprocess.PIPE,
288 stdout=subprocess.PIPE,
282 )
289 )
283 pout, perr = p.communicate(s)
290 pout, perr = p.communicate(s)
284 return pout
291 return pout
285
292
286
293
287 def tempfilter(s, cmd):
294 def tempfilter(s, cmd):
288 '''filter string S through a pair of temporary files with CMD.
295 '''filter string S through a pair of temporary files with CMD.
289 CMD is used as a template to create the real command to be run,
296 CMD is used as a template to create the real command to be run,
290 with the strings INFILE and OUTFILE replaced by the real names of
297 with the strings INFILE and OUTFILE replaced by the real names of
291 the temporary files generated.'''
298 the temporary files generated.'''
292 inname, outname = None, None
299 inname, outname = None, None
293 try:
300 try:
294 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
301 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
295 fp = os.fdopen(infd, 'wb')
302 fp = os.fdopen(infd, 'wb')
296 fp.write(s)
303 fp.write(s)
297 fp.close()
304 fp.close()
298 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
305 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
299 os.close(outfd)
306 os.close(outfd)
300 cmd = cmd.replace(b'INFILE', inname)
307 cmd = cmd.replace(b'INFILE', inname)
301 cmd = cmd.replace(b'OUTFILE', outname)
308 cmd = cmd.replace(b'OUTFILE', outname)
302 code = system(cmd)
309 code = system(cmd)
303 if pycompat.sysplatform == b'OpenVMS' and code & 1:
310 if pycompat.sysplatform == b'OpenVMS' and code & 1:
304 code = 0
311 code = 0
305 if code:
312 if code:
306 raise error.Abort(
313 raise error.Abort(
307 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
314 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
308 )
315 )
309 with open(outname, b'rb') as fp:
316 with open(outname, b'rb') as fp:
310 return fp.read()
317 return fp.read()
311 finally:
318 finally:
312 try:
319 try:
313 if inname:
320 if inname:
314 os.unlink(inname)
321 os.unlink(inname)
315 except OSError:
322 except OSError:
316 pass
323 pass
317 try:
324 try:
318 if outname:
325 if outname:
319 os.unlink(outname)
326 os.unlink(outname)
320 except OSError:
327 except OSError:
321 pass
328 pass
322
329
323
330
324 _filtertable = {
331 _filtertable = {
325 b'tempfile:': tempfilter,
332 b'tempfile:': tempfilter,
326 b'pipe:': pipefilter,
333 b'pipe:': pipefilter,
327 }
334 }
328
335
329
336
330 def filter(s, cmd):
337 def filter(s, cmd):
331 """filter a string through a command that transforms its input to its
338 """filter a string through a command that transforms its input to its
332 output"""
339 output"""
333 for name, fn in pycompat.iteritems(_filtertable):
340 for name, fn in pycompat.iteritems(_filtertable):
334 if cmd.startswith(name):
341 if cmd.startswith(name):
335 return fn(s, cmd[len(name) :].lstrip())
342 return fn(s, cmd[len(name) :].lstrip())
336 return pipefilter(s, cmd)
343 return pipefilter(s, cmd)
337
344
338
345
339 _hgexecutable = None
346 _hgexecutable = None
340
347
341
348
342 def hgexecutable():
349 def hgexecutable():
343 """return location of the 'hg' executable.
350 """return location of the 'hg' executable.
344
351
345 Defaults to $HG or 'hg' in the search path.
352 Defaults to $HG or 'hg' in the search path.
346 """
353 """
347 if _hgexecutable is None:
354 if _hgexecutable is None:
348 hg = encoding.environ.get(b'HG')
355 hg = encoding.environ.get(b'HG')
349 mainmod = sys.modules['__main__']
356 mainmod = sys.modules['__main__']
350 if hg:
357 if hg:
351 _sethgexecutable(hg)
358 _sethgexecutable(hg)
352 elif resourceutil.mainfrozen():
359 elif resourceutil.mainfrozen():
353 if getattr(sys, 'frozen', None) == 'macosx_app':
360 if getattr(sys, 'frozen', None) == 'macosx_app':
354 # Env variable set by py2app
361 # Env variable set by py2app
355 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
362 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
356 else:
363 else:
357 _sethgexecutable(pycompat.sysexecutable)
364 _sethgexecutable(pycompat.sysexecutable)
358 elif (
365 elif (
359 not pycompat.iswindows
366 not pycompat.iswindows
360 and os.path.basename(getattr(mainmod, '__file__', '')) == 'hg'
367 and os.path.basename(getattr(mainmod, '__file__', '')) == 'hg'
361 ):
368 ):
362 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
369 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
363 else:
370 else:
364 _sethgexecutable(
371 _sethgexecutable(
365 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
372 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
366 )
373 )
367 return _hgexecutable
374 return _hgexecutable
368
375
369
376
370 def _sethgexecutable(path):
377 def _sethgexecutable(path):
371 """set location of the 'hg' executable"""
378 """set location of the 'hg' executable"""
372 global _hgexecutable
379 global _hgexecutable
373 _hgexecutable = path
380 _hgexecutable = path
374
381
375
382
376 def _testfileno(f, stdf):
383 def _testfileno(f, stdf):
377 fileno = getattr(f, 'fileno', None)
384 fileno = getattr(f, 'fileno', None)
378 try:
385 try:
379 return fileno and fileno() == stdf.fileno()
386 return fileno and fileno() == stdf.fileno()
380 except io.UnsupportedOperation:
387 except io.UnsupportedOperation:
381 return False # fileno() raised UnsupportedOperation
388 return False # fileno() raised UnsupportedOperation
382
389
383
390
384 def isstdin(f):
391 def isstdin(f):
385 return _testfileno(f, sys.__stdin__)
392 return _testfileno(f, sys.__stdin__)
386
393
387
394
388 def isstdout(f):
395 def isstdout(f):
389 return _testfileno(f, sys.__stdout__)
396 return _testfileno(f, sys.__stdout__)
390
397
391
398
392 def protectstdio(uin, uout):
399 def protectstdio(uin, uout):
393 """Duplicate streams and redirect original if (uin, uout) are stdio
400 """Duplicate streams and redirect original if (uin, uout) are stdio
394
401
395 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
402 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
396 redirected to stderr so the output is still readable.
403 redirected to stderr so the output is still readable.
397
404
398 Returns (fin, fout) which point to the original (uin, uout) fds, but
405 Returns (fin, fout) which point to the original (uin, uout) fds, but
399 may be copy of (uin, uout). The returned streams can be considered
406 may be copy of (uin, uout). The returned streams can be considered
400 "owned" in that print(), exec(), etc. never reach to them.
407 "owned" in that print(), exec(), etc. never reach to them.
401 """
408 """
402 uout.flush()
409 uout.flush()
403 fin, fout = uin, uout
410 fin, fout = uin, uout
404 if _testfileno(uin, stdin):
411 if _testfileno(uin, stdin):
405 newfd = os.dup(uin.fileno())
412 newfd = os.dup(uin.fileno())
406 nullfd = os.open(os.devnull, os.O_RDONLY)
413 nullfd = os.open(os.devnull, os.O_RDONLY)
407 os.dup2(nullfd, uin.fileno())
414 os.dup2(nullfd, uin.fileno())
408 os.close(nullfd)
415 os.close(nullfd)
409 fin = os.fdopen(newfd, 'rb')
416 fin = os.fdopen(newfd, 'rb')
410 if _testfileno(uout, stdout):
417 if _testfileno(uout, stdout):
411 newfd = os.dup(uout.fileno())
418 newfd = os.dup(uout.fileno())
412 os.dup2(stderr.fileno(), uout.fileno())
419 os.dup2(stderr.fileno(), uout.fileno())
413 fout = os.fdopen(newfd, 'wb')
420 fout = os.fdopen(newfd, 'wb')
414 return fin, fout
421 return fin, fout
415
422
416
423
417 def restorestdio(uin, uout, fin, fout):
424 def restorestdio(uin, uout, fin, fout):
418 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
425 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
419 uout.flush()
426 uout.flush()
420 for f, uif in [(fin, uin), (fout, uout)]:
427 for f, uif in [(fin, uin), (fout, uout)]:
421 if f is not uif:
428 if f is not uif:
422 os.dup2(f.fileno(), uif.fileno())
429 os.dup2(f.fileno(), uif.fileno())
423 f.close()
430 f.close()
424
431
425
432
426 def shellenviron(environ=None):
433 def shellenviron(environ=None):
427 """return environ with optional override, useful for shelling out"""
434 """return environ with optional override, useful for shelling out"""
428
435
429 def py2shell(val):
436 def py2shell(val):
430 """convert python object into string that is useful to shell"""
437 """convert python object into string that is useful to shell"""
431 if val is None or val is False:
438 if val is None or val is False:
432 return b'0'
439 return b'0'
433 if val is True:
440 if val is True:
434 return b'1'
441 return b'1'
435 return pycompat.bytestr(val)
442 return pycompat.bytestr(val)
436
443
437 env = dict(encoding.environ)
444 env = dict(encoding.environ)
438 if environ:
445 if environ:
439 env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
446 env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
440 env[b'HG'] = hgexecutable()
447 env[b'HG'] = hgexecutable()
441 return env
448 return env
442
449
443
450
444 if pycompat.iswindows:
451 if pycompat.iswindows:
445
452
446 def shelltonative(cmd, env):
453 def shelltonative(cmd, env):
447 return platform.shelltocmdexe( # pytype: disable=module-attr
454 return platform.shelltocmdexe( # pytype: disable=module-attr
448 cmd, shellenviron(env)
455 cmd, shellenviron(env)
449 )
456 )
450
457
451 tonativestr = encoding.strfromlocal
458 tonativestr = encoding.strfromlocal
452 else:
459 else:
453
460
454 def shelltonative(cmd, env):
461 def shelltonative(cmd, env):
455 return cmd
462 return cmd
456
463
457 tonativestr = pycompat.identity
464 tonativestr = pycompat.identity
458
465
459
466
460 def tonativeenv(env):
467 def tonativeenv(env):
461 '''convert the environment from bytes to strings suitable for Popen(), etc.
468 '''convert the environment from bytes to strings suitable for Popen(), etc.
462 '''
469 '''
463 return pycompat.rapply(tonativestr, env)
470 return pycompat.rapply(tonativestr, env)
464
471
465
472
466 def system(cmd, environ=None, cwd=None, out=None):
473 def system(cmd, environ=None, cwd=None, out=None):
467 '''enhanced shell command execution.
474 '''enhanced shell command execution.
468 run with environment maybe modified, maybe in different dir.
475 run with environment maybe modified, maybe in different dir.
469
476
470 if out is specified, it is assumed to be a file-like object that has a
477 if out is specified, it is assumed to be a file-like object that has a
471 write() method. stdout and stderr will be redirected to out.'''
478 write() method. stdout and stderr will be redirected to out.'''
472 try:
479 try:
473 stdout.flush()
480 stdout.flush()
474 except Exception:
481 except Exception:
475 pass
482 pass
476 env = shellenviron(environ)
483 env = shellenviron(environ)
477 if out is None or isstdout(out):
484 if out is None or isstdout(out):
478 rc = subprocess.call(
485 rc = subprocess.call(
479 tonativestr(cmd),
486 tonativestr(cmd),
480 shell=True,
487 shell=True,
481 close_fds=closefds,
488 close_fds=closefds,
482 env=tonativeenv(env),
489 env=tonativeenv(env),
483 cwd=pycompat.rapply(tonativestr, cwd),
490 cwd=pycompat.rapply(tonativestr, cwd),
484 )
491 )
485 else:
492 else:
486 proc = subprocess.Popen(
493 proc = subprocess.Popen(
487 tonativestr(cmd),
494 tonativestr(cmd),
488 shell=True,
495 shell=True,
489 close_fds=closefds,
496 close_fds=closefds,
490 env=tonativeenv(env),
497 env=tonativeenv(env),
491 cwd=pycompat.rapply(tonativestr, cwd),
498 cwd=pycompat.rapply(tonativestr, cwd),
492 stdout=subprocess.PIPE,
499 stdout=subprocess.PIPE,
493 stderr=subprocess.STDOUT,
500 stderr=subprocess.STDOUT,
494 )
501 )
495 for line in iter(proc.stdout.readline, b''):
502 for line in iter(proc.stdout.readline, b''):
496 out.write(line)
503 out.write(line)
497 proc.wait()
504 proc.wait()
498 rc = proc.returncode
505 rc = proc.returncode
499 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
506 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
500 rc = 0
507 rc = 0
501 return rc
508 return rc
502
509
503
510
504 _is_gui = None
511 _is_gui = None
505
512
506
513
507 def _gui():
514 def _gui():
508 '''Are we running in a GUI?'''
515 '''Are we running in a GUI?'''
509 if pycompat.isdarwin:
516 if pycompat.isdarwin:
510 if b'SSH_CONNECTION' in encoding.environ:
517 if b'SSH_CONNECTION' in encoding.environ:
511 # handle SSH access to a box where the user is logged in
518 # handle SSH access to a box where the user is logged in
512 return False
519 return False
513 elif getattr(osutil, 'isgui', None):
520 elif getattr(osutil, 'isgui', None):
514 # check if a CoreGraphics session is available
521 # check if a CoreGraphics session is available
515 return osutil.isgui()
522 return osutil.isgui()
516 else:
523 else:
517 # pure build; use a safe default
524 # pure build; use a safe default
518 return True
525 return True
519 else:
526 else:
520 return pycompat.iswindows or encoding.environ.get(b"DISPLAY")
527 return pycompat.iswindows or encoding.environ.get(b"DISPLAY")
521
528
522
529
523 def gui():
530 def gui():
524 global _is_gui
531 global _is_gui
525 if _is_gui is None:
532 if _is_gui is None:
526 _is_gui = _gui()
533 _is_gui = _gui()
527 return _is_gui
534 return _is_gui
528
535
529
536
530 def hgcmd():
537 def hgcmd():
531 """Return the command used to execute current hg
538 """Return the command used to execute current hg
532
539
533 This is different from hgexecutable() because on Windows we want
540 This is different from hgexecutable() because on Windows we want
534 to avoid things opening new shell windows like batch files, so we
541 to avoid things opening new shell windows like batch files, so we
535 get either the python call or current executable.
542 get either the python call or current executable.
536 """
543 """
537 if resourceutil.mainfrozen():
544 if resourceutil.mainfrozen():
538 if getattr(sys, 'frozen', None) == 'macosx_app':
545 if getattr(sys, 'frozen', None) == 'macosx_app':
539 # Env variable set by py2app
546 # Env variable set by py2app
540 return [encoding.environ[b'EXECUTABLEPATH']]
547 return [encoding.environ[b'EXECUTABLEPATH']]
541 else:
548 else:
542 return [pycompat.sysexecutable]
549 return [pycompat.sysexecutable]
543 return _gethgcmd()
550 return _gethgcmd()
544
551
545
552
546 def rundetached(args, condfn):
553 def rundetached(args, condfn):
547 """Execute the argument list in a detached process.
554 """Execute the argument list in a detached process.
548
555
549 condfn is a callable which is called repeatedly and should return
556 condfn is a callable which is called repeatedly and should return
550 True once the child process is known to have started successfully.
557 True once the child process is known to have started successfully.
551 At this point, the child process PID is returned. If the child
558 At this point, the child process PID is returned. If the child
552 process fails to start or finishes before condfn() evaluates to
559 process fails to start or finishes before condfn() evaluates to
553 True, return -1.
560 True, return -1.
554 """
561 """
555 # Windows case is easier because the child process is either
562 # Windows case is easier because the child process is either
556 # successfully starting and validating the condition or exiting
563 # successfully starting and validating the condition or exiting
557 # on failure. We just poll on its PID. On Unix, if the child
564 # on failure. We just poll on its PID. On Unix, if the child
558 # process fails to start, it will be left in a zombie state until
565 # process fails to start, it will be left in a zombie state until
559 # the parent wait on it, which we cannot do since we expect a long
566 # the parent wait on it, which we cannot do since we expect a long
560 # running process on success. Instead we listen for SIGCHLD telling
567 # running process on success. Instead we listen for SIGCHLD telling
561 # us our child process terminated.
568 # us our child process terminated.
562 terminated = set()
569 terminated = set()
563
570
564 def handler(signum, frame):
571 def handler(signum, frame):
565 terminated.add(os.wait())
572 terminated.add(os.wait())
566
573
567 prevhandler = None
574 prevhandler = None
568 SIGCHLD = getattr(signal, 'SIGCHLD', None)
575 SIGCHLD = getattr(signal, 'SIGCHLD', None)
569 if SIGCHLD is not None:
576 if SIGCHLD is not None:
570 prevhandler = signal.signal(SIGCHLD, handler)
577 prevhandler = signal.signal(SIGCHLD, handler)
571 try:
578 try:
572 pid = spawndetached(args)
579 pid = spawndetached(args)
573 while not condfn():
580 while not condfn():
574 if (pid in terminated or not testpid(pid)) and not condfn():
581 if (pid in terminated or not testpid(pid)) and not condfn():
575 return -1
582 return -1
576 time.sleep(0.1)
583 time.sleep(0.1)
577 return pid
584 return pid
578 finally:
585 finally:
579 if prevhandler is not None:
586 if prevhandler is not None:
580 signal.signal(signal.SIGCHLD, prevhandler)
587 signal.signal(signal.SIGCHLD, prevhandler)
581
588
582
589
583 @contextlib.contextmanager
590 @contextlib.contextmanager
584 def uninterruptible(warn):
591 def uninterruptible(warn):
585 """Inhibit SIGINT handling on a region of code.
592 """Inhibit SIGINT handling on a region of code.
586
593
587 Note that if this is called in a non-main thread, it turns into a no-op.
594 Note that if this is called in a non-main thread, it turns into a no-op.
588
595
589 Args:
596 Args:
590 warn: A callable which takes no arguments, and returns True if the
597 warn: A callable which takes no arguments, and returns True if the
591 previous signal handling should be restored.
598 previous signal handling should be restored.
592 """
599 """
593
600
594 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
601 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
595 shouldbail = []
602 shouldbail = []
596
603
597 def disabledsiginthandler(*args):
604 def disabledsiginthandler(*args):
598 if warn():
605 if warn():
599 signal.signal(signal.SIGINT, oldsiginthandler[0])
606 signal.signal(signal.SIGINT, oldsiginthandler[0])
600 del oldsiginthandler[0]
607 del oldsiginthandler[0]
601 shouldbail.append(True)
608 shouldbail.append(True)
602
609
603 try:
610 try:
604 try:
611 try:
605 signal.signal(signal.SIGINT, disabledsiginthandler)
612 signal.signal(signal.SIGINT, disabledsiginthandler)
606 except ValueError:
613 except ValueError:
607 # wrong thread, oh well, we tried
614 # wrong thread, oh well, we tried
608 del oldsiginthandler[0]
615 del oldsiginthandler[0]
609 yield
616 yield
610 finally:
617 finally:
611 if oldsiginthandler:
618 if oldsiginthandler:
612 signal.signal(signal.SIGINT, oldsiginthandler[0])
619 signal.signal(signal.SIGINT, oldsiginthandler[0])
613 if shouldbail:
620 if shouldbail:
614 raise KeyboardInterrupt
621 raise KeyboardInterrupt
615
622
616
623
617 if pycompat.iswindows:
624 if pycompat.iswindows:
618 # no fork on Windows, but we can create a detached process
625 # no fork on Windows, but we can create a detached process
619 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
626 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
620 # No stdlib constant exists for this value
627 # No stdlib constant exists for this value
621 DETACHED_PROCESS = 0x00000008
628 DETACHED_PROCESS = 0x00000008
622 # Following creation flags might create a console GUI window.
629 # Following creation flags might create a console GUI window.
623 # Using subprocess.CREATE_NEW_CONSOLE might helps.
630 # Using subprocess.CREATE_NEW_CONSOLE might helps.
624 # See https://phab.mercurial-scm.org/D1701 for discussion
631 # See https://phab.mercurial-scm.org/D1701 for discussion
625 _creationflags = (
632 _creationflags = (
626 DETACHED_PROCESS
633 DETACHED_PROCESS
627 | subprocess.CREATE_NEW_PROCESS_GROUP # pytype: disable=module-attr
634 | subprocess.CREATE_NEW_PROCESS_GROUP # pytype: disable=module-attr
628 )
635 )
629
636
630 def runbgcommand(
637 def runbgcommand(
631 script,
638 script,
632 env,
639 env,
633 shell=False,
640 shell=False,
634 stdout=None,
641 stdout=None,
635 stderr=None,
642 stderr=None,
636 ensurestart=True,
643 ensurestart=True,
637 record_wait=None,
644 record_wait=None,
638 stdin_bytes=None,
645 stdin_bytes=None,
639 ):
646 ):
640 '''Spawn a command without waiting for it to finish.'''
647 '''Spawn a command without waiting for it to finish.'''
641 # we can't use close_fds *and* redirect stdin. I'm not sure that we
648 # we can't use close_fds *and* redirect stdin. I'm not sure that we
642 # need to because the detached process has no console connection.
649 # need to because the detached process has no console connection.
643
650
644 try:
651 try:
645 stdin = None
652 stdin = None
646 if stdin_bytes is not None:
653 if stdin_bytes is not None:
647 stdin = pycompat.unnamedtempfile()
654 stdin = pycompat.unnamedtempfile()
648 stdin.write(stdin_bytes)
655 stdin.write(stdin_bytes)
649 stdin.flush()
656 stdin.flush()
650 stdin.seek(0)
657 stdin.seek(0)
651
658
652 p = subprocess.Popen(
659 p = subprocess.Popen(
653 tonativestr(script),
660 tonativestr(script),
654 shell=shell,
661 shell=shell,
655 env=tonativeenv(env),
662 env=tonativeenv(env),
656 close_fds=True,
663 close_fds=True,
657 creationflags=_creationflags,
664 creationflags=_creationflags,
658 stdin=stdin,
665 stdin=stdin,
659 stdout=stdout,
666 stdout=stdout,
660 stderr=stderr,
667 stderr=stderr,
661 )
668 )
662 if record_wait is not None:
669 if record_wait is not None:
663 record_wait(p.wait)
670 record_wait(p.wait)
664 finally:
671 finally:
665 if stdin is not None:
672 if stdin is not None:
666 stdin.close()
673 stdin.close()
667
674
668
675
669 else:
676 else:
670
677
671 def runbgcommand(
678 def runbgcommand(
672 cmd,
679 cmd,
673 env,
680 env,
674 shell=False,
681 shell=False,
675 stdout=None,
682 stdout=None,
676 stderr=None,
683 stderr=None,
677 ensurestart=True,
684 ensurestart=True,
678 record_wait=None,
685 record_wait=None,
679 stdin_bytes=None,
686 stdin_bytes=None,
680 ):
687 ):
681 '''Spawn a command without waiting for it to finish.
688 '''Spawn a command without waiting for it to finish.
682
689
683
690
684 When `record_wait` is not None, the spawned process will not be fully
691 When `record_wait` is not None, the spawned process will not be fully
685 detached and the `record_wait` argument will be called with a the
692 detached and the `record_wait` argument will be called with a the
686 `Subprocess.wait` function for the spawned process. This is mostly
693 `Subprocess.wait` function for the spawned process. This is mostly
687 useful for developers that need to make sure the spawned process
694 useful for developers that need to make sure the spawned process
688 finished before a certain point. (eg: writing test)'''
695 finished before a certain point. (eg: writing test)'''
689 if pycompat.isdarwin:
696 if pycompat.isdarwin:
690 # avoid crash in CoreFoundation in case another thread
697 # avoid crash in CoreFoundation in case another thread
691 # calls gui() while we're calling fork().
698 # calls gui() while we're calling fork().
692 gui()
699 gui()
693
700
694 # double-fork to completely detach from the parent process
701 # double-fork to completely detach from the parent process
695 # based on http://code.activestate.com/recipes/278731
702 # based on http://code.activestate.com/recipes/278731
696 if record_wait is None:
703 if record_wait is None:
697 pid = os.fork()
704 pid = os.fork()
698 if pid:
705 if pid:
699 if not ensurestart:
706 if not ensurestart:
700 # Even though we're not waiting on the child process,
707 # Even though we're not waiting on the child process,
701 # we still must call waitpid() on it at some point so
708 # we still must call waitpid() on it at some point so
702 # it's not a zombie/defunct. This is especially relevant for
709 # it's not a zombie/defunct. This is especially relevant for
703 # chg since the parent process won't die anytime soon.
710 # chg since the parent process won't die anytime soon.
704 # We use a thread to make the overhead tiny.
711 # We use a thread to make the overhead tiny.
705 def _do_wait():
712 def _do_wait():
706 os.waitpid(pid, 0)
713 os.waitpid(pid, 0)
707
714
708 t = threading.Thread(target=_do_wait)
715 t = threading.Thread(target=_do_wait)
709 t.daemon = True
716 t.daemon = True
710 t.start()
717 t.start()
711 return
718 return
712 # Parent process
719 # Parent process
713 (_pid, status) = os.waitpid(pid, 0)
720 (_pid, status) = os.waitpid(pid, 0)
714 if os.WIFEXITED(status):
721 if os.WIFEXITED(status):
715 returncode = os.WEXITSTATUS(status)
722 returncode = os.WEXITSTATUS(status)
716 else:
723 else:
717 returncode = -(os.WTERMSIG(status))
724 returncode = -(os.WTERMSIG(status))
718 if returncode != 0:
725 if returncode != 0:
719 # The child process's return code is 0 on success, an errno
726 # The child process's return code is 0 on success, an errno
720 # value on failure, or 255 if we don't have a valid errno
727 # value on failure, or 255 if we don't have a valid errno
721 # value.
728 # value.
722 #
729 #
723 # (It would be slightly nicer to return the full exception info
730 # (It would be slightly nicer to return the full exception info
724 # over a pipe as the subprocess module does. For now it
731 # over a pipe as the subprocess module does. For now it
725 # doesn't seem worth adding that complexity here, though.)
732 # doesn't seem worth adding that complexity here, though.)
726 if returncode == 255:
733 if returncode == 255:
727 returncode = errno.EINVAL
734 returncode = errno.EINVAL
728 raise OSError(
735 raise OSError(
729 returncode,
736 returncode,
730 b'error running %r: %s'
737 b'error running %r: %s'
731 % (cmd, os.strerror(returncode)),
738 % (cmd, os.strerror(returncode)),
732 )
739 )
733 return
740 return
734
741
735 returncode = 255
742 returncode = 255
736 try:
743 try:
737 if record_wait is None:
744 if record_wait is None:
738 # Start a new session
745 # Start a new session
739 os.setsid()
746 os.setsid()
740 # connect stdin to devnull to make sure the subprocess can't
747 # connect stdin to devnull to make sure the subprocess can't
741 # muck up that stream for mercurial.
748 # muck up that stream for mercurial.
742 if stdin_bytes is None:
749 if stdin_bytes is None:
743 stdin = open(os.devnull, b'r')
750 stdin = open(os.devnull, b'r')
744 else:
751 else:
745 stdin = pycompat.unnamedtempfile()
752 stdin = pycompat.unnamedtempfile()
746 stdin.write(stdin_bytes)
753 stdin.write(stdin_bytes)
747 stdin.flush()
754 stdin.flush()
748 stdin.seek(0)
755 stdin.seek(0)
749
756
750 if stdout is None:
757 if stdout is None:
751 stdout = open(os.devnull, b'w')
758 stdout = open(os.devnull, b'w')
752 if stderr is None:
759 if stderr is None:
753 stderr = open(os.devnull, b'w')
760 stderr = open(os.devnull, b'w')
754
761
755 p = subprocess.Popen(
762 p = subprocess.Popen(
756 cmd,
763 cmd,
757 shell=shell,
764 shell=shell,
758 env=env,
765 env=env,
759 close_fds=True,
766 close_fds=True,
760 stdin=stdin,
767 stdin=stdin,
761 stdout=stdout,
768 stdout=stdout,
762 stderr=stderr,
769 stderr=stderr,
763 )
770 )
764 if record_wait is not None:
771 if record_wait is not None:
765 record_wait(p.wait)
772 record_wait(p.wait)
766 returncode = 0
773 returncode = 0
767 except EnvironmentError as ex:
774 except EnvironmentError as ex:
768 returncode = ex.errno & 0xFF
775 returncode = ex.errno & 0xFF
769 if returncode == 0:
776 if returncode == 0:
770 # This shouldn't happen, but just in case make sure the
777 # This shouldn't happen, but just in case make sure the
771 # return code is never 0 here.
778 # return code is never 0 here.
772 returncode = 255
779 returncode = 255
773 except Exception:
780 except Exception:
774 returncode = 255
781 returncode = 255
775 finally:
782 finally:
776 # mission accomplished, this child needs to exit and not
783 # mission accomplished, this child needs to exit and not
777 # continue the hg process here.
784 # continue the hg process here.
778 stdin.close()
785 stdin.close()
779 if record_wait is None:
786 if record_wait is None:
780 os._exit(returncode)
787 os._exit(returncode)
General Comments 0
You need to be logged in to leave comments. Login now