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