blob: bbf273d1144d512793fea9e45c6d0ec014196e47 [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
Joanna Wang1a977bd2022-06-02 21:51:17 +0000114def ExtractRefName(remote, full_refs_str):
115 """Returns the ref name if full_refs_str is a valid ref."""
116 result = re.compile(r'^refs(\/.+)?\/((%s)|(heads)|(tags))\/(?P<ref_name>.+)' %
117 remote).match(full_refs_str)
118 if result:
119 return result.group('ref_name')
120 return None
121
122
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000123def IsGitSha(revision):
124 """Returns true if the given string is a valid hex-encoded sha"""
125 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
126
127
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200128def IsFullGitSha(revision):
129 """Returns true if the given string is a valid hex-encoded full sha"""
130 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
131
132
floitsch@google.comeaab7842011-04-28 09:07:58 +0000133def IsDateRevision(revision):
134 """Returns true if the given revision is of the form "{ ... }"."""
135 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
136
137
138def MakeDateRevision(date):
139 """Returns a revision representing the latest revision before the given
140 date."""
141 return "{" + date + "}"
142
143
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000144def SyntaxErrorToError(filename, e):
145 """Raises a gclient_utils.Error exception with the human readable message"""
146 try:
147 # Try to construct a human readable error message
148 if filename:
149 error_message = 'There is a syntax error in %s\n' % filename
150 else:
151 error_message = 'There is a syntax error\n'
152 error_message += 'Line #%s, character %s: "%s"' % (
153 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
154 except:
155 # Something went wrong, re-raise the original exception
156 raise e
157 else:
158 raise Error(error_message)
159
160
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000161class PrintableObject(object):
162 def __str__(self):
163 output = ''
164 for i in dir(self):
165 if i.startswith('__'):
166 continue
167 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
168 return output
169
170
Edward Lesmesae3586b2020-03-23 21:21:14 +0000171def AskForData(message):
Christian Flache6855432021-12-01 08:10:05 +0000172 # Try to load the readline module, so that "elaborate line editing" features
173 # such as backspace work for `raw_input` / `input`.
174 try:
175 import readline
176 except ImportError:
177 # The readline module does not exist in all Python distributions, e.g. on
178 # Windows. Fall back to simple input handling.
179 pass
180
Edward Lesmesae3586b2020-03-23 21:21:14 +0000181 # Use this so that it can be mocked in tests on Python 2 and 3.
182 try:
183 if sys.version_info.major == 2:
184 return raw_input(message)
185 return input(message)
186 except KeyboardInterrupt:
187 # Hide the exception.
188 sys.exit(1)
189
190
Edward Lemur419c92f2019-10-25 22:17:49 +0000191def FileRead(filename, mode='rbU'):
Josip Sokcevic8b3bc252021-06-04 18:44:11 +0000192 # mode is ignored now; we always return unicode strings.
193 with open(filename, mode='rb') as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000194 s = f.read()
Josip Sokcevic8b3bc252021-06-04 18:44:11 +0000195 try:
196 return s.decode('utf-8', 'replace')
197 except (UnicodeDecodeError, AttributeError):
Edward Lemur419c92f2019-10-25 22:17:49 +0000198 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000199
200
Edward Lemur1773f372020-02-22 00:27:14 +0000201def FileWrite(filename, content, mode='w', encoding='utf-8'):
202 with codecs.open(filename, mode=mode, encoding=encoding) as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000203 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000204
205
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000206@contextlib.contextmanager
207def temporary_directory(**kwargs):
208 tdir = tempfile.mkdtemp(**kwargs)
209 try:
210 yield tdir
211 finally:
212 if tdir:
213 rmtree(tdir)
214
215
Edward Lemur1773f372020-02-22 00:27:14 +0000216@contextlib.contextmanager
217def temporary_file():
218 """Creates a temporary file.
219
220 On Windows, a file must be closed before it can be opened again. This function
221 allows to write something like:
222
223 with gclient_utils.temporary_file() as tmp:
224 gclient_utils.FileWrite(tmp, foo)
225 useful_stuff(tmp)
226
227 Instead of something like:
228
229 with tempfile.NamedTemporaryFile(delete=False) as tmp:
230 tmp.write(foo)
231 tmp.close()
232 try:
233 useful_stuff(tmp)
234 finally:
235 os.remove(tmp.name)
236 """
237 handle, name = tempfile.mkstemp()
238 os.close(handle)
239 try:
240 yield name
241 finally:
242 os.remove(name)
243
244
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000245def safe_rename(old, new):
246 """Renames a file reliably.
247
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000248 Sometimes os.rename does not work because a dying git process keeps a handle
249 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000250 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000251 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000252 """
253 # roughly 10s
254 retries = 100
255 for i in range(retries):
256 try:
257 os.rename(old, new)
258 break
259 except OSError:
260 if i == (retries - 1):
261 # Give up.
262 raise
263 # retry
264 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
265 time.sleep(0.1)
266
267
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000268def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000269 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000270 os.remove(path)
271 else:
272 rmtree(path)
273
274
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000275def rmtree(path):
276 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000277
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000278 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000279
280 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700281 are read-only. We need to be able to force the files to be writable (i.e.,
282 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000283
284 Even with all this, Windows still sometimes fails to delete a file, citing
285 a permission error (maybe something to do with antivirus scans or disk
286 indexing). The best suggestion any of the user forums had was to wait a
287 bit and try again, so we do that too. It's hand-waving, but sometimes it
288 works. :/
289
290 On POSIX systems, things are a little bit simpler. The modes of the files
291 to be deleted doesn't matter, only the modes of the directories containing
292 them are significant. As the directory tree is traversed, each directory
293 has its mode set appropriately before descending into it. This should
294 result in the entire tree being removed, with the possible exception of
295 *path itself, because nothing attempts to change the mode of its parent.
296 Doing so would be hazardous, as it's not a directory slated for removal.
297 In the ordinary case, this is not a problem: for our purposes, the user
298 will never lack write permission on *path's parent.
299 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000300 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000301 return
302
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000303 if os.path.islink(path) or not os.path.isdir(path):
304 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000305
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000306 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000307 # Give up and use cmd.exe's rd command.
308 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000309 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000310 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
311 if exitcode == 0:
312 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000313
314 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000315 time.sleep(3)
316 raise Exception('Failed to remove path %s' % path)
317
318 # On POSIX systems, we need the x-bit set on the directory to access it,
319 # the r-bit to see its contents, and the w-bit to remove files from it.
320 # The actual modes of the files within the directory is irrelevant.
321 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000322
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000323 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000324 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000325
326 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000327 # If fullpath is a symbolic link that points to a directory, isdir will
328 # be True, but we don't want to descend into that as a directory, we just
329 # want to remove the link. Check islink and treat links as ordinary files
330 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000331 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000332 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000333 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000334 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000335 # Recurse.
336 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000337
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000338 remove(os.rmdir, path)
339
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000340
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000341def safe_makedirs(tree):
342 """Creates the directory in a safe manner.
343
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000344 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000345 exception and pass on.
346 """
347 count = 0
348 while not os.path.exists(tree):
349 count += 1
350 try:
351 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000352 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000353 # 17 POSIX, 183 Windows
354 if e.errno not in (17, 183):
355 raise
356 if count > 40:
357 # Give up.
358 raise
359
360
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000361def CommandToStr(args):
362 """Converts an arg list into a shell escaped string."""
363 return ' '.join(pipes.quote(arg) for arg in args)
364
365
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000366class Wrapper(object):
367 """Wraps an object, acting as a transparent proxy for all properties by
368 default.
369 """
370 def __init__(self, wrapped):
371 self._wrapped = wrapped
372
373 def __getattr__(self, name):
374 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000375
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000376
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000378 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000379 def __init__(self, wrapped, delay):
380 super(AutoFlush, self).__init__(wrapped)
381 if not hasattr(self, 'lock'):
382 self.lock = threading.Lock()
383 self.__last_flushed_at = time.time()
384 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000385
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000386 @property
387 def autoflush(self):
388 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000389
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000390 def write(self, out, *args, **kwargs):
391 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000392 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000393 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000394 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000395 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000396 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000397 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000398 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000399 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000400 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000401 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000402
403
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000404class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000405 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000406 threads with a NN> prefix.
407 """
408 def __init__(self, wrapped, include_zero=False):
409 super(Annotated, self).__init__(wrapped)
410 if not hasattr(self, 'lock'):
411 self.lock = threading.Lock()
412 self.__output_buffers = {}
413 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000414 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000415
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000416 @property
417 def annotated(self):
418 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000419
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000420 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000421 # Store as bytes to ensure Unicode characters get output correctly.
422 if not isinstance(out, bytes):
423 out = out.encode('utf-8')
424
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000425 index = getattr(threading.currentThread(), 'index', 0)
426 if not index and not self.__include_zero:
427 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000428 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000429
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000430 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000431 try:
432 # Use a dummy array to hold the string so the code can be lockless.
433 # Strings are immutable, requiring to keep a lock for the whole dictionary
434 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000435 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000436 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000437 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000438 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000439 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000440 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000441
442 # Continue lockless.
443 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000444 while True:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000445 cr_loc = obj[0].find(b'\r')
446 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000447 if cr_loc == lf_loc == -1:
448 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000449
450 if cr_loc == -1 or (0 <= lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000451 line, remaining = obj[0].split(b'\n', 1)
Josip Sokcevic42c5bbb2022-01-24 21:42:28 +0000452 if line:
453 self._wrapped_write(b'%d>%s\n' % (index, line))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000454 elif lf_loc == -1 or (0 <= cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000455 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000456 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000457 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000458 obj[0] = remaining
459
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000460 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000461 """Flush buffered output."""
462 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000463 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000464 try:
465 # Detect threads no longer existing.
466 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000467 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000468 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000469 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000470 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000471 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000472 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000473 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000474 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000475
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000476 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000477 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000478 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000479 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000480 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000481
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000482
483def MakeFileAutoFlush(fileobj, delay=10):
484 autoflush = getattr(fileobj, 'autoflush', None)
485 if autoflush:
486 autoflush.delay = delay
487 return fileobj
488 return AutoFlush(fileobj, delay)
489
490
491def MakeFileAnnotated(fileobj, include_zero=False):
492 if getattr(fileobj, 'annotated', None):
493 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000494 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000495
496
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000497GCLIENT_CHILDREN = []
498GCLIENT_CHILDREN_LOCK = threading.Lock()
499
500
501class GClientChildren(object):
502 @staticmethod
503 def add(popen_obj):
504 with GCLIENT_CHILDREN_LOCK:
505 GCLIENT_CHILDREN.append(popen_obj)
506
507 @staticmethod
508 def remove(popen_obj):
509 with GCLIENT_CHILDREN_LOCK:
510 GCLIENT_CHILDREN.remove(popen_obj)
511
512 @staticmethod
513 def _attemptToKillChildren():
514 global GCLIENT_CHILDREN
515 with GCLIENT_CHILDREN_LOCK:
516 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
517
518 for zombie in zombies:
519 try:
520 zombie.kill()
521 except OSError:
522 pass
523
524 with GCLIENT_CHILDREN_LOCK:
525 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
526
527 @staticmethod
528 def _areZombies():
529 with GCLIENT_CHILDREN_LOCK:
530 return bool(GCLIENT_CHILDREN)
531
532 @staticmethod
533 def KillAllRemainingChildren():
534 GClientChildren._attemptToKillChildren()
535
536 if GClientChildren._areZombies():
537 time.sleep(0.5)
538 GClientChildren._attemptToKillChildren()
539
540 with GCLIENT_CHILDREN_LOCK:
541 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000542 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000543 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000544 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000545
546
Edward Lemur24146be2019-08-01 21:44:52 +0000547def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
548 show_header=False, always_show_header=False, retry=False,
549 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000550 """Runs a command and calls back a filter function if needed.
551
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000552 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000553 print_stdout: If True, the command's stdout is forwarded to stdout.
554 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000555 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000556 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000557 show_header: Whether to display a header before the command output.
558 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000559 retry: If the process exits non-zero, sleep for a brief interval and try
560 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000561
562 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000563
564 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000565 """
Edward Lemur24146be2019-08-01 21:44:52 +0000566 def show_header_if_necessary(needs_header, attempt):
567 """Show the header at most once."""
568 if not needs_header[0]:
569 return
570
571 needs_header[0] = False
572 # Automatically generated header. We only prepend a newline if
573 # always_show_header is false, since it usually indicates there's an
574 # external progress display, and it's better not to clobber it in that case.
575 header = '' if always_show_header else '\n'
576 header += '________ running \'%s\' in \'%s\'' % (
577 ' '.join(args), kwargs.get('cwd', '.'))
578 if attempt:
579 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
580 header += '\n'
581
582 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000583 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
584 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000585 if filter_fn:
586 filter_fn(header)
587
588 def filter_line(command_output, line_start):
589 """Extract the last line from command output and filter it."""
590 if not filter_fn or line_start is None:
591 return
592 command_output.seek(line_start)
593 filter_fn(command_output.read().decode('utf-8'))
594
595 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
596 # byte inputs and sys.stdout.buffer must be used instead.
597 if print_stdout:
598 sys.stdout.flush()
599 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
600 else:
601 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000602
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000603 sleep_interval = RETRY_INITIAL_SLEEP
604 run_cwd = kwargs.get('cwd', os.getcwd())
Josip Sokcevic740825e2021-05-12 18:28:34 +0000605
606 # Store the output of the command regardless of the value of print_stdout or
607 # filter_fn.
608 command_output = io.BytesIO()
Edward Lemur24146be2019-08-01 21:44:52 +0000609 for attempt in range(RETRY_MAX + 1):
Ben Pastened410c662020-08-26 17:07:03 +0000610 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
611 # subprocess when filtering its output. This makes the subproc believe
612 # it was launched from a terminal, which will preserve ANSI color codes.
Milad Fad949c912020-09-18 00:26:08 +0000613 os_type = GetMacWinAixOrLinux()
614 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
Ben Pastened410c662020-08-26 17:07:03 +0000615 pipe_reader, pipe_writer = os.openpty()
616 else:
617 pipe_reader, pipe_writer = os.pipe()
618
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000619 kid = subprocess2.Popen(
Ben Pastened410c662020-08-26 17:07:03 +0000620 args, bufsize=0, stdout=pipe_writer, stderr=subprocess2.STDOUT,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000621 **kwargs)
Ben Pastened410c662020-08-26 17:07:03 +0000622 # Close the write end of the pipe once we hand it off to the child proc.
623 os.close(pipe_writer)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000624
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000625 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000626
Edward Lemur24146be2019-08-01 21:44:52 +0000627 # Passed as a list for "by ref" semantics.
628 needs_header = [show_header]
629 if always_show_header:
630 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000631
632 # Also, we need to forward stdout to prevent weird re-ordering of output.
633 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700634 # normally buffering is done for each line, but if the process requests
635 # input, no end-of-line character is output after the prompt and it would
636 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000637 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000638 line_start = None
639 while True:
Ben Pastened410c662020-08-26 17:07:03 +0000640 try:
641 in_byte = os.read(pipe_reader, 1)
642 except (IOError, OSError) as e:
643 if e.errno == errno.EIO:
644 # An errno.EIO means EOF?
645 in_byte = None
646 else:
647 raise e
Edward Lemur24146be2019-08-01 21:44:52 +0000648 is_newline = in_byte in (b'\n', b'\r')
649 if not in_byte:
650 break
651
652 show_header_if_necessary(needs_header, attempt)
653
654 if is_newline:
655 filter_line(command_output, line_start)
656 line_start = None
657 elif line_start is None:
658 line_start = command_output.tell()
659
660 stdout_write(in_byte)
661 command_output.write(in_byte)
662
663 # Flush the rest of buffered output.
664 sys.stdout.flush()
665 if line_start is not None:
666 filter_line(command_output, line_start)
667
Ben Pastened410c662020-08-26 17:07:03 +0000668 os.close(pipe_reader)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000669 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000670
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000671 # Don't put this in a 'finally,' since the child may still run if we get
672 # an exception.
673 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000674
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000675 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000676 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000677 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000678
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000679 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000680 return command_output.getvalue()
681
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000682 if not retry:
683 break
Edward Lemur24146be2019-08-01 21:44:52 +0000684
Raul Tambreb946b232019-03-26 14:48:46 +0000685 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
686 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
Josip Sokcevic740825e2021-05-12 18:28:34 +0000687 command_output = io.BytesIO()
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000688 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000689 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000690
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000691 raise subprocess2.CalledProcessError(
Josip Sokcevic740825e2021-05-12 18:28:34 +0000692 rv, args, kwargs.get('cwd', None), command_output.getvalue(), None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000693
694
agable@chromium.org5a306a22014-02-24 22:13:59 +0000695class GitFilter(object):
696 """A filter_fn implementation for quieting down git output messages.
697
698 Allows a custom function to skip certain lines (predicate), and will throttle
699 the output of percentage completed lines to only output every X seconds.
700 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000701 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000702
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000703 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000704 """
705 Args:
706 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
707 XX% complete messages) to only be printed at least |time_throttle|
708 seconds apart.
709 predicate (f(line)): An optional function which is invoked for every line.
710 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000711 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000712 """
Edward Lemur24146be2019-08-01 21:44:52 +0000713 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000714 self.last_time = 0
715 self.time_throttle = time_throttle
716 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000717 self.out_fh = out_fh or sys.stdout
718 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000719
720 def __call__(self, line):
721 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000722 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000723 if esc > -1:
724 line = line[:esc]
725 if self.predicate and not self.predicate(line):
726 return
727 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000728 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000729 if match:
730 if match.group(1) != self.progress_prefix:
731 self.progress_prefix = match.group(1)
732 elif now - self.last_time < self.time_throttle:
733 return
734 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000735 if not self.first_line:
736 self.out_fh.write('[%s] ' % Elapsed())
737 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000738 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000739
740
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000741def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000742 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000743
rcui@google.com13595ff2011-10-13 01:25:07 +0000744 Returns nearest upper-level directory with the passed in file.
745 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000746 if not path:
747 path = os.getcwd()
748 path = os.path.realpath(path)
749 while True:
750 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000751 if os.path.exists(file_path):
752 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000753 (new_path, _) = os.path.split(path)
754 if new_path == path:
755 return None
756 path = new_path
757
758
Milad Fa52fdd1f2020-09-15 21:24:46 +0000759def GetMacWinAixOrLinux():
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000760 """Returns 'mac', 'win', or 'linux', matching the current platform."""
761 if sys.platform.startswith(('cygwin', 'win')):
762 return 'win'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000763
764 if sys.platform.startswith('linux'):
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000765 return 'linux'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000766
767 if sys.platform == 'darwin':
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000768 return 'mac'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000769
770 if sys.platform.startswith('aix'):
Milad Fa52fdd1f2020-09-15 21:24:46 +0000771 return 'aix'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000772
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000773 raise Error('Unknown platform: ' + sys.platform)
774
775
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000776def GetGClientRootAndEntries(path=None):
777 """Returns the gclient root and the dict of entries."""
778 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000779 root = FindFileUpwards(config_file, path)
780 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000781 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000782 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000783 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000784 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000785 with open(config_path) as config:
786 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000787 config_dir = os.path.dirname(config_path)
788 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000789
790
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000791def lockedmethod(method):
792 """Method decorator that holds self.lock for the duration of the call."""
793 def inner(self, *args, **kwargs):
794 try:
795 try:
796 self.lock.acquire()
797 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000798 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000799 raise
800 return method(self, *args, **kwargs)
801 finally:
802 self.lock.release()
803 return inner
804
805
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000806class WorkItem(object):
807 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000808 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
809 # As a workaround, use a single lock. Yep you read it right. Single lock for
810 # all the 100 objects.
811 lock = threading.Lock()
812
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000813 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000814 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000815 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000816 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000817 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700818 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000819
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000820 def run(self, work_queue):
821 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000822 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000823
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000824 @property
825 def name(self):
826 return self._name
827
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000828
829class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000830 """Runs a set of WorkItem that have interdependencies and were WorkItem are
831 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000832
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200833 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000834 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000835
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000836 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000837 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000838 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000839 """jobs specifies the number of concurrent tasks to allow. progress is a
840 Progress instance."""
841 # Set when a thread is done or a new item is enqueued.
842 self.ready_cond = threading.Condition()
843 # Maximum number of concurrent tasks.
844 self.jobs = jobs
845 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000846 self.queued = []
847 # List of strings representing each Dependency.name that was run.
848 self.ran = []
849 # List of items currently running.
850 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000851 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000852 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000853 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000854 self.progress = progress
855 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000856 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000857
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000858 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000859 self.verbose = verbose
860 self.last_join = None
861 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000862
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000863 def enqueue(self, d):
864 """Enqueue one Dependency to be executed later once its requirements are
865 satisfied.
866 """
867 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000868 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000869 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000870 self.queued.append(d)
871 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000872 if self.jobs == 1:
873 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000874 logging.debug('enqueued(%s)' % d.name)
875 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000876 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000877 self.progress.update(0)
878 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000879 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000880 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000881
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000882 def out_cb(self, _):
883 self.last_subproc_output = datetime.datetime.now()
884 return True
885
886 @staticmethod
887 def format_task_output(task, comment=''):
888 if comment:
889 comment = ' (%s)' % comment
890 if task.start and task.finish:
891 elapsed = ' (Elapsed: %s)' % (
892 str(task.finish - task.start).partition('.')[0])
893 else:
894 elapsed = ''
895 return """
896%s%s%s
897----------------------------------------
898%s
899----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000900 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000901
hinoka885e5b12016-06-08 14:40:09 -0700902 def _is_conflict(self, job):
903 """Checks to see if a job will conflict with another running job."""
904 for running_job in self.running:
905 for used_resource in running_job.item.resources:
906 logging.debug('Checking resource %s' % used_resource)
907 if used_resource in job.resources:
908 return True
909 return False
910
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000911 def flush(self, *args, **kwargs):
912 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000913 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000914 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000915 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000916 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000917 while True:
918 # Check for task to run first, then wait.
919 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000920 if not self.exceptions.empty():
921 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000922 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000923 self._flush_terminated_threads()
924 if (not self.queued and not self.running or
925 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000926 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000927 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000928
929 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000930 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000931 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000932 if (self.ignore_requirements or
933 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700934 if not self._is_conflict(self.queued[i]):
935 # Start one work item: all its requirements are satisfied.
936 self._run_one_task(self.queued.pop(i), args, kwargs)
937 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000938 else:
939 # Couldn't find an item that could run. Break out the outher loop.
940 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000941
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000942 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000943 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000944 break
945 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000946 try:
947 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000948 # If we haven't printed to terminal for a while, but we have received
949 # spew from a suprocess, let the user know we're still progressing.
950 now = datetime.datetime.now()
951 if (now - self.last_join > datetime.timedelta(seconds=60) and
952 self.last_subproc_output > self.last_join):
953 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000954 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000955 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000956 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000957 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000958 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000959 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000960 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000961 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000962 except KeyboardInterrupt:
963 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000964 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000965 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000966 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
967 self.ran), len(self.running)),
968 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000969 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000970 print(
971 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
972 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000973 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000974 print(
975 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000976 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000977 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000978 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000979 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000980
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000981 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000982 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000983 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000984 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000985 # To get back the stack location correctly, the raise a, b, c form must be
986 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000987 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000988 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
989 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000990 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000991 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000992
maruel@chromium.org3742c842010-09-09 19:27:14 +0000993 def _flush_terminated_threads(self):
994 """Flush threads that have terminated."""
995 running = self.running
996 self.running = []
997 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000998 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000999 self.running.append(t)
1000 else:
1001 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001002 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +00001003 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001004 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +00001005 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +00001006 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +00001007 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +00001008 if t.item.name in self.ran:
1009 raise Error(
1010 'gclient is confused, "%s" is already in "%s"' % (
1011 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +00001012 if not t.item.name in self.ran:
1013 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001014
1015 def _run_one_task(self, task_item, args, kwargs):
1016 if self.jobs > 1:
1017 # Start the thread.
1018 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001019 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001020 self.running.append(new_thread)
1021 new_thread.start()
1022 else:
1023 # Run the 'thread' inside the main thread. Don't try to catch any
1024 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001025 try:
1026 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001027 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001028 task_item.run(*args, **kwargs)
1029 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001030 print(
1031 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001032 self.ran.append(task_item.name)
1033 if self.verbose:
1034 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001035 print('')
1036 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001037 if self.progress:
1038 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1039 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +00001040 print(
1041 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001042 raise
1043 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001044 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001045 raise
1046
maruel@chromium.org3742c842010-09-09 19:27:14 +00001047
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001048 class _Worker(threading.Thread):
1049 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001050 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001051 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001052 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001053 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001054 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001055 self.args = args
1056 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001057 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001058
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001059 def run(self):
1060 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001061 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001062 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001063 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001064 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001065 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001066 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001067 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001068 print(
1069 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001070 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001071 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001072 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001073 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001074 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001075 except Exception:
1076 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001077 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001078 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001079 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001080 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001081 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001082 work_queue.ready_cond.acquire()
1083 try:
1084 work_queue.ready_cond.notifyAll()
1085 finally:
1086 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001087
1088
agable92bec4f2016-08-24 09:27:27 -07001089def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001090 """Returns the most plausible editor to use.
1091
1092 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001093 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001094 - core.editor git configuration variable (if supplied by git-cl)
1095 - VISUAL environment variable
1096 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001097 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001098
1099 In the case of git-cl, this matches git's behaviour, except that it does not
1100 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001101 """
agable92bec4f2016-08-24 09:27:27 -07001102 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001103 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001104 editor = os.environ.get('VISUAL')
1105 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001106 editor = os.environ.get('EDITOR')
1107 if not editor:
1108 if sys.platform.startswith('win'):
1109 editor = 'notepad'
1110 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001111 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001112 return editor
1113
1114
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001115def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001116 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001117 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001118 # Make sure CRLF is handled properly by requiring none.
1119 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001120 print(
1121 '!! Please remove \\r from your change description !!', file=sys.stderr)
sokcevic07152802021-08-18 00:06:34 +00001122 fileobj = os.fdopen(file_handle, 'wb')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001123 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001124 content = re.sub('\r?\n', '\n', content)
1125 # Some editors complain when the file doesn't end in \n.
1126 if not content.endswith('\n'):
1127 content += '\n'
sokcevic07152802021-08-18 00:06:34 +00001128 fileobj.write(content.encode('utf-8'))
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001129 fileobj.close()
1130
1131 try:
agable92bec4f2016-08-24 09:27:27 -07001132 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001133 if not editor:
1134 return None
1135 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001136 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1137 # Msysgit requires the usage of 'env' to be present.
1138 cmd = 'env ' + cmd
1139 try:
1140 # shell=True to allow the shell to handle all forms of quotes in
1141 # $EDITOR.
1142 subprocess2.check_call(cmd, shell=True)
1143 except subprocess2.CalledProcessError:
1144 return None
1145 return FileRead(filename)
1146 finally:
1147 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001148
1149
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001150def UpgradeToHttps(url):
1151 """Upgrades random urls to https://.
1152
1153 Do not touch unknown urls like ssh:// or git://.
1154 Do not touch http:// urls with a port number,
1155 Fixes invalid GAE url.
1156 """
1157 if not url:
1158 return url
1159 if not re.match(r'[a-z\-]+\://.*', url):
1160 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1161 # relative url and will use http:///foo. Note that it defaults to http://
1162 # for compatibility with naked url like "localhost:8080".
1163 url = 'http://%s' % url
1164 parsed = list(urlparse.urlparse(url))
1165 # Do not automatically upgrade http to https if a port number is provided.
1166 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1167 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001168 return urlparse.urlunparse(parsed)
1169
1170
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001171def ParseCodereviewSettingsContent(content):
1172 """Process a codereview.settings file properly."""
1173 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1174 try:
1175 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1176 except ValueError:
1177 raise Error(
1178 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001179 def fix_url(key):
1180 if keyvals.get(key):
1181 keyvals[key] = UpgradeToHttps(keyvals[key])
1182 fix_url('CODE_REVIEW_SERVER')
1183 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001184 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001185
1186
1187def NumLocalCpus():
1188 """Returns the number of processors.
1189
dnj@chromium.org530523b2015-01-07 19:54:57 +00001190 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1191 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1192 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001193 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001194 # Surround the entire thing in try/except; no failure here should stop gclient
1195 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001196 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001197 # Use multiprocessing to get CPU count. This may raise
1198 # NotImplementedError.
1199 try:
1200 import multiprocessing
1201 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001202 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001203 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001204 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001205 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1206 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1207
1208 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1209 if 'NUMBER_OF_PROCESSORS' in os.environ:
1210 return int(os.environ['NUMBER_OF_PROCESSORS'])
1211 except Exception as e:
1212 logging.exception("Exception raised while probing CPU count: %s", e)
1213
1214 logging.debug('Failed to get CPU count. Defaulting to 1.')
1215 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001216
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001217
szager@chromium.orgfc616382014-03-18 20:32:04 +00001218def DefaultDeltaBaseCacheLimit():
1219 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1220
1221 The primary constraint is the address space of virtual memory. The cache
1222 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1223 parameter is set too high.
1224 """
1225 if platform.architecture()[0].startswith('64'):
1226 return '2g'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001227
1228 return '512m'
szager@chromium.orgfc616382014-03-18 20:32:04 +00001229
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001230
szager@chromium.orgff113292014-03-25 06:02:08 +00001231def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001232 """Return reasonable default values for configuring git-index-pack.
1233
1234 Experiments suggest that higher values for pack.threads don't improve
1235 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001236 cache_limit = DefaultDeltaBaseCacheLimit()
1237 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
Ayu Ishii09858612020-06-26 18:00:52 +00001238 if url in THREADED_INDEX_PACK_BLOCKLIST:
szager@chromium.orgff113292014-03-25 06:02:08 +00001239 result.extend(['-c', 'pack.threads=1'])
1240 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001241
1242
1243def FindExecutable(executable):
1244 """This mimics the "which" utility."""
1245 path_folders = os.environ.get('PATH').split(os.pathsep)
1246
1247 for path_folder in path_folders:
1248 target = os.path.join(path_folder, executable)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001249 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001250 target = os.path.abspath(os.path.expanduser(target))
1251 if os.path.isfile(target) and os.access(target, os.X_OK):
1252 return target
1253 if sys.platform.startswith('win'):
1254 for suffix in ('.bat', '.cmd', '.exe'):
1255 alt_target = target + suffix
1256 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1257 return alt_target
1258 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001259
1260
1261def freeze(obj):
1262 """Takes a generic object ``obj``, and returns an immutable version of it.
1263
1264 Supported types:
1265 * dict / OrderedDict -> FrozenDict
1266 * list -> tuple
1267 * set -> frozenset
1268 * any object with a working __hash__ implementation (assumes that hashable
1269 means immutable)
1270
1271 Will raise TypeError if you pass an object which is not hashable.
1272 """
Raul Tambre6693d092020-02-19 20:36:45 +00001273 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001274 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001275
1276 if isinstance(obj, (list, tuple)):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001277 return tuple(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001278
1279 if isinstance(obj, set):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001280 return frozenset(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001281
1282 hash(obj)
1283 return obj
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001284
1285
Raul Tambre6693d092020-02-19 20:36:45 +00001286class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001287 """An immutable OrderedDict.
1288
1289 Modified From: http://stackoverflow.com/a/2704866
1290 """
1291 def __init__(self, *args, **kwargs):
1292 self._d = collections.OrderedDict(*args, **kwargs)
1293
1294 # Calculate the hash immediately so that we know all the items are
1295 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001296 self._hash = functools.reduce(
1297 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001298
1299 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001300 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001301 return NotImplemented
1302 if self is other:
1303 return True
1304 if len(self) != len(other):
1305 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001306 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001307 if k not in other or other[k] != v:
1308 return False
1309 return True
1310
1311 def __iter__(self):
1312 return iter(self._d)
1313
1314 def __len__(self):
1315 return len(self._d)
1316
1317 def __getitem__(self, key):
1318 return self._d[key]
1319
1320 def __hash__(self):
1321 return self._hash
1322
1323 def __repr__(self):
1324 return 'FrozenDict(%r)' % (self._d.items(),)