blob: b645806e6fd8861ddd4721f5246b1f8846a72f9a [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
Raul Tambreb946b232019-03-26 14:48:46 +00007from __future__ import print_function
8
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00009import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020010import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +000011import contextlib
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000012import datetime
Ben Pastened410c662020-08-26 17:07:03 +000013import errno
Raul Tambreb946b232019-03-26 14:48:46 +000014import functools
15import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000016import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020017import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000018import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000019import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000020import platform
msb@chromium.orgac915bb2009-11-13 17:03:01 +000021import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000022import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000023import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000025import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000026import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000027import time
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000028import subprocess2
29
Raul Tambreb946b232019-03-26 14:48:46 +000030if sys.version_info.major == 2:
31 from cStringIO import StringIO
Raul Tambre6693d092020-02-19 20:36:45 +000032 import collections as collections_abc
Edward Lemura8145022020-01-06 18:47:54 +000033 import Queue as queue
34 import urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000035else:
Raul Tambre6693d092020-02-19 20:36:45 +000036 from collections import abc as collections_abc
Raul Tambreb946b232019-03-26 14:48:46 +000037 from io import StringIO
Edward Lemura8145022020-01-06 18:47:54 +000038 import queue
39 import urllib.parse as urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000040
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000041
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000042RETRY_MAX = 3
43RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000044START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000045
46
borenet@google.com6a9b1682014-03-24 18:35:23 +000047_WARNINGS = []
48
49
szager@chromium.orgff113292014-03-25 06:02:08 +000050# These repos are known to cause OOM errors on 32-bit platforms, due the the
51# very large objects they contain. It is not safe to use threaded index-pack
52# when cloning/fetching them.
Ayu Ishii09858612020-06-26 18:00:52 +000053THREADED_INDEX_PACK_BLOCKLIST = [
szager@chromium.orgff113292014-03-25 06:02:08 +000054 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
55]
56
Raul Tambreb946b232019-03-26 14:48:46 +000057"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
58if sys.version_info.major == 2:
59 # We have to use exec to avoid a SyntaxError in Python 3.
60 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
61else:
62 def reraise(typ, value, tb=None):
63 if value is None:
64 value = typ()
65 if value.__traceback__ is not tb:
66 raise value.with_traceback(tb)
67 raise value
68
szager@chromium.orgff113292014-03-25 06:02:08 +000069
maruel@chromium.org66c83e62010-09-07 14:18:45 +000070class Error(Exception):
71 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000072 def __init__(self, msg, *args, **kwargs):
73 index = getattr(threading.currentThread(), 'index', 0)
74 if index:
75 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
76 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000077
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000078
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000079def Elapsed(until=None):
80 if until is None:
81 until = datetime.datetime.now()
82 return str(until - START).partition('.')[0]
83
84
borenet@google.com6a9b1682014-03-24 18:35:23 +000085def PrintWarnings():
86 """Prints any accumulated warnings."""
87 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000088 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000089 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000090 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000091
92
93def AddWarning(msg):
94 """Adds the given warning message to the list of accumulated warnings."""
95 _WARNINGS.append(msg)
96
97
msb@chromium.orgac915bb2009-11-13 17:03:01 +000098def SplitUrlRevision(url):
99 """Splits url and returns a two-tuple: url, rev"""
100 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000101 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000102 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000103 components = re.search(regex, url).groups()
104 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000105 components = url.rsplit('@', 1)
106 if re.match(r'^\w+\@', url) and '@' not in components[0]:
107 components = [url]
108
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000109 if len(components) == 1:
110 components += [None]
111 return tuple(components)
112
113
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000114def IsGitSha(revision):
115 """Returns true if the given string is a valid hex-encoded sha"""
116 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
117
118
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200119def IsFullGitSha(revision):
120 """Returns true if the given string is a valid hex-encoded full sha"""
121 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
122
123
floitsch@google.comeaab7842011-04-28 09:07:58 +0000124def IsDateRevision(revision):
125 """Returns true if the given revision is of the form "{ ... }"."""
126 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
127
128
129def MakeDateRevision(date):
130 """Returns a revision representing the latest revision before the given
131 date."""
132 return "{" + date + "}"
133
134
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000135def SyntaxErrorToError(filename, e):
136 """Raises a gclient_utils.Error exception with the human readable message"""
137 try:
138 # Try to construct a human readable error message
139 if filename:
140 error_message = 'There is a syntax error in %s\n' % filename
141 else:
142 error_message = 'There is a syntax error\n'
143 error_message += 'Line #%s, character %s: "%s"' % (
144 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
145 except:
146 # Something went wrong, re-raise the original exception
147 raise e
148 else:
149 raise Error(error_message)
150
151
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000152class PrintableObject(object):
153 def __str__(self):
154 output = ''
155 for i in dir(self):
156 if i.startswith('__'):
157 continue
158 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
159 return output
160
161
Edward Lesmesae3586b2020-03-23 21:21:14 +0000162def AskForData(message):
163 # Use this so that it can be mocked in tests on Python 2 and 3.
164 try:
165 if sys.version_info.major == 2:
166 return raw_input(message)
167 return input(message)
168 except KeyboardInterrupt:
169 # Hide the exception.
170 sys.exit(1)
171
172
Edward Lemur419c92f2019-10-25 22:17:49 +0000173def FileRead(filename, mode='rbU'):
Josip Sokcevic8b3bc252021-06-04 18:44:11 +0000174 # mode is ignored now; we always return unicode strings.
175 with open(filename, mode='rb') as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000176 s = f.read()
Josip Sokcevic8b3bc252021-06-04 18:44:11 +0000177 try:
178 return s.decode('utf-8', 'replace')
179 except (UnicodeDecodeError, AttributeError):
Edward Lemur419c92f2019-10-25 22:17:49 +0000180 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000181
182
Edward Lemur1773f372020-02-22 00:27:14 +0000183def FileWrite(filename, content, mode='w', encoding='utf-8'):
184 with codecs.open(filename, mode=mode, encoding=encoding) as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000185 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000186
187
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000188@contextlib.contextmanager
189def temporary_directory(**kwargs):
190 tdir = tempfile.mkdtemp(**kwargs)
191 try:
192 yield tdir
193 finally:
194 if tdir:
195 rmtree(tdir)
196
197
Edward Lemur1773f372020-02-22 00:27:14 +0000198@contextlib.contextmanager
199def temporary_file():
200 """Creates a temporary file.
201
202 On Windows, a file must be closed before it can be opened again. This function
203 allows to write something like:
204
205 with gclient_utils.temporary_file() as tmp:
206 gclient_utils.FileWrite(tmp, foo)
207 useful_stuff(tmp)
208
209 Instead of something like:
210
211 with tempfile.NamedTemporaryFile(delete=False) as tmp:
212 tmp.write(foo)
213 tmp.close()
214 try:
215 useful_stuff(tmp)
216 finally:
217 os.remove(tmp.name)
218 """
219 handle, name = tempfile.mkstemp()
220 os.close(handle)
221 try:
222 yield name
223 finally:
224 os.remove(name)
225
226
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000227def safe_rename(old, new):
228 """Renames a file reliably.
229
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000230 Sometimes os.rename does not work because a dying git process keeps a handle
231 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000232 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000233 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000234 """
235 # roughly 10s
236 retries = 100
237 for i in range(retries):
238 try:
239 os.rename(old, new)
240 break
241 except OSError:
242 if i == (retries - 1):
243 # Give up.
244 raise
245 # retry
246 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
247 time.sleep(0.1)
248
249
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000250def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000251 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000252 os.remove(path)
253 else:
254 rmtree(path)
255
256
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000257def rmtree(path):
258 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000259
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000260 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000261
262 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700263 are read-only. We need to be able to force the files to be writable (i.e.,
264 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000265
266 Even with all this, Windows still sometimes fails to delete a file, citing
267 a permission error (maybe something to do with antivirus scans or disk
268 indexing). The best suggestion any of the user forums had was to wait a
269 bit and try again, so we do that too. It's hand-waving, but sometimes it
270 works. :/
271
272 On POSIX systems, things are a little bit simpler. The modes of the files
273 to be deleted doesn't matter, only the modes of the directories containing
274 them are significant. As the directory tree is traversed, each directory
275 has its mode set appropriately before descending into it. This should
276 result in the entire tree being removed, with the possible exception of
277 *path itself, because nothing attempts to change the mode of its parent.
278 Doing so would be hazardous, as it's not a directory slated for removal.
279 In the ordinary case, this is not a problem: for our purposes, the user
280 will never lack write permission on *path's parent.
281 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000282 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000283 return
284
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000285 if os.path.islink(path) or not os.path.isdir(path):
286 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000287
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000288 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000289 # Give up and use cmd.exe's rd command.
290 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000291 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000292 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
293 if exitcode == 0:
294 return
295 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000296 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000297 time.sleep(3)
298 raise Exception('Failed to remove path %s' % path)
299
300 # On POSIX systems, we need the x-bit set on the directory to access it,
301 # the r-bit to see its contents, and the w-bit to remove files from it.
302 # The actual modes of the files within the directory is irrelevant.
303 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000304
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000305 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000306 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000307
308 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000309 # If fullpath is a symbolic link that points to a directory, isdir will
310 # be True, but we don't want to descend into that as a directory, we just
311 # want to remove the link. Check islink and treat links as ordinary files
312 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000313 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000314 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000315 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000316 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000317 # Recurse.
318 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000319
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000320 remove(os.rmdir, path)
321
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000322
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000323def safe_makedirs(tree):
324 """Creates the directory in a safe manner.
325
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000326 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000327 exception and pass on.
328 """
329 count = 0
330 while not os.path.exists(tree):
331 count += 1
332 try:
333 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000334 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000335 # 17 POSIX, 183 Windows
336 if e.errno not in (17, 183):
337 raise
338 if count > 40:
339 # Give up.
340 raise
341
342
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000343def CommandToStr(args):
344 """Converts an arg list into a shell escaped string."""
345 return ' '.join(pipes.quote(arg) for arg in args)
346
347
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348class Wrapper(object):
349 """Wraps an object, acting as a transparent proxy for all properties by
350 default.
351 """
352 def __init__(self, wrapped):
353 self._wrapped = wrapped
354
355 def __getattr__(self, name):
356 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000357
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000358
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000359class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000360 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000361 def __init__(self, wrapped, delay):
362 super(AutoFlush, self).__init__(wrapped)
363 if not hasattr(self, 'lock'):
364 self.lock = threading.Lock()
365 self.__last_flushed_at = time.time()
366 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000367
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000368 @property
369 def autoflush(self):
370 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000371
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000372 def write(self, out, *args, **kwargs):
373 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000374 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000375 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000376 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000378 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000379 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000380 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000381 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000382 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000383 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000384
385
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000386class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000387 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000388 threads with a NN> prefix.
389 """
390 def __init__(self, wrapped, include_zero=False):
391 super(Annotated, self).__init__(wrapped)
392 if not hasattr(self, 'lock'):
393 self.lock = threading.Lock()
394 self.__output_buffers = {}
395 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000396 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000397
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000398 @property
399 def annotated(self):
400 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000401
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000402 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000403 # Store as bytes to ensure Unicode characters get output correctly.
404 if not isinstance(out, bytes):
405 out = out.encode('utf-8')
406
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 index = getattr(threading.currentThread(), 'index', 0)
408 if not index and not self.__include_zero:
409 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000410 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000411
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000412 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000413 try:
414 # Use a dummy array to hold the string so the code can be lockless.
415 # Strings are immutable, requiring to keep a lock for the whole dictionary
416 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000417 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000418 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000419 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000420 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000421 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000422 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000423
424 # Continue lockless.
425 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000426 while True:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000427 cr_loc = obj[0].find(b'\r')
428 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000429 if cr_loc == lf_loc == -1:
430 break
431 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000432 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000433 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000434 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000435 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000436 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000437 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000438 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000439 obj[0] = remaining
440
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000441 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000442 """Flush buffered output."""
443 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000444 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000445 try:
446 # Detect threads no longer existing.
447 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000448 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000449 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000450 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000451 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000452 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000453 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000454 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000455 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000456
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000457 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000458 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000459 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000460 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000461 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000462
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000463
464def MakeFileAutoFlush(fileobj, delay=10):
465 autoflush = getattr(fileobj, 'autoflush', None)
466 if autoflush:
467 autoflush.delay = delay
468 return fileobj
469 return AutoFlush(fileobj, delay)
470
471
472def MakeFileAnnotated(fileobj, include_zero=False):
473 if getattr(fileobj, 'annotated', None):
474 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000475 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000476
477
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000478GCLIENT_CHILDREN = []
479GCLIENT_CHILDREN_LOCK = threading.Lock()
480
481
482class GClientChildren(object):
483 @staticmethod
484 def add(popen_obj):
485 with GCLIENT_CHILDREN_LOCK:
486 GCLIENT_CHILDREN.append(popen_obj)
487
488 @staticmethod
489 def remove(popen_obj):
490 with GCLIENT_CHILDREN_LOCK:
491 GCLIENT_CHILDREN.remove(popen_obj)
492
493 @staticmethod
494 def _attemptToKillChildren():
495 global GCLIENT_CHILDREN
496 with GCLIENT_CHILDREN_LOCK:
497 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
498
499 for zombie in zombies:
500 try:
501 zombie.kill()
502 except OSError:
503 pass
504
505 with GCLIENT_CHILDREN_LOCK:
506 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
507
508 @staticmethod
509 def _areZombies():
510 with GCLIENT_CHILDREN_LOCK:
511 return bool(GCLIENT_CHILDREN)
512
513 @staticmethod
514 def KillAllRemainingChildren():
515 GClientChildren._attemptToKillChildren()
516
517 if GClientChildren._areZombies():
518 time.sleep(0.5)
519 GClientChildren._attemptToKillChildren()
520
521 with GCLIENT_CHILDREN_LOCK:
522 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000523 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000524 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000525 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000526
527
Edward Lemur24146be2019-08-01 21:44:52 +0000528def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
529 show_header=False, always_show_header=False, retry=False,
530 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000531 """Runs a command and calls back a filter function if needed.
532
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000533 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000534 print_stdout: If True, the command's stdout is forwarded to stdout.
535 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000536 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000537 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000538 show_header: Whether to display a header before the command output.
539 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000540 retry: If the process exits non-zero, sleep for a brief interval and try
541 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000542
543 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000544
545 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000546 """
Edward Lemur24146be2019-08-01 21:44:52 +0000547 def show_header_if_necessary(needs_header, attempt):
548 """Show the header at most once."""
549 if not needs_header[0]:
550 return
551
552 needs_header[0] = False
553 # Automatically generated header. We only prepend a newline if
554 # always_show_header is false, since it usually indicates there's an
555 # external progress display, and it's better not to clobber it in that case.
556 header = '' if always_show_header else '\n'
557 header += '________ running \'%s\' in \'%s\'' % (
558 ' '.join(args), kwargs.get('cwd', '.'))
559 if attempt:
560 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
561 header += '\n'
562
563 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000564 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
565 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000566 if filter_fn:
567 filter_fn(header)
568
569 def filter_line(command_output, line_start):
570 """Extract the last line from command output and filter it."""
571 if not filter_fn or line_start is None:
572 return
573 command_output.seek(line_start)
574 filter_fn(command_output.read().decode('utf-8'))
575
576 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
577 # byte inputs and sys.stdout.buffer must be used instead.
578 if print_stdout:
579 sys.stdout.flush()
580 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
581 else:
582 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000583
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000584 sleep_interval = RETRY_INITIAL_SLEEP
585 run_cwd = kwargs.get('cwd', os.getcwd())
Josip Sokcevic740825e2021-05-12 18:28:34 +0000586
587 # Store the output of the command regardless of the value of print_stdout or
588 # filter_fn.
589 command_output = io.BytesIO()
Edward Lemur24146be2019-08-01 21:44:52 +0000590 for attempt in range(RETRY_MAX + 1):
Ben Pastened410c662020-08-26 17:07:03 +0000591 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
592 # subprocess when filtering its output. This makes the subproc believe
593 # it was launched from a terminal, which will preserve ANSI color codes.
Milad Fad949c912020-09-18 00:26:08 +0000594 os_type = GetMacWinAixOrLinux()
595 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
Ben Pastened410c662020-08-26 17:07:03 +0000596 pipe_reader, pipe_writer = os.openpty()
597 else:
598 pipe_reader, pipe_writer = os.pipe()
599
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000600 kid = subprocess2.Popen(
Ben Pastened410c662020-08-26 17:07:03 +0000601 args, bufsize=0, stdout=pipe_writer, stderr=subprocess2.STDOUT,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000602 **kwargs)
Ben Pastened410c662020-08-26 17:07:03 +0000603 # Close the write end of the pipe once we hand it off to the child proc.
604 os.close(pipe_writer)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000605
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000606 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000607
Edward Lemur24146be2019-08-01 21:44:52 +0000608 # Passed as a list for "by ref" semantics.
609 needs_header = [show_header]
610 if always_show_header:
611 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000612
613 # Also, we need to forward stdout to prevent weird re-ordering of output.
614 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700615 # normally buffering is done for each line, but if the process requests
616 # input, no end-of-line character is output after the prompt and it would
617 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000618 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000619 line_start = None
620 while True:
Ben Pastened410c662020-08-26 17:07:03 +0000621 try:
622 in_byte = os.read(pipe_reader, 1)
623 except (IOError, OSError) as e:
624 if e.errno == errno.EIO:
625 # An errno.EIO means EOF?
626 in_byte = None
627 else:
628 raise e
Edward Lemur24146be2019-08-01 21:44:52 +0000629 is_newline = in_byte in (b'\n', b'\r')
630 if not in_byte:
631 break
632
633 show_header_if_necessary(needs_header, attempt)
634
635 if is_newline:
636 filter_line(command_output, line_start)
637 line_start = None
638 elif line_start is None:
639 line_start = command_output.tell()
640
641 stdout_write(in_byte)
642 command_output.write(in_byte)
643
644 # Flush the rest of buffered output.
645 sys.stdout.flush()
646 if line_start is not None:
647 filter_line(command_output, line_start)
648
Ben Pastened410c662020-08-26 17:07:03 +0000649 os.close(pipe_reader)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000650 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000651
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000652 # Don't put this in a 'finally,' since the child may still run if we get
653 # an exception.
654 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000655
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000656 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000657 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000658 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000659
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000660 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000661 return command_output.getvalue()
662
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000663 if not retry:
664 break
Edward Lemur24146be2019-08-01 21:44:52 +0000665
Raul Tambreb946b232019-03-26 14:48:46 +0000666 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
667 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
Josip Sokcevic740825e2021-05-12 18:28:34 +0000668 command_output = io.BytesIO()
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000669 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000670 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000671
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000672 raise subprocess2.CalledProcessError(
Josip Sokcevic740825e2021-05-12 18:28:34 +0000673 rv, args, kwargs.get('cwd', None), command_output.getvalue(), None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000674
675
agable@chromium.org5a306a22014-02-24 22:13:59 +0000676class GitFilter(object):
677 """A filter_fn implementation for quieting down git output messages.
678
679 Allows a custom function to skip certain lines (predicate), and will throttle
680 the output of percentage completed lines to only output every X seconds.
681 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000682 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000683
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000684 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000685 """
686 Args:
687 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
688 XX% complete messages) to only be printed at least |time_throttle|
689 seconds apart.
690 predicate (f(line)): An optional function which is invoked for every line.
691 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000692 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000693 """
Edward Lemur24146be2019-08-01 21:44:52 +0000694 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000695 self.last_time = 0
696 self.time_throttle = time_throttle
697 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000698 self.out_fh = out_fh or sys.stdout
699 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000700
701 def __call__(self, line):
702 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000703 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000704 if esc > -1:
705 line = line[:esc]
706 if self.predicate and not self.predicate(line):
707 return
708 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000709 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000710 if match:
711 if match.group(1) != self.progress_prefix:
712 self.progress_prefix = match.group(1)
713 elif now - self.last_time < self.time_throttle:
714 return
715 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000716 if not self.first_line:
717 self.out_fh.write('[%s] ' % Elapsed())
718 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000719 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000720
721
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000722def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000723 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000724
rcui@google.com13595ff2011-10-13 01:25:07 +0000725 Returns nearest upper-level directory with the passed in file.
726 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000727 if not path:
728 path = os.getcwd()
729 path = os.path.realpath(path)
730 while True:
731 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000732 if os.path.exists(file_path):
733 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000734 (new_path, _) = os.path.split(path)
735 if new_path == path:
736 return None
737 path = new_path
738
739
Milad Fa52fdd1f2020-09-15 21:24:46 +0000740def GetMacWinAixOrLinux():
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000741 """Returns 'mac', 'win', or 'linux', matching the current platform."""
742 if sys.platform.startswith(('cygwin', 'win')):
743 return 'win'
744 elif sys.platform.startswith('linux'):
745 return 'linux'
746 elif sys.platform == 'darwin':
747 return 'mac'
Milad Fa52fdd1f2020-09-15 21:24:46 +0000748 elif sys.platform.startswith('aix'):
749 return 'aix'
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000750 raise Error('Unknown platform: ' + sys.platform)
751
752
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000753def GetGClientRootAndEntries(path=None):
754 """Returns the gclient root and the dict of entries."""
755 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000756 root = FindFileUpwards(config_file, path)
757 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000758 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000759 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000760 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000761 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000762 with open(config_path) as config:
763 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000764 config_dir = os.path.dirname(config_path)
765 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000766
767
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000768def lockedmethod(method):
769 """Method decorator that holds self.lock for the duration of the call."""
770 def inner(self, *args, **kwargs):
771 try:
772 try:
773 self.lock.acquire()
774 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000775 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000776 raise
777 return method(self, *args, **kwargs)
778 finally:
779 self.lock.release()
780 return inner
781
782
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000783class WorkItem(object):
784 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000785 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
786 # As a workaround, use a single lock. Yep you read it right. Single lock for
787 # all the 100 objects.
788 lock = threading.Lock()
789
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000790 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000791 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000792 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000793 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000794 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700795 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000796
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000797 def run(self, work_queue):
798 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000799 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000800 pass
801
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000802 @property
803 def name(self):
804 return self._name
805
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000806
807class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000808 """Runs a set of WorkItem that have interdependencies and were WorkItem are
809 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000810
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200811 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000812 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000813
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000814 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000815 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000816 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000817 """jobs specifies the number of concurrent tasks to allow. progress is a
818 Progress instance."""
819 # Set when a thread is done or a new item is enqueued.
820 self.ready_cond = threading.Condition()
821 # Maximum number of concurrent tasks.
822 self.jobs = jobs
823 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000824 self.queued = []
825 # List of strings representing each Dependency.name that was run.
826 self.ran = []
827 # List of items currently running.
828 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000829 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000830 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000831 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000832 self.progress = progress
833 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000834 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000835
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000836 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000837 self.verbose = verbose
838 self.last_join = None
839 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000840
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000841 def enqueue(self, d):
842 """Enqueue one Dependency to be executed later once its requirements are
843 satisfied.
844 """
845 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000846 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000847 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000848 self.queued.append(d)
849 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000850 if self.jobs == 1:
851 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000852 logging.debug('enqueued(%s)' % d.name)
853 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000854 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000855 self.progress.update(0)
856 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000857 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000858 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000859
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000860 def out_cb(self, _):
861 self.last_subproc_output = datetime.datetime.now()
862 return True
863
864 @staticmethod
865 def format_task_output(task, comment=''):
866 if comment:
867 comment = ' (%s)' % comment
868 if task.start and task.finish:
869 elapsed = ' (Elapsed: %s)' % (
870 str(task.finish - task.start).partition('.')[0])
871 else:
872 elapsed = ''
873 return """
874%s%s%s
875----------------------------------------
876%s
877----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000878 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000879
hinoka885e5b12016-06-08 14:40:09 -0700880 def _is_conflict(self, job):
881 """Checks to see if a job will conflict with another running job."""
882 for running_job in self.running:
883 for used_resource in running_job.item.resources:
884 logging.debug('Checking resource %s' % used_resource)
885 if used_resource in job.resources:
886 return True
887 return False
888
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000889 def flush(self, *args, **kwargs):
890 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000891 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000892 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000893 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000894 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000895 while True:
896 # Check for task to run first, then wait.
897 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000898 if not self.exceptions.empty():
899 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000901 self._flush_terminated_threads()
902 if (not self.queued and not self.running or
903 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000904 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000905 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000906
907 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000908 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000909 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000910 if (self.ignore_requirements or
911 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700912 if not self._is_conflict(self.queued[i]):
913 # Start one work item: all its requirements are satisfied.
914 self._run_one_task(self.queued.pop(i), args, kwargs)
915 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000916 else:
917 # Couldn't find an item that could run. Break out the outher loop.
918 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000919
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000920 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000921 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000922 break
923 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000924 try:
925 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000926 # If we haven't printed to terminal for a while, but we have received
927 # spew from a suprocess, let the user know we're still progressing.
928 now = datetime.datetime.now()
929 if (now - self.last_join > datetime.timedelta(seconds=60) and
930 self.last_subproc_output > self.last_join):
931 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000932 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000933 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000934 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000935 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000936 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000937 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000938 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000939 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000940 except KeyboardInterrupt:
941 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000942 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000943 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000944 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
945 self.ran), len(self.running)),
946 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000947 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000948 print(
949 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
950 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000951 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000952 print(
953 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000954 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000955 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000956 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000957 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000958
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000959 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000960 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000961 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000962 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000963 # To get back the stack location correctly, the raise a, b, c form must be
964 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000965 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000966 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
967 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000968 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000969 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000970
maruel@chromium.org3742c842010-09-09 19:27:14 +0000971 def _flush_terminated_threads(self):
972 """Flush threads that have terminated."""
973 running = self.running
974 self.running = []
975 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000976 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000977 self.running.append(t)
978 else:
979 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000980 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000981 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000982 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000983 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000984 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000985 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000986 if t.item.name in self.ran:
987 raise Error(
988 'gclient is confused, "%s" is already in "%s"' % (
989 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000990 if not t.item.name in self.ran:
991 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000992
993 def _run_one_task(self, task_item, args, kwargs):
994 if self.jobs > 1:
995 # Start the thread.
996 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000997 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000998 self.running.append(new_thread)
999 new_thread.start()
1000 else:
1001 # Run the 'thread' inside the main thread. Don't try to catch any
1002 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001003 try:
1004 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001005 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001006 task_item.run(*args, **kwargs)
1007 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001008 print(
1009 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001010 self.ran.append(task_item.name)
1011 if self.verbose:
1012 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001013 print('')
1014 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001015 if self.progress:
1016 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1017 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +00001018 print(
1019 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001020 raise
1021 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001022 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001023 raise
1024
maruel@chromium.org3742c842010-09-09 19:27:14 +00001025
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001026 class _Worker(threading.Thread):
1027 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001028 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001029 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001030 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001031 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001032 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001033 self.args = args
1034 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001035 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001036
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001037 def run(self):
1038 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001039 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001040 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001041 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001042 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001043 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001044 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001045 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001046 print(
1047 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001048 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001049 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001050 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001051 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001052 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001053 except Exception:
1054 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001055 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001056 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001057 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001058 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001059 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001060 work_queue.ready_cond.acquire()
1061 try:
1062 work_queue.ready_cond.notifyAll()
1063 finally:
1064 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001065
1066
agable92bec4f2016-08-24 09:27:27 -07001067def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001068 """Returns the most plausible editor to use.
1069
1070 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001071 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001072 - core.editor git configuration variable (if supplied by git-cl)
1073 - VISUAL environment variable
1074 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001075 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001076
1077 In the case of git-cl, this matches git's behaviour, except that it does not
1078 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001079 """
agable92bec4f2016-08-24 09:27:27 -07001080 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001081 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001082 editor = os.environ.get('VISUAL')
1083 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001084 editor = os.environ.get('EDITOR')
1085 if not editor:
1086 if sys.platform.startswith('win'):
1087 editor = 'notepad'
1088 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001089 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001090 return editor
1091
1092
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001093def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001094 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001095 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001096 # Make sure CRLF is handled properly by requiring none.
1097 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001098 print(
1099 '!! Please remove \\r from your change description !!', file=sys.stderr)
sokcevic07152802021-08-18 00:06:34 +00001100 fileobj = os.fdopen(file_handle, 'wb')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001101 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001102 content = re.sub('\r?\n', '\n', content)
1103 # Some editors complain when the file doesn't end in \n.
1104 if not content.endswith('\n'):
1105 content += '\n'
sokcevic07152802021-08-18 00:06:34 +00001106 fileobj.write(content.encode('utf-8'))
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001107 fileobj.close()
1108
1109 try:
agable92bec4f2016-08-24 09:27:27 -07001110 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001111 if not editor:
1112 return None
1113 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001114 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1115 # Msysgit requires the usage of 'env' to be present.
1116 cmd = 'env ' + cmd
1117 try:
1118 # shell=True to allow the shell to handle all forms of quotes in
1119 # $EDITOR.
1120 subprocess2.check_call(cmd, shell=True)
1121 except subprocess2.CalledProcessError:
1122 return None
1123 return FileRead(filename)
1124 finally:
1125 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001126
1127
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001128def UpgradeToHttps(url):
1129 """Upgrades random urls to https://.
1130
1131 Do not touch unknown urls like ssh:// or git://.
1132 Do not touch http:// urls with a port number,
1133 Fixes invalid GAE url.
1134 """
1135 if not url:
1136 return url
1137 if not re.match(r'[a-z\-]+\://.*', url):
1138 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1139 # relative url and will use http:///foo. Note that it defaults to http://
1140 # for compatibility with naked url like "localhost:8080".
1141 url = 'http://%s' % url
1142 parsed = list(urlparse.urlparse(url))
1143 # Do not automatically upgrade http to https if a port number is provided.
1144 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1145 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001146 return urlparse.urlunparse(parsed)
1147
1148
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001149def ParseCodereviewSettingsContent(content):
1150 """Process a codereview.settings file properly."""
1151 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1152 try:
1153 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1154 except ValueError:
1155 raise Error(
1156 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001157 def fix_url(key):
1158 if keyvals.get(key):
1159 keyvals[key] = UpgradeToHttps(keyvals[key])
1160 fix_url('CODE_REVIEW_SERVER')
1161 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001162 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001163
1164
1165def NumLocalCpus():
1166 """Returns the number of processors.
1167
dnj@chromium.org530523b2015-01-07 19:54:57 +00001168 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1169 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1170 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001171 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001172 # Surround the entire thing in try/except; no failure here should stop gclient
1173 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001174 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001175 # Use multiprocessing to get CPU count. This may raise
1176 # NotImplementedError.
1177 try:
1178 import multiprocessing
1179 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001180 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001181 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001182 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001183 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1184 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1185
1186 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1187 if 'NUMBER_OF_PROCESSORS' in os.environ:
1188 return int(os.environ['NUMBER_OF_PROCESSORS'])
1189 except Exception as e:
1190 logging.exception("Exception raised while probing CPU count: %s", e)
1191
1192 logging.debug('Failed to get CPU count. Defaulting to 1.')
1193 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001194
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001195
szager@chromium.orgfc616382014-03-18 20:32:04 +00001196def DefaultDeltaBaseCacheLimit():
1197 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1198
1199 The primary constraint is the address space of virtual memory. The cache
1200 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1201 parameter is set too high.
1202 """
1203 if platform.architecture()[0].startswith('64'):
1204 return '2g'
1205 else:
1206 return '512m'
1207
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001208
szager@chromium.orgff113292014-03-25 06:02:08 +00001209def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001210 """Return reasonable default values for configuring git-index-pack.
1211
1212 Experiments suggest that higher values for pack.threads don't improve
1213 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001214 cache_limit = DefaultDeltaBaseCacheLimit()
1215 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
Ayu Ishii09858612020-06-26 18:00:52 +00001216 if url in THREADED_INDEX_PACK_BLOCKLIST:
szager@chromium.orgff113292014-03-25 06:02:08 +00001217 result.extend(['-c', 'pack.threads=1'])
1218 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001219
1220
1221def FindExecutable(executable):
1222 """This mimics the "which" utility."""
1223 path_folders = os.environ.get('PATH').split(os.pathsep)
1224
1225 for path_folder in path_folders:
1226 target = os.path.join(path_folder, executable)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001227 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001228 target = os.path.abspath(os.path.expanduser(target))
1229 if os.path.isfile(target) and os.access(target, os.X_OK):
1230 return target
1231 if sys.platform.startswith('win'):
1232 for suffix in ('.bat', '.cmd', '.exe'):
1233 alt_target = target + suffix
1234 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1235 return alt_target
1236 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001237
1238
1239def freeze(obj):
1240 """Takes a generic object ``obj``, and returns an immutable version of it.
1241
1242 Supported types:
1243 * dict / OrderedDict -> FrozenDict
1244 * list -> tuple
1245 * set -> frozenset
1246 * any object with a working __hash__ implementation (assumes that hashable
1247 means immutable)
1248
1249 Will raise TypeError if you pass an object which is not hashable.
1250 """
Raul Tambre6693d092020-02-19 20:36:45 +00001251 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001252 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001253 elif isinstance(obj, (list, tuple)):
1254 return tuple(freeze(i) for i in obj)
1255 elif isinstance(obj, set):
1256 return frozenset(freeze(i) for i in obj)
1257 else:
1258 hash(obj)
1259 return obj
1260
1261
Raul Tambre6693d092020-02-19 20:36:45 +00001262class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001263 """An immutable OrderedDict.
1264
1265 Modified From: http://stackoverflow.com/a/2704866
1266 """
1267 def __init__(self, *args, **kwargs):
1268 self._d = collections.OrderedDict(*args, **kwargs)
1269
1270 # Calculate the hash immediately so that we know all the items are
1271 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001272 self._hash = functools.reduce(
1273 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001274
1275 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001276 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001277 return NotImplemented
1278 if self is other:
1279 return True
1280 if len(self) != len(other):
1281 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001282 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001283 if k not in other or other[k] != v:
1284 return False
1285 return True
1286
1287 def __iter__(self):
1288 return iter(self._d)
1289
1290 def __len__(self):
1291 return len(self._d)
1292
1293 def __getitem__(self, key):
1294 return self._d[key]
1295
1296 def __hash__(self):
1297 return self._hash
1298
1299 def __repr__(self):
1300 return 'FrozenDict(%r)' % (self._d.items(),)