blob: fb6770870a1ef733b6fb32757b0ff278721b7dec [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'):
174 # Always decodes output to a Unicode string.
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000175 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
Edward Lemur419c92f2019-10-25 22:17:49 +0000176 if mode == 'rbU' and sys.version_info.major == 3:
177 mode = 'rb'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000178 with open(filename, mode=mode) as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000179 s = f.read()
Edward Lemur419c92f2019-10-25 22:17:49 +0000180 if isinstance(s, bytes):
181 return s.decode('utf-8', 'replace')
182 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000183
184
Edward Lemur1773f372020-02-22 00:27:14 +0000185def FileWrite(filename, content, mode='w', encoding='utf-8'):
186 with codecs.open(filename, mode=mode, encoding=encoding) as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000188
189
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000190@contextlib.contextmanager
191def temporary_directory(**kwargs):
192 tdir = tempfile.mkdtemp(**kwargs)
193 try:
194 yield tdir
195 finally:
196 if tdir:
197 rmtree(tdir)
198
199
Edward Lemur1773f372020-02-22 00:27:14 +0000200@contextlib.contextmanager
201def temporary_file():
202 """Creates a temporary file.
203
204 On Windows, a file must be closed before it can be opened again. This function
205 allows to write something like:
206
207 with gclient_utils.temporary_file() as tmp:
208 gclient_utils.FileWrite(tmp, foo)
209 useful_stuff(tmp)
210
211 Instead of something like:
212
213 with tempfile.NamedTemporaryFile(delete=False) as tmp:
214 tmp.write(foo)
215 tmp.close()
216 try:
217 useful_stuff(tmp)
218 finally:
219 os.remove(tmp.name)
220 """
221 handle, name = tempfile.mkstemp()
222 os.close(handle)
223 try:
224 yield name
225 finally:
226 os.remove(name)
227
228
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000229def safe_rename(old, new):
230 """Renames a file reliably.
231
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000232 Sometimes os.rename does not work because a dying git process keeps a handle
233 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000234 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000235 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000236 """
237 # roughly 10s
238 retries = 100
239 for i in range(retries):
240 try:
241 os.rename(old, new)
242 break
243 except OSError:
244 if i == (retries - 1):
245 # Give up.
246 raise
247 # retry
248 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
249 time.sleep(0.1)
250
251
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000252def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000253 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000254 os.remove(path)
255 else:
256 rmtree(path)
257
258
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000259def rmtree(path):
260 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000261
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000262 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000263
264 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700265 are read-only. We need to be able to force the files to be writable (i.e.,
266 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000267
268 Even with all this, Windows still sometimes fails to delete a file, citing
269 a permission error (maybe something to do with antivirus scans or disk
270 indexing). The best suggestion any of the user forums had was to wait a
271 bit and try again, so we do that too. It's hand-waving, but sometimes it
272 works. :/
273
274 On POSIX systems, things are a little bit simpler. The modes of the files
275 to be deleted doesn't matter, only the modes of the directories containing
276 them are significant. As the directory tree is traversed, each directory
277 has its mode set appropriately before descending into it. This should
278 result in the entire tree being removed, with the possible exception of
279 *path itself, because nothing attempts to change the mode of its parent.
280 Doing so would be hazardous, as it's not a directory slated for removal.
281 In the ordinary case, this is not a problem: for our purposes, the user
282 will never lack write permission on *path's parent.
283 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000284 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000285 return
286
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000287 if os.path.islink(path) or not os.path.isdir(path):
288 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000289
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000290 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000291 # Give up and use cmd.exe's rd command.
292 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000293 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000294 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
295 if exitcode == 0:
296 return
297 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000298 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000299 time.sleep(3)
300 raise Exception('Failed to remove path %s' % path)
301
302 # On POSIX systems, we need the x-bit set on the directory to access it,
303 # the r-bit to see its contents, and the w-bit to remove files from it.
304 # The actual modes of the files within the directory is irrelevant.
305 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000306
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000307 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000308 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000309
310 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000311 # If fullpath is a symbolic link that points to a directory, isdir will
312 # be True, but we don't want to descend into that as a directory, we just
313 # want to remove the link. Check islink and treat links as ordinary files
314 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000315 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000316 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000317 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000318 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000319 # Recurse.
320 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000321
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000322 remove(os.rmdir, path)
323
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000324
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000325def safe_makedirs(tree):
326 """Creates the directory in a safe manner.
327
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000328 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000329 exception and pass on.
330 """
331 count = 0
332 while not os.path.exists(tree):
333 count += 1
334 try:
335 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000336 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000337 # 17 POSIX, 183 Windows
338 if e.errno not in (17, 183):
339 raise
340 if count > 40:
341 # Give up.
342 raise
343
344
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000345def CommandToStr(args):
346 """Converts an arg list into a shell escaped string."""
347 return ' '.join(pipes.quote(arg) for arg in args)
348
349
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000350class Wrapper(object):
351 """Wraps an object, acting as a transparent proxy for all properties by
352 default.
353 """
354 def __init__(self, wrapped):
355 self._wrapped = wrapped
356
357 def __getattr__(self, name):
358 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000359
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000360
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000361class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000362 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000363 def __init__(self, wrapped, delay):
364 super(AutoFlush, self).__init__(wrapped)
365 if not hasattr(self, 'lock'):
366 self.lock = threading.Lock()
367 self.__last_flushed_at = time.time()
368 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000369
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000370 @property
371 def autoflush(self):
372 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000373
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000374 def write(self, out, *args, **kwargs):
375 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000376 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000378 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000379 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000380 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000381 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000382 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000383 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000384 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000385 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000386
387
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000388class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000389 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000390 threads with a NN> prefix.
391 """
392 def __init__(self, wrapped, include_zero=False):
393 super(Annotated, self).__init__(wrapped)
394 if not hasattr(self, 'lock'):
395 self.lock = threading.Lock()
396 self.__output_buffers = {}
397 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000398 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000399
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000400 @property
401 def annotated(self):
402 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000403
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000404 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000405 # Store as bytes to ensure Unicode characters get output correctly.
406 if not isinstance(out, bytes):
407 out = out.encode('utf-8')
408
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000409 index = getattr(threading.currentThread(), 'index', 0)
410 if not index and not self.__include_zero:
411 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000412 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000413
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000414 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000415 try:
416 # Use a dummy array to hold the string so the code can be lockless.
417 # Strings are immutable, requiring to keep a lock for the whole dictionary
418 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000419 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000420 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000421 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000422 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000423 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000424 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000425
426 # Continue lockless.
427 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000428 while True:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000429 cr_loc = obj[0].find(b'\r')
430 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000431 if cr_loc == lf_loc == -1:
432 break
433 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000434 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000435 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000436 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000437 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000438 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000439 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000440 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000441 obj[0] = remaining
442
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000443 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000444 """Flush buffered output."""
445 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000446 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000447 try:
448 # Detect threads no longer existing.
449 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000450 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000451 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000452 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000453 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000454 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000455 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000456 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000457 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000458
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000459 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000460 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000461 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000462 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000463 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000464
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000465
466def MakeFileAutoFlush(fileobj, delay=10):
467 autoflush = getattr(fileobj, 'autoflush', None)
468 if autoflush:
469 autoflush.delay = delay
470 return fileobj
471 return AutoFlush(fileobj, delay)
472
473
474def MakeFileAnnotated(fileobj, include_zero=False):
475 if getattr(fileobj, 'annotated', None):
476 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000477 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000478
479
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000480GCLIENT_CHILDREN = []
481GCLIENT_CHILDREN_LOCK = threading.Lock()
482
483
484class GClientChildren(object):
485 @staticmethod
486 def add(popen_obj):
487 with GCLIENT_CHILDREN_LOCK:
488 GCLIENT_CHILDREN.append(popen_obj)
489
490 @staticmethod
491 def remove(popen_obj):
492 with GCLIENT_CHILDREN_LOCK:
493 GCLIENT_CHILDREN.remove(popen_obj)
494
495 @staticmethod
496 def _attemptToKillChildren():
497 global GCLIENT_CHILDREN
498 with GCLIENT_CHILDREN_LOCK:
499 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
500
501 for zombie in zombies:
502 try:
503 zombie.kill()
504 except OSError:
505 pass
506
507 with GCLIENT_CHILDREN_LOCK:
508 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
509
510 @staticmethod
511 def _areZombies():
512 with GCLIENT_CHILDREN_LOCK:
513 return bool(GCLIENT_CHILDREN)
514
515 @staticmethod
516 def KillAllRemainingChildren():
517 GClientChildren._attemptToKillChildren()
518
519 if GClientChildren._areZombies():
520 time.sleep(0.5)
521 GClientChildren._attemptToKillChildren()
522
523 with GCLIENT_CHILDREN_LOCK:
524 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000525 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000526 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000527 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000528
529
Edward Lemur24146be2019-08-01 21:44:52 +0000530def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
531 show_header=False, always_show_header=False, retry=False,
532 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000533 """Runs a command and calls back a filter function if needed.
534
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000535 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000536 print_stdout: If True, the command's stdout is forwarded to stdout.
537 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000538 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000539 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000540 show_header: Whether to display a header before the command output.
541 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000542 retry: If the process exits non-zero, sleep for a brief interval and try
543 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000544
545 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000546
547 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000548 """
Edward Lemur24146be2019-08-01 21:44:52 +0000549 def show_header_if_necessary(needs_header, attempt):
550 """Show the header at most once."""
551 if not needs_header[0]:
552 return
553
554 needs_header[0] = False
555 # Automatically generated header. We only prepend a newline if
556 # always_show_header is false, since it usually indicates there's an
557 # external progress display, and it's better not to clobber it in that case.
558 header = '' if always_show_header else '\n'
559 header += '________ running \'%s\' in \'%s\'' % (
560 ' '.join(args), kwargs.get('cwd', '.'))
561 if attempt:
562 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
563 header += '\n'
564
565 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000566 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
567 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000568 if filter_fn:
569 filter_fn(header)
570
571 def filter_line(command_output, line_start):
572 """Extract the last line from command output and filter it."""
573 if not filter_fn or line_start is None:
574 return
575 command_output.seek(line_start)
576 filter_fn(command_output.read().decode('utf-8'))
577
578 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
579 # byte inputs and sys.stdout.buffer must be used instead.
580 if print_stdout:
581 sys.stdout.flush()
582 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
583 else:
584 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000585
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000586 sleep_interval = RETRY_INITIAL_SLEEP
587 run_cwd = kwargs.get('cwd', os.getcwd())
Josip Sokcevic740825e2021-05-12 18:28:34 +0000588
589 # Store the output of the command regardless of the value of print_stdout or
590 # filter_fn.
591 command_output = io.BytesIO()
Edward Lemur24146be2019-08-01 21:44:52 +0000592 for attempt in range(RETRY_MAX + 1):
Ben Pastened410c662020-08-26 17:07:03 +0000593 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
594 # subprocess when filtering its output. This makes the subproc believe
595 # it was launched from a terminal, which will preserve ANSI color codes.
Milad Fad949c912020-09-18 00:26:08 +0000596 os_type = GetMacWinAixOrLinux()
597 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
Ben Pastened410c662020-08-26 17:07:03 +0000598 pipe_reader, pipe_writer = os.openpty()
599 else:
600 pipe_reader, pipe_writer = os.pipe()
601
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000602 kid = subprocess2.Popen(
Ben Pastened410c662020-08-26 17:07:03 +0000603 args, bufsize=0, stdout=pipe_writer, stderr=subprocess2.STDOUT,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000604 **kwargs)
Ben Pastened410c662020-08-26 17:07:03 +0000605 # Close the write end of the pipe once we hand it off to the child proc.
606 os.close(pipe_writer)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000607
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000608 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000609
Edward Lemur24146be2019-08-01 21:44:52 +0000610 # Passed as a list for "by ref" semantics.
611 needs_header = [show_header]
612 if always_show_header:
613 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000614
615 # Also, we need to forward stdout to prevent weird re-ordering of output.
616 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700617 # normally buffering is done for each line, but if the process requests
618 # input, no end-of-line character is output after the prompt and it would
619 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000620 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000621 line_start = None
622 while True:
Ben Pastened410c662020-08-26 17:07:03 +0000623 try:
624 in_byte = os.read(pipe_reader, 1)
625 except (IOError, OSError) as e:
626 if e.errno == errno.EIO:
627 # An errno.EIO means EOF?
628 in_byte = None
629 else:
630 raise e
Edward Lemur24146be2019-08-01 21:44:52 +0000631 is_newline = in_byte in (b'\n', b'\r')
632 if not in_byte:
633 break
634
635 show_header_if_necessary(needs_header, attempt)
636
637 if is_newline:
638 filter_line(command_output, line_start)
639 line_start = None
640 elif line_start is None:
641 line_start = command_output.tell()
642
643 stdout_write(in_byte)
644 command_output.write(in_byte)
645
646 # Flush the rest of buffered output.
647 sys.stdout.flush()
648 if line_start is not None:
649 filter_line(command_output, line_start)
650
Ben Pastened410c662020-08-26 17:07:03 +0000651 os.close(pipe_reader)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000652 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000653
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000654 # Don't put this in a 'finally,' since the child may still run if we get
655 # an exception.
656 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000657
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000658 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000659 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000660 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000661
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000662 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000663 return command_output.getvalue()
664
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000665 if not retry:
666 break
Edward Lemur24146be2019-08-01 21:44:52 +0000667
Raul Tambreb946b232019-03-26 14:48:46 +0000668 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
669 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
Josip Sokcevic740825e2021-05-12 18:28:34 +0000670 command_output = io.BytesIO()
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000671 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000672 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000673
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000674 raise subprocess2.CalledProcessError(
Josip Sokcevic740825e2021-05-12 18:28:34 +0000675 rv, args, kwargs.get('cwd', None), command_output.getvalue(), None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000676
677
agable@chromium.org5a306a22014-02-24 22:13:59 +0000678class GitFilter(object):
679 """A filter_fn implementation for quieting down git output messages.
680
681 Allows a custom function to skip certain lines (predicate), and will throttle
682 the output of percentage completed lines to only output every X seconds.
683 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000684 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000685
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000686 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000687 """
688 Args:
689 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
690 XX% complete messages) to only be printed at least |time_throttle|
691 seconds apart.
692 predicate (f(line)): An optional function which is invoked for every line.
693 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000694 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000695 """
Edward Lemur24146be2019-08-01 21:44:52 +0000696 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000697 self.last_time = 0
698 self.time_throttle = time_throttle
699 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000700 self.out_fh = out_fh or sys.stdout
701 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000702
703 def __call__(self, line):
704 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000705 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000706 if esc > -1:
707 line = line[:esc]
708 if self.predicate and not self.predicate(line):
709 return
710 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000711 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000712 if match:
713 if match.group(1) != self.progress_prefix:
714 self.progress_prefix = match.group(1)
715 elif now - self.last_time < self.time_throttle:
716 return
717 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000718 if not self.first_line:
719 self.out_fh.write('[%s] ' % Elapsed())
720 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000721 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000722
723
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000724def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000725 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000726
rcui@google.com13595ff2011-10-13 01:25:07 +0000727 Returns nearest upper-level directory with the passed in file.
728 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000729 if not path:
730 path = os.getcwd()
731 path = os.path.realpath(path)
732 while True:
733 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000734 if os.path.exists(file_path):
735 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000736 (new_path, _) = os.path.split(path)
737 if new_path == path:
738 return None
739 path = new_path
740
741
Milad Fa52fdd1f2020-09-15 21:24:46 +0000742def GetMacWinAixOrLinux():
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000743 """Returns 'mac', 'win', or 'linux', matching the current platform."""
744 if sys.platform.startswith(('cygwin', 'win')):
745 return 'win'
746 elif sys.platform.startswith('linux'):
747 return 'linux'
748 elif sys.platform == 'darwin':
749 return 'mac'
Milad Fa52fdd1f2020-09-15 21:24:46 +0000750 elif sys.platform.startswith('aix'):
751 return 'aix'
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000752 raise Error('Unknown platform: ' + sys.platform)
753
754
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000755def GetGClientRootAndEntries(path=None):
756 """Returns the gclient root and the dict of entries."""
757 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000758 root = FindFileUpwards(config_file, path)
759 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000760 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000761 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000762 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000763 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000764 with open(config_path) as config:
765 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000766 config_dir = os.path.dirname(config_path)
767 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000768
769
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000770def lockedmethod(method):
771 """Method decorator that holds self.lock for the duration of the call."""
772 def inner(self, *args, **kwargs):
773 try:
774 try:
775 self.lock.acquire()
776 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000777 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000778 raise
779 return method(self, *args, **kwargs)
780 finally:
781 self.lock.release()
782 return inner
783
784
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000785class WorkItem(object):
786 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000787 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
788 # As a workaround, use a single lock. Yep you read it right. Single lock for
789 # all the 100 objects.
790 lock = threading.Lock()
791
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000792 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000793 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000794 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000795 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000796 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700797 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000798
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000799 def run(self, work_queue):
800 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000801 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000802 pass
803
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000804 @property
805 def name(self):
806 return self._name
807
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000808
809class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000810 """Runs a set of WorkItem that have interdependencies and were WorkItem are
811 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000812
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200813 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000814 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000815
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000816 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000817 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000818 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000819 """jobs specifies the number of concurrent tasks to allow. progress is a
820 Progress instance."""
821 # Set when a thread is done or a new item is enqueued.
822 self.ready_cond = threading.Condition()
823 # Maximum number of concurrent tasks.
824 self.jobs = jobs
825 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000826 self.queued = []
827 # List of strings representing each Dependency.name that was run.
828 self.ran = []
829 # List of items currently running.
830 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000831 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000832 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000833 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000834 self.progress = progress
835 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000836 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000837
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000838 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000839 self.verbose = verbose
840 self.last_join = None
841 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000842
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000843 def enqueue(self, d):
844 """Enqueue one Dependency to be executed later once its requirements are
845 satisfied.
846 """
847 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000848 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000849 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000850 self.queued.append(d)
851 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000852 if self.jobs == 1:
853 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000854 logging.debug('enqueued(%s)' % d.name)
855 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000856 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000857 self.progress.update(0)
858 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000859 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000860 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000861
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000862 def out_cb(self, _):
863 self.last_subproc_output = datetime.datetime.now()
864 return True
865
866 @staticmethod
867 def format_task_output(task, comment=''):
868 if comment:
869 comment = ' (%s)' % comment
870 if task.start and task.finish:
871 elapsed = ' (Elapsed: %s)' % (
872 str(task.finish - task.start).partition('.')[0])
873 else:
874 elapsed = ''
875 return """
876%s%s%s
877----------------------------------------
878%s
879----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000880 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000881
hinoka885e5b12016-06-08 14:40:09 -0700882 def _is_conflict(self, job):
883 """Checks to see if a job will conflict with another running job."""
884 for running_job in self.running:
885 for used_resource in running_job.item.resources:
886 logging.debug('Checking resource %s' % used_resource)
887 if used_resource in job.resources:
888 return True
889 return False
890
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000891 def flush(self, *args, **kwargs):
892 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000893 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000894 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000895 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000896 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000897 while True:
898 # Check for task to run first, then wait.
899 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000900 if not self.exceptions.empty():
901 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000902 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000903 self._flush_terminated_threads()
904 if (not self.queued and not self.running or
905 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000906 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000907 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000908
909 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000910 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000911 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000912 if (self.ignore_requirements or
913 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700914 if not self._is_conflict(self.queued[i]):
915 # Start one work item: all its requirements are satisfied.
916 self._run_one_task(self.queued.pop(i), args, kwargs)
917 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000918 else:
919 # Couldn't find an item that could run. Break out the outher loop.
920 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000921
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000922 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000923 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000924 break
925 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000926 try:
927 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000928 # If we haven't printed to terminal for a while, but we have received
929 # spew from a suprocess, let the user know we're still progressing.
930 now = datetime.datetime.now()
931 if (now - self.last_join > datetime.timedelta(seconds=60) and
932 self.last_subproc_output > self.last_join):
933 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000934 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000935 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000936 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000937 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000938 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000939 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000940 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000941 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000942 except KeyboardInterrupt:
943 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000944 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000945 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000946 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
947 self.ran), len(self.running)),
948 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000949 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000950 print(
951 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
952 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000953 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000954 print(
955 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000956 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000957 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000958 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000959 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000960
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000961 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000962 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000963 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000964 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000965 # To get back the stack location correctly, the raise a, b, c form must be
966 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000967 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000968 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
969 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000970 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000971 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000972
maruel@chromium.org3742c842010-09-09 19:27:14 +0000973 def _flush_terminated_threads(self):
974 """Flush threads that have terminated."""
975 running = self.running
976 self.running = []
977 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000978 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000979 self.running.append(t)
980 else:
981 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000982 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000983 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000984 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000985 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000986 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000987 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000988 if t.item.name in self.ran:
989 raise Error(
990 'gclient is confused, "%s" is already in "%s"' % (
991 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000992 if not t.item.name in self.ran:
993 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000994
995 def _run_one_task(self, task_item, args, kwargs):
996 if self.jobs > 1:
997 # Start the thread.
998 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000999 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001000 self.running.append(new_thread)
1001 new_thread.start()
1002 else:
1003 # Run the 'thread' inside the main thread. Don't try to catch any
1004 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001005 try:
1006 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001007 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001008 task_item.run(*args, **kwargs)
1009 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001010 print(
1011 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001012 self.ran.append(task_item.name)
1013 if self.verbose:
1014 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001015 print('')
1016 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001017 if self.progress:
1018 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1019 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +00001020 print(
1021 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001022 raise
1023 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001024 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001025 raise
1026
maruel@chromium.org3742c842010-09-09 19:27:14 +00001027
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001028 class _Worker(threading.Thread):
1029 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001030 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001031 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001032 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001033 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001034 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001035 self.args = args
1036 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001037 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001038
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001039 def run(self):
1040 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001041 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001042 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001043 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001044 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001045 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001046 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001047 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001048 print(
1049 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
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:
Raul Tambreb946b232019-03-26 14:48:46 +00001100 print(
1101 '!! Please remove \\r from your change description !!', file=sys.stderr)
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]
Ayu Ishii09858612020-06-26 18:00:52 +00001218 if url in THREADED_INDEX_PACK_BLOCKLIST:
szager@chromium.orgff113292014-03-25 06:02:08 +00001219 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)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001229 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001230 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
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001239
1240
1241def freeze(obj):
1242 """Takes a generic object ``obj``, and returns an immutable version of it.
1243
1244 Supported types:
1245 * dict / OrderedDict -> FrozenDict
1246 * list -> tuple
1247 * set -> frozenset
1248 * any object with a working __hash__ implementation (assumes that hashable
1249 means immutable)
1250
1251 Will raise TypeError if you pass an object which is not hashable.
1252 """
Raul Tambre6693d092020-02-19 20:36:45 +00001253 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001254 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001255 elif isinstance(obj, (list, tuple)):
1256 return tuple(freeze(i) for i in obj)
1257 elif isinstance(obj, set):
1258 return frozenset(freeze(i) for i in obj)
1259 else:
1260 hash(obj)
1261 return obj
1262
1263
Raul Tambre6693d092020-02-19 20:36:45 +00001264class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001265 """An immutable OrderedDict.
1266
1267 Modified From: http://stackoverflow.com/a/2704866
1268 """
1269 def __init__(self, *args, **kwargs):
1270 self._d = collections.OrderedDict(*args, **kwargs)
1271
1272 # Calculate the hash immediately so that we know all the items are
1273 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001274 self._hash = functools.reduce(
1275 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001276
1277 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001278 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001279 return NotImplemented
1280 if self is other:
1281 return True
1282 if len(self) != len(other):
1283 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001284 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001285 if k not in other or other[k] != v:
1286 return False
1287 return True
1288
1289 def __iter__(self):
1290 return iter(self._d)
1291
1292 def __len__(self):
1293 return len(self._d)
1294
1295 def __getitem__(self, key):
1296 return self._d[key]
1297
1298 def __hash__(self):
1299 return self._hash
1300
1301 def __repr__(self):
1302 return 'FrozenDict(%r)' % (self._d.items(),)