blob: 45617dfdf3011475e08f417188185b7f8708da0d [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
Raul Tambreb946b232019-03-26 14:48:46 +000013import functools
14import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000015import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020016import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000018import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000019import platform
Raul Tambreb946b232019-03-26 14:48:46 +000020
21try:
22 import Queue as queue
23except ImportError: # For Py3 compatibility
24 import queue
25
msb@chromium.orgac915bb2009-11-13 17:03:01 +000026import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000027import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000028import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000031import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000032import time
Raul Tambreb946b232019-03-26 14:48:46 +000033
34try:
35 import urlparse
36except ImportError: # For Py3 compatibility
37 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000038
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000039import subprocess2
40
Raul Tambreb946b232019-03-26 14:48:46 +000041if sys.version_info.major == 2:
42 from cStringIO import StringIO
43else:
44 from io import StringIO
45
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000046
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000047RETRY_MAX = 3
48RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000049START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000050
51
borenet@google.com6a9b1682014-03-24 18:35:23 +000052_WARNINGS = []
53
54
szager@chromium.orgff113292014-03-25 06:02:08 +000055# These repos are known to cause OOM errors on 32-bit platforms, due the the
56# very large objects they contain. It is not safe to use threaded index-pack
57# when cloning/fetching them.
58THREADED_INDEX_PACK_BLACKLIST = [
59 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
60]
61
Raul Tambreb946b232019-03-26 14:48:46 +000062"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
63if sys.version_info.major == 2:
64 # We have to use exec to avoid a SyntaxError in Python 3.
65 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
66else:
67 def reraise(typ, value, tb=None):
68 if value is None:
69 value = typ()
70 if value.__traceback__ is not tb:
71 raise value.with_traceback(tb)
72 raise value
73
szager@chromium.orgff113292014-03-25 06:02:08 +000074
maruel@chromium.org66c83e62010-09-07 14:18:45 +000075class Error(Exception):
76 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000077 def __init__(self, msg, *args, **kwargs):
78 index = getattr(threading.currentThread(), 'index', 0)
79 if index:
80 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
81 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000082
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000083
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000084def Elapsed(until=None):
85 if until is None:
86 until = datetime.datetime.now()
87 return str(until - START).partition('.')[0]
88
89
borenet@google.com6a9b1682014-03-24 18:35:23 +000090def PrintWarnings():
91 """Prints any accumulated warnings."""
92 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000093 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000094 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000095 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000096
97
98def AddWarning(msg):
99 """Adds the given warning message to the list of accumulated warnings."""
100 _WARNINGS.append(msg)
101
102
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000103def SplitUrlRevision(url):
104 """Splits url and returns a two-tuple: url, rev"""
105 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000106 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000107 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000108 components = re.search(regex, url).groups()
109 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000110 components = url.rsplit('@', 1)
111 if re.match(r'^\w+\@', url) and '@' not in components[0]:
112 components = [url]
113
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000114 if len(components) == 1:
115 components += [None]
116 return tuple(components)
117
118
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000119def IsGitSha(revision):
120 """Returns true if the given string is a valid hex-encoded sha"""
121 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
122
123
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200124def IsFullGitSha(revision):
125 """Returns true if the given string is a valid hex-encoded full sha"""
126 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
127
128
floitsch@google.comeaab7842011-04-28 09:07:58 +0000129def IsDateRevision(revision):
130 """Returns true if the given revision is of the form "{ ... }"."""
131 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
132
133
134def MakeDateRevision(date):
135 """Returns a revision representing the latest revision before the given
136 date."""
137 return "{" + date + "}"
138
139
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000140def SyntaxErrorToError(filename, e):
141 """Raises a gclient_utils.Error exception with the human readable message"""
142 try:
143 # Try to construct a human readable error message
144 if filename:
145 error_message = 'There is a syntax error in %s\n' % filename
146 else:
147 error_message = 'There is a syntax error\n'
148 error_message += 'Line #%s, character %s: "%s"' % (
149 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
150 except:
151 # Something went wrong, re-raise the original exception
152 raise e
153 else:
154 raise Error(error_message)
155
156
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000157class PrintableObject(object):
158 def __str__(self):
159 output = ''
160 for i in dir(self):
161 if i.startswith('__'):
162 continue
163 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
164 return output
165
166
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000167def FileRead(filename, mode='rU'):
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000168 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
169 if mode == 'rU' and sys.version_info.major == 3:
170 mode = 'r'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000171 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000172 # codecs.open() has different behavior than open() on python 2.6 so use
173 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000174 s = f.read()
175 try:
176 return s.decode('utf-8')
Raul Tambreb946b232019-03-26 14:48:46 +0000177 # AttributeError is for Py3 compatibility
178 except (UnicodeDecodeError, AttributeError):
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000179 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000180
181
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000182def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000183 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000184 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000185
186
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000187@contextlib.contextmanager
188def temporary_directory(**kwargs):
189 tdir = tempfile.mkdtemp(**kwargs)
190 try:
191 yield tdir
192 finally:
193 if tdir:
194 rmtree(tdir)
195
196
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000197def safe_rename(old, new):
198 """Renames a file reliably.
199
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000200 Sometimes os.rename does not work because a dying git process keeps a handle
201 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000202 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000203 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000204 """
205 # roughly 10s
206 retries = 100
207 for i in range(retries):
208 try:
209 os.rename(old, new)
210 break
211 except OSError:
212 if i == (retries - 1):
213 # Give up.
214 raise
215 # retry
216 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
217 time.sleep(0.1)
218
219
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000220def rm_file_or_tree(path):
221 if os.path.isfile(path):
222 os.remove(path)
223 else:
224 rmtree(path)
225
226
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000227def rmtree(path):
228 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000229
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000230 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000231
232 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700233 are read-only. We need to be able to force the files to be writable (i.e.,
234 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000235
236 Even with all this, Windows still sometimes fails to delete a file, citing
237 a permission error (maybe something to do with antivirus scans or disk
238 indexing). The best suggestion any of the user forums had was to wait a
239 bit and try again, so we do that too. It's hand-waving, but sometimes it
240 works. :/
241
242 On POSIX systems, things are a little bit simpler. The modes of the files
243 to be deleted doesn't matter, only the modes of the directories containing
244 them are significant. As the directory tree is traversed, each directory
245 has its mode set appropriately before descending into it. This should
246 result in the entire tree being removed, with the possible exception of
247 *path itself, because nothing attempts to change the mode of its parent.
248 Doing so would be hazardous, as it's not a directory slated for removal.
249 In the ordinary case, this is not a problem: for our purposes, the user
250 will never lack write permission on *path's parent.
251 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000252 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000253 return
254
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000255 if os.path.islink(path) or not os.path.isdir(path):
256 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000257
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000258 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000259 # Give up and use cmd.exe's rd command.
260 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000261 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000262 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
263 if exitcode == 0:
264 return
265 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000266 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000267 time.sleep(3)
268 raise Exception('Failed to remove path %s' % path)
269
270 # On POSIX systems, we need the x-bit set on the directory to access it,
271 # the r-bit to see its contents, and the w-bit to remove files from it.
272 # The actual modes of the files within the directory is irrelevant.
273 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000275 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000276 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000277
278 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000279 # If fullpath is a symbolic link that points to a directory, isdir will
280 # be True, but we don't want to descend into that as a directory, we just
281 # want to remove the link. Check islink and treat links as ordinary files
282 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000283 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000284 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000285 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000287 # Recurse.
288 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000289
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000290 remove(os.rmdir, path)
291
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000292
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000293def safe_makedirs(tree):
294 """Creates the directory in a safe manner.
295
296 Because multiple threads can create these directories concurently, trap the
297 exception and pass on.
298 """
299 count = 0
300 while not os.path.exists(tree):
301 count += 1
302 try:
303 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000304 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000305 # 17 POSIX, 183 Windows
306 if e.errno not in (17, 183):
307 raise
308 if count > 40:
309 # Give up.
310 raise
311
312
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000313def CommandToStr(args):
314 """Converts an arg list into a shell escaped string."""
315 return ' '.join(pipes.quote(arg) for arg in args)
316
317
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000318class Wrapper(object):
319 """Wraps an object, acting as a transparent proxy for all properties by
320 default.
321 """
322 def __init__(self, wrapped):
323 self._wrapped = wrapped
324
325 def __getattr__(self, name):
326 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000327
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000328
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000329class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000330 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000331 def __init__(self, wrapped, delay):
332 super(AutoFlush, self).__init__(wrapped)
333 if not hasattr(self, 'lock'):
334 self.lock = threading.Lock()
335 self.__last_flushed_at = time.time()
336 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000337
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000338 @property
339 def autoflush(self):
340 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000341
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000342 def write(self, out, *args, **kwargs):
343 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000344 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000345 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000346 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000348 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000349 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000350 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000351 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000352 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000353 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000354
355
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000356class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000357 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000358 threads with a NN> prefix.
359 """
360 def __init__(self, wrapped, include_zero=False):
361 super(Annotated, self).__init__(wrapped)
362 if not hasattr(self, 'lock'):
363 self.lock = threading.Lock()
364 self.__output_buffers = {}
365 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000366
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000367 @property
368 def annotated(self):
369 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000370
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000371 def write(self, out):
372 index = getattr(threading.currentThread(), 'index', 0)
373 if not index and not self.__include_zero:
374 # Unindexed threads aren't buffered.
375 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000376
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000378 try:
379 # Use a dummy array to hold the string so the code can be lockless.
380 # Strings are immutable, requiring to keep a lock for the whole dictionary
381 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 if not index in self.__output_buffers:
383 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000384 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000385 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000386 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000388
389 # Continue lockless.
390 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000391 while True:
392 # TODO(agable): find both of these with a single pass.
393 cr_loc = obj[0].find('\r')
394 lf_loc = obj[0].find('\n')
395 if cr_loc == lf_loc == -1:
396 break
397 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
398 line, remaining = obj[0].split('\n', 1)
399 if line:
400 self._wrapped.write('%d>%s\n' % (index, line))
401 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
402 line, remaining = obj[0].split('\r', 1)
403 if line:
404 self._wrapped.write('%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000405 obj[0] = remaining
406
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000408 """Flush buffered output."""
409 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000410 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000411 try:
412 # Detect threads no longer existing.
413 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000414 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000415 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000416 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000417 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000418 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000419 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000420 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000421 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000422
423 # Don't keep the lock while writting. Will append \n when it shouldn't.
424 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000425 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000426 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
427 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000428
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000429
430def MakeFileAutoFlush(fileobj, delay=10):
431 autoflush = getattr(fileobj, 'autoflush', None)
432 if autoflush:
433 autoflush.delay = delay
434 return fileobj
435 return AutoFlush(fileobj, delay)
436
437
438def MakeFileAnnotated(fileobj, include_zero=False):
439 if getattr(fileobj, 'annotated', None):
440 return fileobj
441 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000442
443
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000444GCLIENT_CHILDREN = []
445GCLIENT_CHILDREN_LOCK = threading.Lock()
446
447
448class GClientChildren(object):
449 @staticmethod
450 def add(popen_obj):
451 with GCLIENT_CHILDREN_LOCK:
452 GCLIENT_CHILDREN.append(popen_obj)
453
454 @staticmethod
455 def remove(popen_obj):
456 with GCLIENT_CHILDREN_LOCK:
457 GCLIENT_CHILDREN.remove(popen_obj)
458
459 @staticmethod
460 def _attemptToKillChildren():
461 global GCLIENT_CHILDREN
462 with GCLIENT_CHILDREN_LOCK:
463 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
464
465 for zombie in zombies:
466 try:
467 zombie.kill()
468 except OSError:
469 pass
470
471 with GCLIENT_CHILDREN_LOCK:
472 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
473
474 @staticmethod
475 def _areZombies():
476 with GCLIENT_CHILDREN_LOCK:
477 return bool(GCLIENT_CHILDREN)
478
479 @staticmethod
480 def KillAllRemainingChildren():
481 GClientChildren._attemptToKillChildren()
482
483 if GClientChildren._areZombies():
484 time.sleep(0.5)
485 GClientChildren._attemptToKillChildren()
486
487 with GCLIENT_CHILDREN_LOCK:
488 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000489 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000490 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000491 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000492
493
Edward Lemur24146be2019-08-01 21:44:52 +0000494def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
495 show_header=False, always_show_header=False, retry=False,
496 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000497 """Runs a command and calls back a filter function if needed.
498
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000499 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000500 print_stdout: If True, the command's stdout is forwarded to stdout.
501 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000502 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000503 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000504 show_header: Whether to display a header before the command output.
505 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000506 retry: If the process exits non-zero, sleep for a brief interval and try
507 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000508
509 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000510
511 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000512 """
Edward Lemur24146be2019-08-01 21:44:52 +0000513 def show_header_if_necessary(needs_header, attempt):
514 """Show the header at most once."""
515 if not needs_header[0]:
516 return
517
518 needs_header[0] = False
519 # Automatically generated header. We only prepend a newline if
520 # always_show_header is false, since it usually indicates there's an
521 # external progress display, and it's better not to clobber it in that case.
522 header = '' if always_show_header else '\n'
523 header += '________ running \'%s\' in \'%s\'' % (
524 ' '.join(args), kwargs.get('cwd', '.'))
525 if attempt:
526 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
527 header += '\n'
528
529 if print_stdout:
530 sys.stdout.write(header)
531 if filter_fn:
532 filter_fn(header)
533
534 def filter_line(command_output, line_start):
535 """Extract the last line from command output and filter it."""
536 if not filter_fn or line_start is None:
537 return
538 command_output.seek(line_start)
539 filter_fn(command_output.read().decode('utf-8'))
540
541 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
542 # byte inputs and sys.stdout.buffer must be used instead.
543 if print_stdout:
544 sys.stdout.flush()
545 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
546 else:
547 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000548
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000549 sleep_interval = RETRY_INITIAL_SLEEP
550 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000551 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000552 kid = subprocess2.Popen(
553 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
554 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000555
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000556 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000557
Edward Lemur24146be2019-08-01 21:44:52 +0000558 # Store the output of the command regardless of the value of print_stdout or
559 # filter_fn.
560 command_output = io.BytesIO()
561
562 # Passed as a list for "by ref" semantics.
563 needs_header = [show_header]
564 if always_show_header:
565 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000566
567 # Also, we need to forward stdout to prevent weird re-ordering of output.
568 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700569 # normally buffering is done for each line, but if the process requests
570 # input, no end-of-line character is output after the prompt and it would
571 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000572 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000573 line_start = None
574 while True:
575 in_byte = kid.stdout.read(1)
576 is_newline = in_byte in (b'\n', b'\r')
577 if not in_byte:
578 break
579
580 show_header_if_necessary(needs_header, attempt)
581
582 if is_newline:
583 filter_line(command_output, line_start)
584 line_start = None
585 elif line_start is None:
586 line_start = command_output.tell()
587
588 stdout_write(in_byte)
589 command_output.write(in_byte)
590
591 # Flush the rest of buffered output.
592 sys.stdout.flush()
593 if line_start is not None:
594 filter_line(command_output, line_start)
595
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000596 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000597 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000598
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000599 # Don't put this in a 'finally,' since the child may still run if we get
600 # an exception.
601 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000602
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000603 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000604 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000605 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000606
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000607 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000608 return command_output.getvalue()
609
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000610 if not retry:
611 break
Edward Lemur24146be2019-08-01 21:44:52 +0000612
Raul Tambreb946b232019-03-26 14:48:46 +0000613 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
614 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000615 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000616 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000617
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000618 raise subprocess2.CalledProcessError(
619 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000620
621
agable@chromium.org5a306a22014-02-24 22:13:59 +0000622class GitFilter(object):
623 """A filter_fn implementation for quieting down git output messages.
624
625 Allows a custom function to skip certain lines (predicate), and will throttle
626 the output of percentage completed lines to only output every X seconds.
627 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000628 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000629
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000630 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000631 """
632 Args:
633 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
634 XX% complete messages) to only be printed at least |time_throttle|
635 seconds apart.
636 predicate (f(line)): An optional function which is invoked for every line.
637 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000638 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000639 """
Edward Lemur24146be2019-08-01 21:44:52 +0000640 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000641 self.last_time = 0
642 self.time_throttle = time_throttle
643 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000644 self.out_fh = out_fh or sys.stdout
645 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000646
647 def __call__(self, line):
648 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000649 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000650 if esc > -1:
651 line = line[:esc]
652 if self.predicate and not self.predicate(line):
653 return
654 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000655 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000656 if match:
657 if match.group(1) != self.progress_prefix:
658 self.progress_prefix = match.group(1)
659 elif now - self.last_time < self.time_throttle:
660 return
661 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000662 if not self.first_line:
663 self.out_fh.write('[%s] ' % Elapsed())
664 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000665 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000666
667
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000668def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000669 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000670
rcui@google.com13595ff2011-10-13 01:25:07 +0000671 Returns nearest upper-level directory with the passed in file.
672 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000673 if not path:
674 path = os.getcwd()
675 path = os.path.realpath(path)
676 while True:
677 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000678 if os.path.exists(file_path):
679 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000680 (new_path, _) = os.path.split(path)
681 if new_path == path:
682 return None
683 path = new_path
684
685
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000686def GetMacWinOrLinux():
687 """Returns 'mac', 'win', or 'linux', matching the current platform."""
688 if sys.platform.startswith(('cygwin', 'win')):
689 return 'win'
690 elif sys.platform.startswith('linux'):
691 return 'linux'
692 elif sys.platform == 'darwin':
693 return 'mac'
694 raise Error('Unknown platform: ' + sys.platform)
695
696
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000697def GetGClientRootAndEntries(path=None):
698 """Returns the gclient root and the dict of entries."""
699 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000700 root = FindFileUpwards(config_file, path)
701 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000702 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000703 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000704 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000705 env = {}
706 execfile(config_path, env)
707 config_dir = os.path.dirname(config_path)
708 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000709
710
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000711def lockedmethod(method):
712 """Method decorator that holds self.lock for the duration of the call."""
713 def inner(self, *args, **kwargs):
714 try:
715 try:
716 self.lock.acquire()
717 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000718 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000719 raise
720 return method(self, *args, **kwargs)
721 finally:
722 self.lock.release()
723 return inner
724
725
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000726class WorkItem(object):
727 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000728 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
729 # As a workaround, use a single lock. Yep you read it right. Single lock for
730 # all the 100 objects.
731 lock = threading.Lock()
732
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000733 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000734 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000735 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000736 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000737 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700738 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000739
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000740 def run(self, work_queue):
741 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000742 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000743 pass
744
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000745 @property
746 def name(self):
747 return self._name
748
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000749
750class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000751 """Runs a set of WorkItem that have interdependencies and were WorkItem are
752 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000753
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200754 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000755 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000756
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000757 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000758 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000759 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000760 """jobs specifies the number of concurrent tasks to allow. progress is a
761 Progress instance."""
762 # Set when a thread is done or a new item is enqueued.
763 self.ready_cond = threading.Condition()
764 # Maximum number of concurrent tasks.
765 self.jobs = jobs
766 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000767 self.queued = []
768 # List of strings representing each Dependency.name that was run.
769 self.ran = []
770 # List of items currently running.
771 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000772 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000773 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000774 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000775 self.progress = progress
776 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000777 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000778
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000779 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000780 self.verbose = verbose
781 self.last_join = None
782 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000783
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000784 def enqueue(self, d):
785 """Enqueue one Dependency to be executed later once its requirements are
786 satisfied.
787 """
788 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000789 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000790 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000791 self.queued.append(d)
792 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000793 if self.jobs == 1:
794 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000795 logging.debug('enqueued(%s)' % d.name)
796 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000797 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000798 self.progress.update(0)
799 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000800 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000801 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000802
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000803 def out_cb(self, _):
804 self.last_subproc_output = datetime.datetime.now()
805 return True
806
807 @staticmethod
808 def format_task_output(task, comment=''):
809 if comment:
810 comment = ' (%s)' % comment
811 if task.start and task.finish:
812 elapsed = ' (Elapsed: %s)' % (
813 str(task.finish - task.start).partition('.')[0])
814 else:
815 elapsed = ''
816 return """
817%s%s%s
818----------------------------------------
819%s
820----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000821 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000822
hinoka885e5b12016-06-08 14:40:09 -0700823 def _is_conflict(self, job):
824 """Checks to see if a job will conflict with another running job."""
825 for running_job in self.running:
826 for used_resource in running_job.item.resources:
827 logging.debug('Checking resource %s' % used_resource)
828 if used_resource in job.resources:
829 return True
830 return False
831
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000832 def flush(self, *args, **kwargs):
833 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000834 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000835 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000836 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000837 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000838 while True:
839 # Check for task to run first, then wait.
840 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000841 if not self.exceptions.empty():
842 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000843 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000844 self._flush_terminated_threads()
845 if (not self.queued and not self.running or
846 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000847 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000848 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000849
850 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000851 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000852 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000853 if (self.ignore_requirements or
854 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700855 if not self._is_conflict(self.queued[i]):
856 # Start one work item: all its requirements are satisfied.
857 self._run_one_task(self.queued.pop(i), args, kwargs)
858 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000859 else:
860 # Couldn't find an item that could run. Break out the outher loop.
861 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000862
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000864 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000865 break
866 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000867 try:
868 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000869 # If we haven't printed to terminal for a while, but we have received
870 # spew from a suprocess, let the user know we're still progressing.
871 now = datetime.datetime.now()
872 if (now - self.last_join > datetime.timedelta(seconds=60) and
873 self.last_subproc_output > self.last_join):
874 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000875 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000876 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000877 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000878 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000879 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000880 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000881 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000882 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000883 except KeyboardInterrupt:
884 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000885 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000886 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000887 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
888 self.ran), len(self.running)),
889 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000890 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000891 print(
892 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
893 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000894 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000895 print(
896 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000897 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000898 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000899 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000901
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000902 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000903 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000904 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000905 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000906 # To get back the stack location correctly, the raise a, b, c form must be
907 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000908 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000909 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
910 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000911 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000912 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000913
maruel@chromium.org3742c842010-09-09 19:27:14 +0000914 def _flush_terminated_threads(self):
915 """Flush threads that have terminated."""
916 running = self.running
917 self.running = []
918 for t in running:
919 if t.isAlive():
920 self.running.append(t)
921 else:
922 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000923 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000924 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000925 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000926 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000927 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000928 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000929 if t.item.name in self.ran:
930 raise Error(
931 'gclient is confused, "%s" is already in "%s"' % (
932 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000933 if not t.item.name in self.ran:
934 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000935
936 def _run_one_task(self, task_item, args, kwargs):
937 if self.jobs > 1:
938 # Start the thread.
939 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000940 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000941 self.running.append(new_thread)
942 new_thread.start()
943 else:
944 # Run the 'thread' inside the main thread. Don't try to catch any
945 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000946 try:
947 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000948 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000949 task_item.run(*args, **kwargs)
950 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000951 print(
952 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000953 self.ran.append(task_item.name)
954 if self.verbose:
955 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000956 print('')
957 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000958 if self.progress:
959 self.progress.update(1, ', '.join(t.item.name for t in self.running))
960 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000961 print(
962 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000963 raise
964 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000965 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000966 raise
967
maruel@chromium.org3742c842010-09-09 19:27:14 +0000968
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000969 class _Worker(threading.Thread):
970 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000971 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000972 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000973 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000974 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000975 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000976 self.args = args
977 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000978 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000979
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000980 def run(self):
981 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000982 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000983 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000984 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000985 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000986 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000987 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000988 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000989 print(
990 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000991 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000992 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000993 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000994 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000995 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000996 except Exception:
997 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000998 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000999 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001000 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001001 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001002 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001003 work_queue.ready_cond.acquire()
1004 try:
1005 work_queue.ready_cond.notifyAll()
1006 finally:
1007 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001008
1009
agable92bec4f2016-08-24 09:27:27 -07001010def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001011 """Returns the most plausible editor to use.
1012
1013 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001014 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001015 - core.editor git configuration variable (if supplied by git-cl)
1016 - VISUAL environment variable
1017 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001018 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001019
1020 In the case of git-cl, this matches git's behaviour, except that it does not
1021 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001022 """
agable92bec4f2016-08-24 09:27:27 -07001023 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001024 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001025 editor = os.environ.get('VISUAL')
1026 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001027 editor = os.environ.get('EDITOR')
1028 if not editor:
1029 if sys.platform.startswith('win'):
1030 editor = 'notepad'
1031 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001032 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001033 return editor
1034
1035
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001036def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001037 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001038 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001039 # Make sure CRLF is handled properly by requiring none.
1040 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001041 print(
1042 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001043 fileobj = os.fdopen(file_handle, 'w')
1044 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001045 content = re.sub('\r?\n', '\n', content)
1046 # Some editors complain when the file doesn't end in \n.
1047 if not content.endswith('\n'):
1048 content += '\n'
1049 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001050 fileobj.close()
1051
1052 try:
agable92bec4f2016-08-24 09:27:27 -07001053 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001054 if not editor:
1055 return None
1056 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001057 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1058 # Msysgit requires the usage of 'env' to be present.
1059 cmd = 'env ' + cmd
1060 try:
1061 # shell=True to allow the shell to handle all forms of quotes in
1062 # $EDITOR.
1063 subprocess2.check_call(cmd, shell=True)
1064 except subprocess2.CalledProcessError:
1065 return None
1066 return FileRead(filename)
1067 finally:
1068 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001069
1070
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001071def UpgradeToHttps(url):
1072 """Upgrades random urls to https://.
1073
1074 Do not touch unknown urls like ssh:// or git://.
1075 Do not touch http:// urls with a port number,
1076 Fixes invalid GAE url.
1077 """
1078 if not url:
1079 return url
1080 if not re.match(r'[a-z\-]+\://.*', url):
1081 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1082 # relative url and will use http:///foo. Note that it defaults to http://
1083 # for compatibility with naked url like "localhost:8080".
1084 url = 'http://%s' % url
1085 parsed = list(urlparse.urlparse(url))
1086 # Do not automatically upgrade http to https if a port number is provided.
1087 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1088 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001089 return urlparse.urlunparse(parsed)
1090
1091
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001092def ParseCodereviewSettingsContent(content):
1093 """Process a codereview.settings file properly."""
1094 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1095 try:
1096 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1097 except ValueError:
1098 raise Error(
1099 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001100 def fix_url(key):
1101 if keyvals.get(key):
1102 keyvals[key] = UpgradeToHttps(keyvals[key])
1103 fix_url('CODE_REVIEW_SERVER')
1104 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001105 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001106
1107
1108def NumLocalCpus():
1109 """Returns the number of processors.
1110
dnj@chromium.org530523b2015-01-07 19:54:57 +00001111 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1112 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1113 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001114 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001115 # Surround the entire thing in try/except; no failure here should stop gclient
1116 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001117 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001118 # Use multiprocessing to get CPU count. This may raise
1119 # NotImplementedError.
1120 try:
1121 import multiprocessing
1122 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001123 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001124 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001125 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001126 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1127 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1128
1129 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1130 if 'NUMBER_OF_PROCESSORS' in os.environ:
1131 return int(os.environ['NUMBER_OF_PROCESSORS'])
1132 except Exception as e:
1133 logging.exception("Exception raised while probing CPU count: %s", e)
1134
1135 logging.debug('Failed to get CPU count. Defaulting to 1.')
1136 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001137
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001138
szager@chromium.orgfc616382014-03-18 20:32:04 +00001139def DefaultDeltaBaseCacheLimit():
1140 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1141
1142 The primary constraint is the address space of virtual memory. The cache
1143 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1144 parameter is set too high.
1145 """
1146 if platform.architecture()[0].startswith('64'):
1147 return '2g'
1148 else:
1149 return '512m'
1150
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001151
szager@chromium.orgff113292014-03-25 06:02:08 +00001152def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001153 """Return reasonable default values for configuring git-index-pack.
1154
1155 Experiments suggest that higher values for pack.threads don't improve
1156 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001157 cache_limit = DefaultDeltaBaseCacheLimit()
1158 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1159 if url in THREADED_INDEX_PACK_BLACKLIST:
1160 result.extend(['-c', 'pack.threads=1'])
1161 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001162
1163
1164def FindExecutable(executable):
1165 """This mimics the "which" utility."""
1166 path_folders = os.environ.get('PATH').split(os.pathsep)
1167
1168 for path_folder in path_folders:
1169 target = os.path.join(path_folder, executable)
1170 # Just incase we have some ~/blah paths.
1171 target = os.path.abspath(os.path.expanduser(target))
1172 if os.path.isfile(target) and os.access(target, os.X_OK):
1173 return target
1174 if sys.platform.startswith('win'):
1175 for suffix in ('.bat', '.cmd', '.exe'):
1176 alt_target = target + suffix
1177 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1178 return alt_target
1179 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001180
1181
1182def freeze(obj):
1183 """Takes a generic object ``obj``, and returns an immutable version of it.
1184
1185 Supported types:
1186 * dict / OrderedDict -> FrozenDict
1187 * list -> tuple
1188 * set -> frozenset
1189 * any object with a working __hash__ implementation (assumes that hashable
1190 means immutable)
1191
1192 Will raise TypeError if you pass an object which is not hashable.
1193 """
Edward Lesmes6f64a052018-03-20 17:35:49 -04001194 if isinstance(obj, collections.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001195 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001196 elif isinstance(obj, (list, tuple)):
1197 return tuple(freeze(i) for i in obj)
1198 elif isinstance(obj, set):
1199 return frozenset(freeze(i) for i in obj)
1200 else:
1201 hash(obj)
1202 return obj
1203
1204
1205class FrozenDict(collections.Mapping):
1206 """An immutable OrderedDict.
1207
1208 Modified From: http://stackoverflow.com/a/2704866
1209 """
1210 def __init__(self, *args, **kwargs):
1211 self._d = collections.OrderedDict(*args, **kwargs)
1212
1213 # Calculate the hash immediately so that we know all the items are
1214 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001215 self._hash = functools.reduce(
1216 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001217
1218 def __eq__(self, other):
1219 if not isinstance(other, collections.Mapping):
1220 return NotImplemented
1221 if self is other:
1222 return True
1223 if len(self) != len(other):
1224 return False
1225 for k, v in self.iteritems():
1226 if k not in other or other[k] != v:
1227 return False
1228 return True
1229
1230 def __iter__(self):
1231 return iter(self._d)
1232
1233 def __len__(self):
1234 return len(self._d)
1235
1236 def __getitem__(self, key):
1237 return self._d[key]
1238
1239 def __hash__(self):
1240 return self._hash
1241
1242 def __repr__(self):
1243 return 'FrozenDict(%r)' % (self._d.items(),)