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