blob: 8fe0c5ed83c21e35edf14ce7aca054fa59d819df [file] [log] [blame]
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org06617272010-11-04 13:50:50 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""Generic utils."""
6
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00007import codecs
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00008import contextlib
hinoka@google.com267f33e2014-02-28 22:02:32 +00009import cStringIO
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000010import datetime
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000011import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000012import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000013import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000014import platform
maruel@chromium.org3742c842010-09-09 19:27:14 +000015import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000016import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000017import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000018import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000019import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000020import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000021import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000022import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000023import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000025import subprocess2
26
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000028RETRY_MAX = 3
29RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000030START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000031
32
borenet@google.com6a9b1682014-03-24 18:35:23 +000033_WARNINGS = []
34
35
szager@chromium.orgff113292014-03-25 06:02:08 +000036# These repos are known to cause OOM errors on 32-bit platforms, due the the
37# very large objects they contain. It is not safe to use threaded index-pack
38# when cloning/fetching them.
39THREADED_INDEX_PACK_BLACKLIST = [
40 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
41]
42
43
maruel@chromium.org66c83e62010-09-07 14:18:45 +000044class Error(Exception):
45 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000046 def __init__(self, msg, *args, **kwargs):
47 index = getattr(threading.currentThread(), 'index', 0)
48 if index:
49 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
50 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000051
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000052
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000053def Elapsed(until=None):
54 if until is None:
55 until = datetime.datetime.now()
56 return str(until - START).partition('.')[0]
57
58
borenet@google.com6a9b1682014-03-24 18:35:23 +000059def PrintWarnings():
60 """Prints any accumulated warnings."""
61 if _WARNINGS:
62 print >> sys.stderr, '\n\nWarnings:'
63 for warning in _WARNINGS:
64 print >> sys.stderr, warning
65
66
67def AddWarning(msg):
68 """Adds the given warning message to the list of accumulated warnings."""
69 _WARNINGS.append(msg)
70
71
msb@chromium.orgac915bb2009-11-13 17:03:01 +000072def SplitUrlRevision(url):
73 """Splits url and returns a two-tuple: url, rev"""
74 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000075 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000076 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000077 components = re.search(regex, url).groups()
78 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +000079 components = url.rsplit('@', 1)
80 if re.match(r'^\w+\@', url) and '@' not in components[0]:
81 components = [url]
82
msb@chromium.orgac915bb2009-11-13 17:03:01 +000083 if len(components) == 1:
84 components += [None]
85 return tuple(components)
86
87
primiano@chromium.org5439ea52014-08-06 17:18:18 +000088def IsGitSha(revision):
89 """Returns true if the given string is a valid hex-encoded sha"""
90 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
91
92
floitsch@google.comeaab7842011-04-28 09:07:58 +000093def IsDateRevision(revision):
94 """Returns true if the given revision is of the form "{ ... }"."""
95 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
96
97
98def MakeDateRevision(date):
99 """Returns a revision representing the latest revision before the given
100 date."""
101 return "{" + date + "}"
102
103
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000104def SyntaxErrorToError(filename, e):
105 """Raises a gclient_utils.Error exception with the human readable message"""
106 try:
107 # Try to construct a human readable error message
108 if filename:
109 error_message = 'There is a syntax error in %s\n' % filename
110 else:
111 error_message = 'There is a syntax error\n'
112 error_message += 'Line #%s, character %s: "%s"' % (
113 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
114 except:
115 # Something went wrong, re-raise the original exception
116 raise e
117 else:
118 raise Error(error_message)
119
120
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121class PrintableObject(object):
122 def __str__(self):
123 output = ''
124 for i in dir(self):
125 if i.startswith('__'):
126 continue
127 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
128 return output
129
130
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000131def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000132 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000133 # codecs.open() has different behavior than open() on python 2.6 so use
134 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000135 s = f.read()
136 try:
137 return s.decode('utf-8')
138 except UnicodeDecodeError:
139 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000140
141
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000142def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000143 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000144 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000145
146
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000147@contextlib.contextmanager
148def temporary_directory(**kwargs):
149 tdir = tempfile.mkdtemp(**kwargs)
150 try:
151 yield tdir
152 finally:
153 if tdir:
154 rmtree(tdir)
155
156
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000157def safe_rename(old, new):
158 """Renames a file reliably.
159
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000160 Sometimes os.rename does not work because a dying git process keeps a handle
161 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000162 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000163 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000164 """
165 # roughly 10s
166 retries = 100
167 for i in range(retries):
168 try:
169 os.rename(old, new)
170 break
171 except OSError:
172 if i == (retries - 1):
173 # Give up.
174 raise
175 # retry
176 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
177 time.sleep(0.1)
178
179
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000180def rm_file_or_tree(path):
181 if os.path.isfile(path):
182 os.remove(path)
183 else:
184 rmtree(path)
185
186
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000187def rmtree(path):
188 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000189
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000190 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000191
192 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700193 are read-only. We need to be able to force the files to be writable (i.e.,
194 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000195
196 Even with all this, Windows still sometimes fails to delete a file, citing
197 a permission error (maybe something to do with antivirus scans or disk
198 indexing). The best suggestion any of the user forums had was to wait a
199 bit and try again, so we do that too. It's hand-waving, but sometimes it
200 works. :/
201
202 On POSIX systems, things are a little bit simpler. The modes of the files
203 to be deleted doesn't matter, only the modes of the directories containing
204 them are significant. As the directory tree is traversed, each directory
205 has its mode set appropriately before descending into it. This should
206 result in the entire tree being removed, with the possible exception of
207 *path itself, because nothing attempts to change the mode of its parent.
208 Doing so would be hazardous, as it's not a directory slated for removal.
209 In the ordinary case, this is not a problem: for our purposes, the user
210 will never lack write permission on *path's parent.
211 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000212 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000213 return
214
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000215 if os.path.islink(path) or not os.path.isdir(path):
216 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000217
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000218 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000219 # Give up and use cmd.exe's rd command.
220 path = os.path.normcase(path)
221 for _ in xrange(3):
222 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
223 if exitcode == 0:
224 return
225 else:
226 print >> sys.stderr, 'rd exited with code %d' % exitcode
227 time.sleep(3)
228 raise Exception('Failed to remove path %s' % path)
229
230 # On POSIX systems, we need the x-bit set on the directory to access it,
231 # the r-bit to see its contents, and the w-bit to remove files from it.
232 # The actual modes of the files within the directory is irrelevant.
233 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000234
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000235 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000236 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000237
238 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000239 # If fullpath is a symbolic link that points to a directory, isdir will
240 # be True, but we don't want to descend into that as a directory, we just
241 # want to remove the link. Check islink and treat links as ordinary files
242 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000243 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000244 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000245 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000246 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000247 # Recurse.
248 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000249
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000250 remove(os.rmdir, path)
251
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000252
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000253def safe_makedirs(tree):
254 """Creates the directory in a safe manner.
255
256 Because multiple threads can create these directories concurently, trap the
257 exception and pass on.
258 """
259 count = 0
260 while not os.path.exists(tree):
261 count += 1
262 try:
263 os.makedirs(tree)
264 except OSError, e:
265 # 17 POSIX, 183 Windows
266 if e.errno not in (17, 183):
267 raise
268 if count > 40:
269 # Give up.
270 raise
271
272
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000273def CommandToStr(args):
274 """Converts an arg list into a shell escaped string."""
275 return ' '.join(pipes.quote(arg) for arg in args)
276
277
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000278def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000279 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000280
maruel@chromium.org17d01792010-09-01 18:07:10 +0000281 If |always| is True, a message indicating what is being done
282 is printed to stdout all the time even if not output is generated. Otherwise
283 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000284 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000285 stdout = kwargs.setdefault('stdout', sys.stdout)
286 if header is None:
287 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000288 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000289
maruel@chromium.org17d01792010-09-01 18:07:10 +0000290 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000291 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000292 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000293 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000294 def filter_msg(line):
295 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000296 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000297 elif filter_fn:
298 filter_fn(line)
299 kwargs['filter_fn'] = filter_msg
300 kwargs['call_filter_on_first_line'] = True
301 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000302 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000303 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000304
maruel@chromium.org17d01792010-09-01 18:07:10 +0000305
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000306class Wrapper(object):
307 """Wraps an object, acting as a transparent proxy for all properties by
308 default.
309 """
310 def __init__(self, wrapped):
311 self._wrapped = wrapped
312
313 def __getattr__(self, name):
314 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000315
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000316
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000317class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000318 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000319 def __init__(self, wrapped, delay):
320 super(AutoFlush, self).__init__(wrapped)
321 if not hasattr(self, 'lock'):
322 self.lock = threading.Lock()
323 self.__last_flushed_at = time.time()
324 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000325
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000326 @property
327 def autoflush(self):
328 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000329
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000330 def write(self, out, *args, **kwargs):
331 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000332 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000333 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000334 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000335 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000336 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000337 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000338 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000339 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000340 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000341 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000342
343
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000345 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000346 threads with a NN> prefix.
347 """
348 def __init__(self, wrapped, include_zero=False):
349 super(Annotated, self).__init__(wrapped)
350 if not hasattr(self, 'lock'):
351 self.lock = threading.Lock()
352 self.__output_buffers = {}
353 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000354
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000355 @property
356 def annotated(self):
357 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000358
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000359 def write(self, out):
360 index = getattr(threading.currentThread(), 'index', 0)
361 if not index and not self.__include_zero:
362 # Unindexed threads aren't buffered.
363 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000364
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000365 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000366 try:
367 # Use a dummy array to hold the string so the code can be lockless.
368 # Strings are immutable, requiring to keep a lock for the whole dictionary
369 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000370 if not index in self.__output_buffers:
371 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000372 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000373 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000374 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000375 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000376
377 # Continue lockless.
378 obj[0] += out
379 while '\n' in obj[0]:
380 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000381 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000383 obj[0] = remaining
384
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000385 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000386 """Flush buffered output."""
387 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000388 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000389 try:
390 # Detect threads no longer existing.
391 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000392 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000393 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000394 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000395 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000396 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000397 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000398 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000399 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000400
401 # Don't keep the lock while writting. Will append \n when it shouldn't.
402 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000403 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000404 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
405 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000406
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407
408def MakeFileAutoFlush(fileobj, delay=10):
409 autoflush = getattr(fileobj, 'autoflush', None)
410 if autoflush:
411 autoflush.delay = delay
412 return fileobj
413 return AutoFlush(fileobj, delay)
414
415
416def MakeFileAnnotated(fileobj, include_zero=False):
417 if getattr(fileobj, 'annotated', None):
418 return fileobj
419 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000420
421
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000422GCLIENT_CHILDREN = []
423GCLIENT_CHILDREN_LOCK = threading.Lock()
424
425
426class GClientChildren(object):
427 @staticmethod
428 def add(popen_obj):
429 with GCLIENT_CHILDREN_LOCK:
430 GCLIENT_CHILDREN.append(popen_obj)
431
432 @staticmethod
433 def remove(popen_obj):
434 with GCLIENT_CHILDREN_LOCK:
435 GCLIENT_CHILDREN.remove(popen_obj)
436
437 @staticmethod
438 def _attemptToKillChildren():
439 global GCLIENT_CHILDREN
440 with GCLIENT_CHILDREN_LOCK:
441 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
442
443 for zombie in zombies:
444 try:
445 zombie.kill()
446 except OSError:
447 pass
448
449 with GCLIENT_CHILDREN_LOCK:
450 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
451
452 @staticmethod
453 def _areZombies():
454 with GCLIENT_CHILDREN_LOCK:
455 return bool(GCLIENT_CHILDREN)
456
457 @staticmethod
458 def KillAllRemainingChildren():
459 GClientChildren._attemptToKillChildren()
460
461 if GClientChildren._areZombies():
462 time.sleep(0.5)
463 GClientChildren._attemptToKillChildren()
464
465 with GCLIENT_CHILDREN_LOCK:
466 if GCLIENT_CHILDREN:
467 print >> sys.stderr, 'Could not kill the following subprocesses:'
468 for zombie in GCLIENT_CHILDREN:
469 print >> sys.stderr, ' ', zombie.pid
470
471
maruel@chromium.org17d01792010-09-01 18:07:10 +0000472def CheckCallAndFilter(args, stdout=None, filter_fn=None,
473 print_stdout=None, call_filter_on_first_line=False,
tandrii64103db2016-10-11 05:30:05 -0700474 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000475 """Runs a command and calls back a filter function if needed.
476
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000477 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000478 print_stdout: If True, the command's stdout is forwarded to stdout.
479 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000480 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000481 character trimmed.
482 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000483 retry: If the process exits non-zero, sleep for a brief interval and try
484 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000485
486 stderr is always redirected to stdout.
487 """
488 assert print_stdout or filter_fn
489 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000490 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000491 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000492
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000493 sleep_interval = RETRY_INITIAL_SLEEP
494 run_cwd = kwargs.get('cwd', os.getcwd())
495 for _ in xrange(RETRY_MAX + 1):
496 kid = subprocess2.Popen(
497 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
498 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000499
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000500 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000501
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000502 # Do a flush of stdout before we begin reading from the subprocess2's stdout
503 stdout.flush()
504
505 # Also, we need to forward stdout to prevent weird re-ordering of output.
506 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700507 # normally buffering is done for each line, but if the process requests
508 # input, no end-of-line character is output after the prompt and it would
509 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000510 try:
511 in_byte = kid.stdout.read(1)
512 if in_byte:
513 if call_filter_on_first_line:
514 filter_fn(None)
515 in_line = ''
516 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000517 output.write(in_byte)
518 if print_stdout:
519 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000520 if in_byte not in ['\r', '\n']:
521 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000522 else:
523 filter_fn(in_line)
524 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000525 in_byte = kid.stdout.read(1)
526 # Flush the rest of buffered output. This is only an issue with
527 # stdout/stderr not ending with a \n.
528 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000529 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000530 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000531
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000532 # Don't put this in a 'finally,' since the child may still run if we get
533 # an exception.
534 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000535
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000536 except KeyboardInterrupt:
537 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
538 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000539
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000540 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000541 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000542 if not retry:
543 break
tandrii30d95622016-10-11 05:20:26 -0700544 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
545 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000546 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000547 sleep_interval *= 2
548 raise subprocess2.CalledProcessError(
549 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000550
551
agable@chromium.org5a306a22014-02-24 22:13:59 +0000552class GitFilter(object):
553 """A filter_fn implementation for quieting down git output messages.
554
555 Allows a custom function to skip certain lines (predicate), and will throttle
556 the output of percentage completed lines to only output every X seconds.
557 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000558 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000559
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000560 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000561 """
562 Args:
563 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
564 XX% complete messages) to only be printed at least |time_throttle|
565 seconds apart.
566 predicate (f(line)): An optional function which is invoked for every line.
567 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000568 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000569 """
570 self.last_time = 0
571 self.time_throttle = time_throttle
572 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000573 self.out_fh = out_fh or sys.stdout
574 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000575
576 def __call__(self, line):
577 # git uses an escape sequence to clear the line; elide it.
578 esc = line.find(unichr(033))
579 if esc > -1:
580 line = line[:esc]
581 if self.predicate and not self.predicate(line):
582 return
583 now = time.time()
584 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000585 if match:
586 if match.group(1) != self.progress_prefix:
587 self.progress_prefix = match.group(1)
588 elif now - self.last_time < self.time_throttle:
589 return
590 self.last_time = now
591 self.out_fh.write('[%s] ' % Elapsed())
592 print >> self.out_fh, line
agable@chromium.org5a306a22014-02-24 22:13:59 +0000593
594
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000595def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000596 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000597 real_from_dir = os.path.realpath(from_dir)
598 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000599 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000600 split_path = os.path.split(path)
601 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000602 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000603 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000604
605 # If we did not find the file in the current directory, make sure we are in a
606 # sub directory that is controlled by this configuration.
607 if path != real_from_dir:
608 entries_filename = os.path.join(path, filename + '_entries')
609 if not os.path.exists(entries_filename):
610 # If .gclient_entries does not exist, a previous call to gclient sync
611 # might have failed. In that case, we cannot verify that the .gclient
612 # is the one we want to use. In order to not to cause too much trouble,
613 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000614 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000615 "file you want to use" % (filename, path))
616 return path
617 scope = {}
618 try:
619 exec(FileRead(entries_filename), scope)
620 except SyntaxError, e:
621 SyntaxErrorToError(filename, e)
622 all_directories = scope['entries'].keys()
623 path_to_check = real_from_dir[len(path)+1:]
624 while path_to_check:
625 if path_to_check in all_directories:
626 return path
627 path_to_check = os.path.dirname(path_to_check)
628 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000629
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000630 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000631 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000632
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000633
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000634def PathDifference(root, subpath):
635 """Returns the difference subpath minus root."""
636 root = os.path.realpath(root)
637 subpath = os.path.realpath(subpath)
638 if not subpath.startswith(root):
639 return None
640 # If the root does not have a trailing \ or /, we add it so the returned
641 # path starts immediately after the seperator regardless of whether it is
642 # provided.
643 root = os.path.join(root, '')
644 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000645
646
647def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000648 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000649
rcui@google.com13595ff2011-10-13 01:25:07 +0000650 Returns nearest upper-level directory with the passed in file.
651 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000652 if not path:
653 path = os.getcwd()
654 path = os.path.realpath(path)
655 while True:
656 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000657 if os.path.exists(file_path):
658 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000659 (new_path, _) = os.path.split(path)
660 if new_path == path:
661 return None
662 path = new_path
663
664
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000665def GetMacWinOrLinux():
666 """Returns 'mac', 'win', or 'linux', matching the current platform."""
667 if sys.platform.startswith(('cygwin', 'win')):
668 return 'win'
669 elif sys.platform.startswith('linux'):
670 return 'linux'
671 elif sys.platform == 'darwin':
672 return 'mac'
673 raise Error('Unknown platform: ' + sys.platform)
674
675
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000676def GetPrimarySolutionPath():
677 """Returns the full path to the primary solution. (gclient_root + src)"""
zturner@chromium.org0db9a142014-08-13 23:15:25 +0000678
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000679 gclient_root = FindGclientRoot(os.getcwd())
680 if not gclient_root:
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000681 # Some projects might not use .gclient. Try to see whether we're in a git
682 # checkout.
683 top_dir = [os.getcwd()]
684 def filter_fn(line):
hanpfei13f9c372016-08-08 22:05:56 -0700685 repo_root_path = os.path.normpath(line.rstrip('\n'))
686 if os.path.exists(repo_root_path):
687 top_dir[0] = repo_root_path
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000688 try:
689 CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
690 print_stdout=False, filter_fn=filter_fn)
691 except Exception:
692 pass
693 top_dir = top_dir[0]
694 if os.path.exists(os.path.join(top_dir, 'buildtools')):
jiangj@opera.comd6d15b82015-04-20 06:43:48 +0000695 return top_dir
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000696 return None
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000697
698 # Some projects' top directory is not named 'src'.
699 source_dir_name = GetGClientPrimarySolutionName(gclient_root) or 'src'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000700 return os.path.join(gclient_root, source_dir_name)
701
702
703def GetBuildtoolsPath():
704 """Returns the full path to the buildtools directory.
705 This is based on the root of the checkout containing the current directory."""
706
707 # Overriding the build tools path by environment is highly unsupported and may
708 # break without warning. Do not rely on this for anything important.
709 override = os.environ.get('CHROMIUM_BUILDTOOLS_PATH')
710 if override is not None:
711 return override
712
713 primary_solution = GetPrimarySolutionPath()
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000714 if not primary_solution:
715 return None
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000716 buildtools_path = os.path.join(primary_solution, 'buildtools')
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000717 if not os.path.exists(buildtools_path):
718 # Buildtools may be in the gclient root.
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000719 gclient_root = FindGclientRoot(os.getcwd())
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000720 buildtools_path = os.path.join(gclient_root, 'buildtools')
721 return buildtools_path
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000722
723
724def GetBuildtoolsPlatformBinaryPath():
725 """Returns the full path to the binary directory for the current platform."""
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000726 buildtools_path = GetBuildtoolsPath()
727 if not buildtools_path:
728 return None
729
730 if sys.platform.startswith(('cygwin', 'win')):
731 subdir = 'win'
732 elif sys.platform == 'darwin':
733 subdir = 'mac'
734 elif sys.platform.startswith('linux'):
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000735 subdir = 'linux64'
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000736 else:
737 raise Error('Unknown platform: ' + sys.platform)
738 return os.path.join(buildtools_path, subdir)
739
740
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000741def GetExeSuffix():
742 """Returns '' or '.exe' depending on how executables work on this platform."""
743 if sys.platform.startswith(('cygwin', 'win')):
744 return '.exe'
745 return ''
746
747
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000748def GetGClientPrimarySolutionName(gclient_root_dir_path):
749 """Returns the name of the primary solution in the .gclient file specified."""
750 gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
751 env = {}
752 execfile(gclient_config_file, env)
753 solutions = env.get('solutions', [])
754 if solutions:
755 return solutions[0].get('name')
756 return None
757
758
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000759def GetGClientRootAndEntries(path=None):
760 """Returns the gclient root and the dict of entries."""
761 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000762 root = FindFileUpwards(config_file, path)
763 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000764 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000765 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000766 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000767 env = {}
768 execfile(config_path, env)
769 config_dir = os.path.dirname(config_path)
770 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000771
772
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000773def lockedmethod(method):
774 """Method decorator that holds self.lock for the duration of the call."""
775 def inner(self, *args, **kwargs):
776 try:
777 try:
778 self.lock.acquire()
779 except KeyboardInterrupt:
780 print >> sys.stderr, 'Was deadlocked'
781 raise
782 return method(self, *args, **kwargs)
783 finally:
784 self.lock.release()
785 return inner
786
787
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000788class WorkItem(object):
789 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000790 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
791 # As a workaround, use a single lock. Yep you read it right. Single lock for
792 # all the 100 objects.
793 lock = threading.Lock()
794
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000795 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000796 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000797 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000798 self.outbuf = cStringIO.StringIO()
799 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700800 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000801
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000802 def run(self, work_queue):
803 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000804 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000805 pass
806
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000807 @property
808 def name(self):
809 return self._name
810
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000811
812class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000813 """Runs a set of WorkItem that have interdependencies and were WorkItem are
814 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000815
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200816 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000817 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000818
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000819 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000820 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000821 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000822 """jobs specifies the number of concurrent tasks to allow. progress is a
823 Progress instance."""
824 # Set when a thread is done or a new item is enqueued.
825 self.ready_cond = threading.Condition()
826 # Maximum number of concurrent tasks.
827 self.jobs = jobs
828 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000829 self.queued = []
830 # List of strings representing each Dependency.name that was run.
831 self.ran = []
832 # List of items currently running.
833 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000834 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000835 self.exceptions = Queue.Queue()
836 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000837 self.progress = progress
838 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000839 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000840
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000841 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000842 self.verbose = verbose
843 self.last_join = None
844 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000845
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000846 def enqueue(self, d):
847 """Enqueue one Dependency to be executed later once its requirements are
848 satisfied.
849 """
850 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000851 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000852 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000853 self.queued.append(d)
854 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000855 if self.jobs == 1:
856 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000857 logging.debug('enqueued(%s)' % d.name)
858 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000859 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000860 self.progress.update(0)
861 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000862 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000864
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000865 def out_cb(self, _):
866 self.last_subproc_output = datetime.datetime.now()
867 return True
868
869 @staticmethod
870 def format_task_output(task, comment=''):
871 if comment:
872 comment = ' (%s)' % comment
873 if task.start and task.finish:
874 elapsed = ' (Elapsed: %s)' % (
875 str(task.finish - task.start).partition('.')[0])
876 else:
877 elapsed = ''
878 return """
879%s%s%s
880----------------------------------------
881%s
882----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000883 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000884
hinoka885e5b12016-06-08 14:40:09 -0700885 def _is_conflict(self, job):
886 """Checks to see if a job will conflict with another running job."""
887 for running_job in self.running:
888 for used_resource in running_job.item.resources:
889 logging.debug('Checking resource %s' % used_resource)
890 if used_resource in job.resources:
891 return True
892 return False
893
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000894 def flush(self, *args, **kwargs):
895 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000896 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000897 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000898 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000899 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 while True:
901 # Check for task to run first, then wait.
902 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000903 if not self.exceptions.empty():
904 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000905 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000906 self._flush_terminated_threads()
907 if (not self.queued and not self.running or
908 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000909 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000910 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000911
912 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000913 for i in xrange(len(self.queued)):
914 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000915 if (self.ignore_requirements or
916 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700917 if not self._is_conflict(self.queued[i]):
918 # Start one work item: all its requirements are satisfied.
919 self._run_one_task(self.queued.pop(i), args, kwargs)
920 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000921 else:
922 # Couldn't find an item that could run. Break out the outher loop.
923 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000924
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000925 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000926 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000927 break
928 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000929 try:
930 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000931 # If we haven't printed to terminal for a while, but we have received
932 # spew from a suprocess, let the user know we're still progressing.
933 now = datetime.datetime.now()
934 if (now - self.last_join > datetime.timedelta(seconds=60) and
935 self.last_subproc_output > self.last_join):
936 if self.progress:
937 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000938 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000939 elapsed = Elapsed()
940 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000941 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000942 for task in self.running:
943 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000944 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000945 except KeyboardInterrupt:
946 # Help debugging by printing some information:
947 print >> sys.stderr, (
948 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
949 'Running: %d') % (
950 self.jobs,
951 len(self.queued),
952 ', '.join(self.ran),
953 len(self.running)))
954 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000955 print >> sys.stderr, '%s (not started): %s' % (
956 i.name, ', '.join(i.requirements))
957 for i in self.running:
958 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000959 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000960 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000961 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000962 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000963
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000964 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000965 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000966 if self.progress:
967 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000968 # To get back the stack location correctly, the raise a, b, c form must be
969 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000970 e, task = self.exceptions.get()
971 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000972 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000973 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000974 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000975
maruel@chromium.org3742c842010-09-09 19:27:14 +0000976 def _flush_terminated_threads(self):
977 """Flush threads that have terminated."""
978 running = self.running
979 self.running = []
980 for t in running:
981 if t.isAlive():
982 self.running.append(t)
983 else:
984 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000985 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000986 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000987 if self.verbose:
988 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000989 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000990 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000991 if t.item.name in self.ran:
992 raise Error(
993 'gclient is confused, "%s" is already in "%s"' % (
994 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000995 if not t.item.name in self.ran:
996 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000997
998 def _run_one_task(self, task_item, args, kwargs):
999 if self.jobs > 1:
1000 # Start the thread.
1001 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001002 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001003 self.running.append(new_thread)
1004 new_thread.start()
1005 else:
1006 # Run the 'thread' inside the main thread. Don't try to catch any
1007 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001008 try:
1009 task_item.start = datetime.datetime.now()
1010 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
1011 task_item.run(*args, **kwargs)
1012 task_item.finish = datetime.datetime.now()
1013 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
1014 self.ran.append(task_item.name)
1015 if self.verbose:
1016 if self.progress:
1017 print >> sys.stdout, ''
1018 print >> sys.stdout, self.format_task_output(task_item)
1019 if self.progress:
1020 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1021 except KeyboardInterrupt:
1022 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
1023 raise
1024 except Exception:
1025 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
1026 raise
1027
maruel@chromium.org3742c842010-09-09 19:27:14 +00001028
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001029 class _Worker(threading.Thread):
1030 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001031 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001032 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001033 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001034 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001035 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001036 self.args = args
1037 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001038 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001039
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001040 def run(self):
1041 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001042 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001043 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001044 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001045 self.item.start = datetime.datetime.now()
1046 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001047 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001048 self.item.finish = datetime.datetime.now()
1049 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001050 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001051 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001052 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001053 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001054 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001055 except Exception:
1056 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001057 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001058 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001059 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001060 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001061 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001062 work_queue.ready_cond.acquire()
1063 try:
1064 work_queue.ready_cond.notifyAll()
1065 finally:
1066 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001067
1068
agable92bec4f2016-08-24 09:27:27 -07001069def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001070 """Returns the most plausible editor to use.
1071
1072 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001073 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001074 - core.editor git configuration variable (if supplied by git-cl)
1075 - VISUAL environment variable
1076 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001077 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001078
1079 In the case of git-cl, this matches git's behaviour, except that it does not
1080 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001081 """
agable92bec4f2016-08-24 09:27:27 -07001082 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001083 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001084 editor = os.environ.get('VISUAL')
1085 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001086 editor = os.environ.get('EDITOR')
1087 if not editor:
1088 if sys.platform.startswith('win'):
1089 editor = 'notepad'
1090 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001091 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001092 return editor
1093
1094
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001095def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001096 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001097 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001098 # Make sure CRLF is handled properly by requiring none.
1099 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001100 print >> sys.stderr, (
1101 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001102 fileobj = os.fdopen(file_handle, 'w')
1103 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001104 content = re.sub('\r?\n', '\n', content)
1105 # Some editors complain when the file doesn't end in \n.
1106 if not content.endswith('\n'):
1107 content += '\n'
1108 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001109 fileobj.close()
1110
1111 try:
agable92bec4f2016-08-24 09:27:27 -07001112 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001113 if not editor:
1114 return None
1115 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001116 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1117 # Msysgit requires the usage of 'env' to be present.
1118 cmd = 'env ' + cmd
1119 try:
1120 # shell=True to allow the shell to handle all forms of quotes in
1121 # $EDITOR.
1122 subprocess2.check_call(cmd, shell=True)
1123 except subprocess2.CalledProcessError:
1124 return None
1125 return FileRead(filename)
1126 finally:
1127 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001128
1129
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001130def UpgradeToHttps(url):
1131 """Upgrades random urls to https://.
1132
1133 Do not touch unknown urls like ssh:// or git://.
1134 Do not touch http:// urls with a port number,
1135 Fixes invalid GAE url.
1136 """
1137 if not url:
1138 return url
1139 if not re.match(r'[a-z\-]+\://.*', url):
1140 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1141 # relative url and will use http:///foo. Note that it defaults to http://
1142 # for compatibility with naked url like "localhost:8080".
1143 url = 'http://%s' % url
1144 parsed = list(urlparse.urlparse(url))
1145 # Do not automatically upgrade http to https if a port number is provided.
1146 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1147 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001148 return urlparse.urlunparse(parsed)
1149
1150
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001151def ParseCodereviewSettingsContent(content):
1152 """Process a codereview.settings file properly."""
1153 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1154 try:
1155 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1156 except ValueError:
1157 raise Error(
1158 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001159 def fix_url(key):
1160 if keyvals.get(key):
1161 keyvals[key] = UpgradeToHttps(keyvals[key])
1162 fix_url('CODE_REVIEW_SERVER')
1163 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001164 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001165
1166
1167def NumLocalCpus():
1168 """Returns the number of processors.
1169
dnj@chromium.org530523b2015-01-07 19:54:57 +00001170 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1171 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1172 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001173 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001174 # Surround the entire thing in try/except; no failure here should stop gclient
1175 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001176 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001177 # Use multiprocessing to get CPU count. This may raise
1178 # NotImplementedError.
1179 try:
1180 import multiprocessing
1181 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001182 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001183 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001184 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001185 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1186 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1187
1188 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1189 if 'NUMBER_OF_PROCESSORS' in os.environ:
1190 return int(os.environ['NUMBER_OF_PROCESSORS'])
1191 except Exception as e:
1192 logging.exception("Exception raised while probing CPU count: %s", e)
1193
1194 logging.debug('Failed to get CPU count. Defaulting to 1.')
1195 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001196
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001197
szager@chromium.orgfc616382014-03-18 20:32:04 +00001198def DefaultDeltaBaseCacheLimit():
1199 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1200
1201 The primary constraint is the address space of virtual memory. The cache
1202 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1203 parameter is set too high.
1204 """
1205 if platform.architecture()[0].startswith('64'):
1206 return '2g'
1207 else:
1208 return '512m'
1209
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001210
szager@chromium.orgff113292014-03-25 06:02:08 +00001211def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001212 """Return reasonable default values for configuring git-index-pack.
1213
1214 Experiments suggest that higher values for pack.threads don't improve
1215 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001216 cache_limit = DefaultDeltaBaseCacheLimit()
1217 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1218 if url in THREADED_INDEX_PACK_BLACKLIST:
1219 result.extend(['-c', 'pack.threads=1'])
1220 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001221
1222
1223def FindExecutable(executable):
1224 """This mimics the "which" utility."""
1225 path_folders = os.environ.get('PATH').split(os.pathsep)
1226
1227 for path_folder in path_folders:
1228 target = os.path.join(path_folder, executable)
1229 # Just incase we have some ~/blah paths.
1230 target = os.path.abspath(os.path.expanduser(target))
1231 if os.path.isfile(target) and os.access(target, os.X_OK):
1232 return target
1233 if sys.platform.startswith('win'):
1234 for suffix in ('.bat', '.cmd', '.exe'):
1235 alt_target = target + suffix
1236 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1237 return alt_target
1238 return None