##// END OF EJS Templates
merge with stable
Martin von Zweigbergk -
r45537:3fadbdc4 merge default
parent child Browse files
Show More
@@ -1,203 +1,212 b''
1 1 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 '''share a common history between several working directories
7 7
8 The share extension introduces a new command :hg:`share` to create a new
9 working directory. This is similar to :hg:`clone`, but doesn't involve
10 copying or linking the storage of the repository. This allows working on
11 different branches or changes in parallel without the associated cost in
12 terms of disk space.
13
14 Note: destructive operations or extensions like :hg:`rollback` should be
15 used with care as they can result in confusing problems.
16
8 17 Automatic Pooled Storage for Clones
9 18 -----------------------------------
10 19
11 20 When this extension is active, :hg:`clone` can be configured to
12 21 automatically share/pool storage across multiple clones. This
13 22 mode effectively converts :hg:`clone` to :hg:`clone` + :hg:`share`.
14 23 The benefit of using this mode is the automatic management of
15 24 store paths and intelligent pooling of related repositories.
16 25
17 26 The following ``share.`` config options influence this feature:
18 27
19 28 ``share.pool``
20 29 Filesystem path where shared repository data will be stored. When
21 30 defined, :hg:`clone` will automatically use shared repository
22 31 storage instead of creating a store inside each clone.
23 32
24 33 ``share.poolnaming``
25 34 How directory names in ``share.pool`` are constructed.
26 35
27 36 "identity" means the name is derived from the first changeset in the
28 37 repository. In this mode, different remotes share storage if their
29 38 root/initial changeset is identical. In this mode, the local shared
30 39 repository is an aggregate of all encountered remote repositories.
31 40
32 41 "remote" means the name is derived from the source repository's
33 42 path or URL. In this mode, storage is only shared if the path or URL
34 43 requested in the :hg:`clone` command matches exactly to a repository
35 44 that was cloned before.
36 45
37 46 The default naming mode is "identity".
38 47 '''
39 48
40 49 from __future__ import absolute_import
41 50
42 51 import errno
43 52 from mercurial.i18n import _
44 53 from mercurial import (
45 54 bookmarks,
46 55 commands,
47 56 error,
48 57 extensions,
49 58 hg,
50 59 registrar,
51 60 txnutil,
52 61 util,
53 62 )
54 63
55 64 cmdtable = {}
56 65 command = registrar.command(cmdtable)
57 66 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
58 67 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
59 68 # be specifying the version(s) of Mercurial they are tested with, or
60 69 # leave the attribute unspecified.
61 70 testedwith = b'ships-with-hg-core'
62 71
63 72
64 73 @command(
65 74 b'share',
66 75 [
67 76 (b'U', b'noupdate', None, _(b'do not create a working directory')),
68 77 (b'B', b'bookmarks', None, _(b'also share bookmarks')),
69 78 (b'', b'relative', None, _(b'point to source using a relative path'),),
70 79 ],
71 80 _(b'[-U] [-B] SOURCE [DEST]'),
72 81 helpcategory=command.CATEGORY_REPO_CREATION,
73 82 norepo=True,
74 83 )
75 84 def share(
76 85 ui, source, dest=None, noupdate=False, bookmarks=False, relative=False
77 86 ):
78 87 """create a new shared repository
79 88
80 89 Initialize a new repository and working directory that shares its
81 90 history (and optionally bookmarks) with another repository.
82 91
83 92 .. note::
84 93
85 94 using rollback or extensions that destroy/modify history (mq,
86 95 rebase, etc.) can cause considerable confusion with shared
87 96 clones. In particular, if two shared clones are both updated to
88 97 the same changeset, and one of them destroys that changeset
89 98 with rollback, the other clone will suddenly stop working: all
90 99 operations will fail with "abort: working directory has unknown
91 100 parent". The only known workaround is to use debugsetparents on
92 101 the broken clone to reset it to a changeset that still exists.
93 102 """
94 103
95 104 hg.share(
96 105 ui,
97 106 source,
98 107 dest=dest,
99 108 update=not noupdate,
100 109 bookmarks=bookmarks,
101 110 relative=relative,
102 111 )
103 112 return 0
104 113
105 114
106 115 @command(b'unshare', [], b'', helpcategory=command.CATEGORY_MAINTENANCE)
107 116 def unshare(ui, repo):
108 117 """convert a shared repository to a normal one
109 118
110 119 Copy the store data to the repo and remove the sharedpath data.
111 120 """
112 121
113 122 if not repo.shared():
114 123 raise error.Abort(_(b"this is not a shared repo"))
115 124
116 125 hg.unshare(ui, repo)
117 126
118 127
119 128 # Wrap clone command to pass auto share options.
120 129 def clone(orig, ui, source, *args, **opts):
121 130 pool = ui.config(b'share', b'pool')
122 131 if pool:
123 132 pool = util.expandpath(pool)
124 133
125 134 opts['shareopts'] = {
126 135 b'pool': pool,
127 136 b'mode': ui.config(b'share', b'poolnaming'),
128 137 }
129 138
130 139 return orig(ui, source, *args, **opts)
131 140
132 141
133 142 def extsetup(ui):
134 143 extensions.wrapfunction(bookmarks, b'_getbkfile', getbkfile)
135 144 extensions.wrapfunction(bookmarks.bmstore, b'_recordchange', recordchange)
136 145 extensions.wrapfunction(bookmarks.bmstore, b'_writerepo', writerepo)
137 146 extensions.wrapcommand(commands.table, b'clone', clone)
138 147
139 148
140 149 def _hassharedbookmarks(repo):
141 150 """Returns whether this repo has shared bookmarks"""
142 151 if bookmarks.bookmarksinstore(repo):
143 152 # Kind of a lie, but it means that we skip our custom reads and writes
144 153 # from/to the source repo.
145 154 return False
146 155 try:
147 156 shared = repo.vfs.read(b'shared').splitlines()
148 157 except IOError as inst:
149 158 if inst.errno != errno.ENOENT:
150 159 raise
151 160 return False
152 161 return hg.sharedbookmarks in shared
153 162
154 163
155 164 def getbkfile(orig, repo):
156 165 if _hassharedbookmarks(repo):
157 166 srcrepo = hg.sharedreposource(repo)
158 167 if srcrepo is not None:
159 168 # just orig(srcrepo) doesn't work as expected, because
160 169 # HG_PENDING refers repo.root.
161 170 try:
162 171 fp, pending = txnutil.trypending(
163 172 repo.root, repo.vfs, b'bookmarks'
164 173 )
165 174 if pending:
166 175 # only in this case, bookmark information in repo
167 176 # is up-to-date.
168 177 return fp
169 178 fp.close()
170 179 except IOError as inst:
171 180 if inst.errno != errno.ENOENT:
172 181 raise
173 182
174 183 # otherwise, we should read bookmarks from srcrepo,
175 184 # because .hg/bookmarks in srcrepo might be already
176 185 # changed via another sharing repo
177 186 repo = srcrepo
178 187
179 188 # TODO: Pending changes in repo are still invisible in
180 189 # srcrepo, because bookmarks.pending is written only into repo.
181 190 # See also https://www.mercurial-scm.org/wiki/SharedRepository
182 191 return orig(repo)
183 192
184 193
185 194 def recordchange(orig, self, tr):
186 195 # Continue with write to local bookmarks file as usual
187 196 orig(self, tr)
188 197
189 198 if _hassharedbookmarks(self._repo):
190 199 srcrepo = hg.sharedreposource(self._repo)
191 200 if srcrepo is not None:
192 201 category = b'share-bookmarks'
193 202 tr.addpostclose(category, lambda tr: self._writerepo(srcrepo))
194 203
195 204
196 205 def writerepo(orig, self, repo):
197 206 # First write local bookmarks file in case we ever unshare
198 207 orig(self, repo)
199 208
200 209 if _hassharedbookmarks(self._repo):
201 210 srcrepo = hg.sharedreposource(self._repo)
202 211 if srcrepo is not None:
203 212 orig(self, srcrepo)
@@ -1,701 +1,703 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 stderr = pycompat.stderr
41 41 stdin = pycompat.stdin
42 42 stdout = pycompat.stdout
43 43
44 44
45 45 def isatty(fp):
46 46 try:
47 47 return fp.isatty()
48 48 except AttributeError:
49 49 return False
50 50
51 51
52 52 if pycompat.ispy3:
53 53
54 54 class LineBufferedWrapper(object):
55 55 def __init__(self, orig):
56 56 self.orig = orig
57 57
58 58 def __getattr__(self, attr):
59 59 return getattr(self.orig, attr)
60 60
61 61 def write(self, s):
62 62 orig = self.orig
63 63 res = orig.write(s)
64 64 if s.endswith(b'\n'):
65 65 orig.flush()
66 66 return res
67 67
68 68 io.BufferedIOBase.register(LineBufferedWrapper)
69 69
70 70
71 71 # glibc determines buffering on first write to stdout - if we replace a TTY
72 72 # destined stdout with a pipe destined stdout (e.g. pager), we want line
73 73 # buffering (or unbuffered, on Windows)
74 74 if isatty(stdout):
75 75 if pycompat.iswindows:
76 76 # Windows doesn't support line buffering
77 77 stdout = os.fdopen(stdout.fileno(), 'wb', 0)
78 78 elif pycompat.ispy3:
79 79 # On Python 3, buffered binary streams can't be set line-buffered.
80 80 # Therefore we have a wrapper that implements line buffering.
81 81 if isinstance(stdout, io.BufferedIOBase) and not isinstance(
82 82 stdout, LineBufferedWrapper
83 83 ):
84 84 stdout = LineBufferedWrapper(stdout)
85 85 else:
86 86 stdout = os.fdopen(stdout.fileno(), 'wb', 1)
87 87
88 88 if pycompat.iswindows:
89 89 from .. import windows as platform
90 90
91 91 stdout = platform.winstdout(stdout)
92 92 else:
93 93 from .. import posix as platform
94 94
95 95 findexe = platform.findexe
96 96 _gethgcmd = platform.gethgcmd
97 97 getuser = platform.getuser
98 98 getpid = os.getpid
99 99 hidewindow = platform.hidewindow
100 100 readpipe = platform.readpipe
101 101 setbinary = platform.setbinary
102 102 setsignalhandler = platform.setsignalhandler
103 103 shellquote = platform.shellquote
104 104 shellsplit = platform.shellsplit
105 105 spawndetached = platform.spawndetached
106 106 sshargs = platform.sshargs
107 107 testpid = platform.testpid
108 108
109 109 try:
110 110 setprocname = osutil.setprocname
111 111 except AttributeError:
112 112 pass
113 113 try:
114 114 unblocksignal = osutil.unblocksignal
115 115 except AttributeError:
116 116 pass
117 117
118 118 closefds = pycompat.isposix
119 119
120 120
121 121 def explainexit(code):
122 122 """return a message describing a subprocess status
123 123 (codes from kill are negative - not os.system/wait encoding)"""
124 124 if code >= 0:
125 125 return _(b"exited with status %d") % code
126 126 return _(b"killed by signal %d") % -code
127 127
128 128
129 129 class _pfile(object):
130 130 """File-like wrapper for a stream opened by subprocess.Popen()"""
131 131
132 132 def __init__(self, proc, fp):
133 133 self._proc = proc
134 134 self._fp = fp
135 135
136 136 def close(self):
137 137 # unlike os.popen(), this returns an integer in subprocess coding
138 138 self._fp.close()
139 139 return self._proc.wait()
140 140
141 141 def __iter__(self):
142 142 return iter(self._fp)
143 143
144 144 def __getattr__(self, attr):
145 145 return getattr(self._fp, attr)
146 146
147 147 def __enter__(self):
148 148 return self
149 149
150 150 def __exit__(self, exc_type, exc_value, exc_tb):
151 151 self.close()
152 152
153 153
154 154 def popen(cmd, mode=b'rb', bufsize=-1):
155 155 if mode == b'rb':
156 156 return _popenreader(cmd, bufsize)
157 157 elif mode == b'wb':
158 158 return _popenwriter(cmd, bufsize)
159 159 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
160 160
161 161
162 162 def _popenreader(cmd, bufsize):
163 163 p = subprocess.Popen(
164 164 tonativestr(cmd),
165 165 shell=True,
166 166 bufsize=bufsize,
167 167 close_fds=closefds,
168 168 stdout=subprocess.PIPE,
169 169 )
170 170 return _pfile(p, p.stdout)
171 171
172 172
173 173 def _popenwriter(cmd, bufsize):
174 174 p = subprocess.Popen(
175 175 tonativestr(cmd),
176 176 shell=True,
177 177 bufsize=bufsize,
178 178 close_fds=closefds,
179 179 stdin=subprocess.PIPE,
180 180 )
181 181 return _pfile(p, p.stdin)
182 182
183 183
184 184 def popen2(cmd, env=None):
185 185 # Setting bufsize to -1 lets the system decide the buffer size.
186 186 # The default for bufsize is 0, meaning unbuffered. This leads to
187 187 # poor performance on Mac OS X: http://bugs.python.org/issue4194
188 188 p = subprocess.Popen(
189 189 tonativestr(cmd),
190 190 shell=True,
191 191 bufsize=-1,
192 192 close_fds=closefds,
193 193 stdin=subprocess.PIPE,
194 194 stdout=subprocess.PIPE,
195 195 env=tonativeenv(env),
196 196 )
197 197 return p.stdin, p.stdout
198 198
199 199
200 200 def popen3(cmd, env=None):
201 201 stdin, stdout, stderr, p = popen4(cmd, env)
202 202 return stdin, stdout, stderr
203 203
204 204
205 205 def popen4(cmd, env=None, bufsize=-1):
206 206 p = subprocess.Popen(
207 207 tonativestr(cmd),
208 208 shell=True,
209 209 bufsize=bufsize,
210 210 close_fds=closefds,
211 211 stdin=subprocess.PIPE,
212 212 stdout=subprocess.PIPE,
213 213 stderr=subprocess.PIPE,
214 214 env=tonativeenv(env),
215 215 )
216 216 return p.stdin, p.stdout, p.stderr, p
217 217
218 218
219 219 def pipefilter(s, cmd):
220 220 '''filter string S through command CMD, returning its output'''
221 221 p = subprocess.Popen(
222 222 tonativestr(cmd),
223 223 shell=True,
224 224 close_fds=closefds,
225 225 stdin=subprocess.PIPE,
226 226 stdout=subprocess.PIPE,
227 227 )
228 228 pout, perr = p.communicate(s)
229 229 return pout
230 230
231 231
232 232 def tempfilter(s, cmd):
233 233 '''filter string S through a pair of temporary files with CMD.
234 234 CMD is used as a template to create the real command to be run,
235 235 with the strings INFILE and OUTFILE replaced by the real names of
236 236 the temporary files generated.'''
237 237 inname, outname = None, None
238 238 try:
239 239 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
240 240 fp = os.fdopen(infd, 'wb')
241 241 fp.write(s)
242 242 fp.close()
243 243 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
244 244 os.close(outfd)
245 245 cmd = cmd.replace(b'INFILE', inname)
246 246 cmd = cmd.replace(b'OUTFILE', outname)
247 247 code = system(cmd)
248 248 if pycompat.sysplatform == b'OpenVMS' and code & 1:
249 249 code = 0
250 250 if code:
251 251 raise error.Abort(
252 252 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
253 253 )
254 254 with open(outname, b'rb') as fp:
255 255 return fp.read()
256 256 finally:
257 257 try:
258 258 if inname:
259 259 os.unlink(inname)
260 260 except OSError:
261 261 pass
262 262 try:
263 263 if outname:
264 264 os.unlink(outname)
265 265 except OSError:
266 266 pass
267 267
268 268
269 269 _filtertable = {
270 270 b'tempfile:': tempfilter,
271 271 b'pipe:': pipefilter,
272 272 }
273 273
274 274
275 275 def filter(s, cmd):
276 276 """filter a string through a command that transforms its input to its
277 277 output"""
278 278 for name, fn in pycompat.iteritems(_filtertable):
279 279 if cmd.startswith(name):
280 280 return fn(s, cmd[len(name) :].lstrip())
281 281 return pipefilter(s, cmd)
282 282
283 283
284 284 _hgexecutable = None
285 285
286 286
287 287 def hgexecutable():
288 288 """return location of the 'hg' executable.
289 289
290 290 Defaults to $HG or 'hg' in the search path.
291 291 """
292 292 if _hgexecutable is None:
293 293 hg = encoding.environ.get(b'HG')
294 294 mainmod = sys.modules['__main__']
295 295 if hg:
296 296 _sethgexecutable(hg)
297 297 elif resourceutil.mainfrozen():
298 298 if getattr(sys, 'frozen', None) == 'macosx_app':
299 299 # Env variable set by py2app
300 300 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
301 301 else:
302 302 _sethgexecutable(pycompat.sysexecutable)
303 303 elif (
304 304 not pycompat.iswindows
305 305 and os.path.basename(getattr(mainmod, '__file__', '')) == 'hg'
306 306 ):
307 307 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
308 308 else:
309 309 _sethgexecutable(
310 310 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
311 311 )
312 312 return _hgexecutable
313 313
314 314
315 315 def _sethgexecutable(path):
316 316 """set location of the 'hg' executable"""
317 317 global _hgexecutable
318 318 _hgexecutable = path
319 319
320 320
321 321 def _testfileno(f, stdf):
322 322 fileno = getattr(f, 'fileno', None)
323 323 try:
324 324 return fileno and fileno() == stdf.fileno()
325 325 except io.UnsupportedOperation:
326 326 return False # fileno() raised UnsupportedOperation
327 327
328 328
329 329 def isstdin(f):
330 330 return _testfileno(f, sys.__stdin__)
331 331
332 332
333 333 def isstdout(f):
334 334 return _testfileno(f, sys.__stdout__)
335 335
336 336
337 337 def protectstdio(uin, uout):
338 338 """Duplicate streams and redirect original if (uin, uout) are stdio
339 339
340 340 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
341 341 redirected to stderr so the output is still readable.
342 342
343 343 Returns (fin, fout) which point to the original (uin, uout) fds, but
344 344 may be copy of (uin, uout). The returned streams can be considered
345 345 "owned" in that print(), exec(), etc. never reach to them.
346 346 """
347 347 uout.flush()
348 348 fin, fout = uin, uout
349 349 if _testfileno(uin, stdin):
350 350 newfd = os.dup(uin.fileno())
351 351 nullfd = os.open(os.devnull, os.O_RDONLY)
352 352 os.dup2(nullfd, uin.fileno())
353 353 os.close(nullfd)
354 354 fin = os.fdopen(newfd, 'rb')
355 355 if _testfileno(uout, stdout):
356 356 newfd = os.dup(uout.fileno())
357 357 os.dup2(stderr.fileno(), uout.fileno())
358 358 fout = os.fdopen(newfd, 'wb')
359 359 return fin, fout
360 360
361 361
362 362 def restorestdio(uin, uout, fin, fout):
363 363 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
364 364 uout.flush()
365 365 for f, uif in [(fin, uin), (fout, uout)]:
366 366 if f is not uif:
367 367 os.dup2(f.fileno(), uif.fileno())
368 368 f.close()
369 369
370 370
371 371 def shellenviron(environ=None):
372 372 """return environ with optional override, useful for shelling out"""
373 373
374 374 def py2shell(val):
375 375 """convert python object into string that is useful to shell"""
376 376 if val is None or val is False:
377 377 return b'0'
378 378 if val is True:
379 379 return b'1'
380 380 return pycompat.bytestr(val)
381 381
382 382 env = dict(encoding.environ)
383 383 if environ:
384 384 env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
385 385 env[b'HG'] = hgexecutable()
386 386 return env
387 387
388 388
389 389 if pycompat.iswindows:
390 390
391 391 def shelltonative(cmd, env):
392 392 return platform.shelltocmdexe( # pytype: disable=module-attr
393 393 cmd, shellenviron(env)
394 394 )
395 395
396 396 tonativestr = encoding.strfromlocal
397 397 else:
398 398
399 399 def shelltonative(cmd, env):
400 400 return cmd
401 401
402 402 tonativestr = pycompat.identity
403 403
404 404
405 405 def tonativeenv(env):
406 406 '''convert the environment from bytes to strings suitable for Popen(), etc.
407 407 '''
408 408 return pycompat.rapply(tonativestr, env)
409 409
410 410
411 411 def system(cmd, environ=None, cwd=None, out=None):
412 412 '''enhanced shell command execution.
413 413 run with environment maybe modified, maybe in different dir.
414 414
415 415 if out is specified, it is assumed to be a file-like object that has a
416 416 write() method. stdout and stderr will be redirected to out.'''
417 417 try:
418 418 stdout.flush()
419 419 except Exception:
420 420 pass
421 421 env = shellenviron(environ)
422 422 if out is None or isstdout(out):
423 423 rc = subprocess.call(
424 424 tonativestr(cmd),
425 425 shell=True,
426 426 close_fds=closefds,
427 427 env=tonativeenv(env),
428 428 cwd=pycompat.rapply(tonativestr, cwd),
429 429 )
430 430 else:
431 431 proc = subprocess.Popen(
432 432 tonativestr(cmd),
433 433 shell=True,
434 434 close_fds=closefds,
435 435 env=tonativeenv(env),
436 436 cwd=pycompat.rapply(tonativestr, cwd),
437 437 stdout=subprocess.PIPE,
438 438 stderr=subprocess.STDOUT,
439 439 )
440 440 for line in iter(proc.stdout.readline, b''):
441 441 out.write(line)
442 442 proc.wait()
443 443 rc = proc.returncode
444 444 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
445 445 rc = 0
446 446 return rc
447 447
448 448
449 449 _is_gui = None
450 450
451 451
452 452 def _gui():
453 453 '''Are we running in a GUI?'''
454 454 if pycompat.isdarwin:
455 455 if b'SSH_CONNECTION' in encoding.environ:
456 456 # handle SSH access to a box where the user is logged in
457 457 return False
458 458 elif getattr(osutil, 'isgui', None):
459 459 # check if a CoreGraphics session is available
460 460 return osutil.isgui()
461 461 else:
462 462 # pure build; use a safe default
463 463 return True
464 464 else:
465 465 return pycompat.iswindows or encoding.environ.get(b"DISPLAY")
466 466
467 467
468 468 def gui():
469 469 global _is_gui
470 470 if _is_gui is None:
471 471 _is_gui = _gui()
472 472 return _is_gui
473 473
474 474
475 475 def hgcmd():
476 476 """Return the command used to execute current hg
477 477
478 478 This is different from hgexecutable() because on Windows we want
479 479 to avoid things opening new shell windows like batch files, so we
480 480 get either the python call or current executable.
481 481 """
482 482 if resourceutil.mainfrozen():
483 483 if getattr(sys, 'frozen', None) == 'macosx_app':
484 484 # Env variable set by py2app
485 485 return [encoding.environ[b'EXECUTABLEPATH']]
486 486 else:
487 487 return [pycompat.sysexecutable]
488 488 return _gethgcmd()
489 489
490 490
491 491 def rundetached(args, condfn):
492 492 """Execute the argument list in a detached process.
493 493
494 494 condfn is a callable which is called repeatedly and should return
495 495 True once the child process is known to have started successfully.
496 496 At this point, the child process PID is returned. If the child
497 497 process fails to start or finishes before condfn() evaluates to
498 498 True, return -1.
499 499 """
500 500 # Windows case is easier because the child process is either
501 501 # successfully starting and validating the condition or exiting
502 502 # on failure. We just poll on its PID. On Unix, if the child
503 503 # process fails to start, it will be left in a zombie state until
504 504 # the parent wait on it, which we cannot do since we expect a long
505 505 # running process on success. Instead we listen for SIGCHLD telling
506 506 # us our child process terminated.
507 507 terminated = set()
508 508
509 509 def handler(signum, frame):
510 510 terminated.add(os.wait())
511 511
512 512 prevhandler = None
513 513 SIGCHLD = getattr(signal, 'SIGCHLD', None)
514 514 if SIGCHLD is not None:
515 515 prevhandler = signal.signal(SIGCHLD, handler)
516 516 try:
517 517 pid = spawndetached(args)
518 518 while not condfn():
519 519 if (pid in terminated or not testpid(pid)) and not condfn():
520 520 return -1
521 521 time.sleep(0.1)
522 522 return pid
523 523 finally:
524 524 if prevhandler is not None:
525 525 signal.signal(signal.SIGCHLD, prevhandler)
526 526
527 527
528 528 @contextlib.contextmanager
529 529 def uninterruptible(warn):
530 530 """Inhibit SIGINT handling on a region of code.
531 531
532 532 Note that if this is called in a non-main thread, it turns into a no-op.
533 533
534 534 Args:
535 535 warn: A callable which takes no arguments, and returns True if the
536 536 previous signal handling should be restored.
537 537 """
538 538
539 539 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
540 540 shouldbail = []
541 541
542 542 def disabledsiginthandler(*args):
543 543 if warn():
544 544 signal.signal(signal.SIGINT, oldsiginthandler[0])
545 545 del oldsiginthandler[0]
546 546 shouldbail.append(True)
547 547
548 548 try:
549 549 try:
550 550 signal.signal(signal.SIGINT, disabledsiginthandler)
551 551 except ValueError:
552 552 # wrong thread, oh well, we tried
553 553 del oldsiginthandler[0]
554 554 yield
555 555 finally:
556 556 if oldsiginthandler:
557 557 signal.signal(signal.SIGINT, oldsiginthandler[0])
558 558 if shouldbail:
559 559 raise KeyboardInterrupt
560 560
561 561
562 562 if pycompat.iswindows:
563 563 # no fork on Windows, but we can create a detached process
564 564 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
565 565 # No stdlib constant exists for this value
566 566 DETACHED_PROCESS = 0x00000008
567 567 # Following creation flags might create a console GUI window.
568 568 # Using subprocess.CREATE_NEW_CONSOLE might helps.
569 569 # See https://phab.mercurial-scm.org/D1701 for discussion
570 570 _creationflags = (
571 571 DETACHED_PROCESS
572 572 | subprocess.CREATE_NEW_PROCESS_GROUP # pytype: disable=module-attr
573 573 )
574 574
575 575 def runbgcommand(
576 576 script,
577 577 env,
578 578 shell=False,
579 579 stdout=None,
580 580 stderr=None,
581 581 ensurestart=True,
582 582 record_wait=None,
583 583 ):
584 584 '''Spawn a command without waiting for it to finish.'''
585 585 # we can't use close_fds *and* redirect stdin. I'm not sure that we
586 586 # need to because the detached process has no console connection.
587 587 p = subprocess.Popen(
588 588 tonativestr(script),
589 589 shell=shell,
590 590 env=tonativeenv(env),
591 591 close_fds=True,
592 592 creationflags=_creationflags,
593 593 stdout=stdout,
594 594 stderr=stderr,
595 595 )
596 596 if record_wait is not None:
597 597 record_wait(p.wait)
598 598
599 599
600 600 else:
601 601
602 602 def runbgcommand(
603 603 cmd,
604 604 env,
605 605 shell=False,
606 606 stdout=None,
607 607 stderr=None,
608 608 ensurestart=True,
609 609 record_wait=None,
610 610 ):
611 611 '''Spawn a command without waiting for it to finish.
612 612
613 613
614 614 When `record_wait` is not None, the spawned process will not be fully
615 615 detached and the `record_wait` argument will be called with a the
616 616 `Subprocess.wait` function for the spawned process. This is mostly
617 617 useful for developers that need to make sure the spawned process
618 618 finished before a certain point. (eg: writing test)'''
619 619 if pycompat.isdarwin:
620 620 # avoid crash in CoreFoundation in case another thread
621 621 # calls gui() while we're calling fork().
622 622 gui()
623 623
624 624 # double-fork to completely detach from the parent process
625 625 # based on http://code.activestate.com/recipes/278731
626 626 if record_wait is None:
627 627 pid = os.fork()
628 628 if pid:
629 629 if not ensurestart:
630 630 # Even though we're not waiting on the child process,
631 631 # we still must call waitpid() on it at some point so
632 632 # it's not a zombie/defunct. This is especially relevant for
633 633 # chg since the parent process won't die anytime soon.
634 634 # We use a thread to make the overhead tiny.
635 635 def _do_wait():
636 636 os.waitpid(pid, 0)
637 637
638 threading.Thread(target=_do_wait, daemon=True).start()
638 t = threading.Thread(target=_do_wait)
639 t.daemon = True
640 t.start()
639 641 return
640 642 # Parent process
641 643 (_pid, status) = os.waitpid(pid, 0)
642 644 if os.WIFEXITED(status):
643 645 returncode = os.WEXITSTATUS(status)
644 646 else:
645 647 returncode = -(os.WTERMSIG(status))
646 648 if returncode != 0:
647 649 # The child process's return code is 0 on success, an errno
648 650 # value on failure, or 255 if we don't have a valid errno
649 651 # value.
650 652 #
651 653 # (It would be slightly nicer to return the full exception info
652 654 # over a pipe as the subprocess module does. For now it
653 655 # doesn't seem worth adding that complexity here, though.)
654 656 if returncode == 255:
655 657 returncode = errno.EINVAL
656 658 raise OSError(
657 659 returncode,
658 660 b'error running %r: %s'
659 661 % (cmd, os.strerror(returncode)),
660 662 )
661 663 return
662 664
663 665 returncode = 255
664 666 try:
665 667 if record_wait is None:
666 668 # Start a new session
667 669 os.setsid()
668 670
669 671 stdin = open(os.devnull, b'r')
670 672 if stdout is None:
671 673 stdout = open(os.devnull, b'w')
672 674 if stderr is None:
673 675 stderr = open(os.devnull, b'w')
674 676
675 677 # connect stdin to devnull to make sure the subprocess can't
676 678 # muck up that stream for mercurial.
677 679 p = subprocess.Popen(
678 680 cmd,
679 681 shell=shell,
680 682 env=env,
681 683 close_fds=True,
682 684 stdin=stdin,
683 685 stdout=stdout,
684 686 stderr=stderr,
685 687 )
686 688 if record_wait is not None:
687 689 record_wait(p.wait)
688 690 returncode = 0
689 691 except EnvironmentError as ex:
690 692 returncode = ex.errno & 0xFF
691 693 if returncode == 0:
692 694 # This shouldn't happen, but just in case make sure the
693 695 # return code is never 0 here.
694 696 returncode = 255
695 697 except Exception:
696 698 returncode = 255
697 699 finally:
698 700 # mission accomplished, this child needs to exit and not
699 701 # continue the hg process here.
700 702 if record_wait is None:
701 703 os._exit(returncode)
General Comments 0
You need to be logged in to leave comments. Login now