blob: 0957ec1e73b93e7c3e6008d7e007d6bb657f6558 [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'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000168 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000169 # codecs.open() has different behavior than open() on python 2.6 so use
170 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000171 s = f.read()
172 try:
173 return s.decode('utf-8')
Raul Tambreb946b232019-03-26 14:48:46 +0000174 # AttributeError is for Py3 compatibility
175 except (UnicodeDecodeError, AttributeError):
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000176 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000177
178
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000179def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000180 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000181 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182
183
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000184@contextlib.contextmanager
185def temporary_directory(**kwargs):
186 tdir = tempfile.mkdtemp(**kwargs)
187 try:
188 yield tdir
189 finally:
190 if tdir:
191 rmtree(tdir)
192
193
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000194def safe_rename(old, new):
195 """Renames a file reliably.
196
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000197 Sometimes os.rename does not work because a dying git process keeps a handle
198 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000199 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000200 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000201 """
202 # roughly 10s
203 retries = 100
204 for i in range(retries):
205 try:
206 os.rename(old, new)
207 break
208 except OSError:
209 if i == (retries - 1):
210 # Give up.
211 raise
212 # retry
213 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
214 time.sleep(0.1)
215
216
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000217def rm_file_or_tree(path):
218 if os.path.isfile(path):
219 os.remove(path)
220 else:
221 rmtree(path)
222
223
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000224def rmtree(path):
225 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000226
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000227 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000228
229 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700230 are read-only. We need to be able to force the files to be writable (i.e.,
231 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000232
233 Even with all this, Windows still sometimes fails to delete a file, citing
234 a permission error (maybe something to do with antivirus scans or disk
235 indexing). The best suggestion any of the user forums had was to wait a
236 bit and try again, so we do that too. It's hand-waving, but sometimes it
237 works. :/
238
239 On POSIX systems, things are a little bit simpler. The modes of the files
240 to be deleted doesn't matter, only the modes of the directories containing
241 them are significant. As the directory tree is traversed, each directory
242 has its mode set appropriately before descending into it. This should
243 result in the entire tree being removed, with the possible exception of
244 *path itself, because nothing attempts to change the mode of its parent.
245 Doing so would be hazardous, as it's not a directory slated for removal.
246 In the ordinary case, this is not a problem: for our purposes, the user
247 will never lack write permission on *path's parent.
248 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000249 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000250 return
251
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000252 if os.path.islink(path) or not os.path.isdir(path):
253 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000254
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000256 # Give up and use cmd.exe's rd command.
257 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000258 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000259 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
260 if exitcode == 0:
261 return
262 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000263 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000264 time.sleep(3)
265 raise Exception('Failed to remove path %s' % path)
266
267 # On POSIX systems, we need the x-bit set on the directory to access it,
268 # the r-bit to see its contents, and the w-bit to remove files from it.
269 # The actual modes of the files within the directory is irrelevant.
270 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000271
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000272 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000273 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000274
275 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000276 # If fullpath is a symbolic link that points to a directory, isdir will
277 # be True, but we don't want to descend into that as a directory, we just
278 # want to remove the link. Check islink and treat links as ordinary files
279 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000280 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000281 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000282 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000283 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000284 # Recurse.
285 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000287 remove(os.rmdir, path)
288
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000289
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000290def safe_makedirs(tree):
291 """Creates the directory in a safe manner.
292
293 Because multiple threads can create these directories concurently, trap the
294 exception and pass on.
295 """
296 count = 0
297 while not os.path.exists(tree):
298 count += 1
299 try:
300 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000301 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000302 # 17 POSIX, 183 Windows
303 if e.errno not in (17, 183):
304 raise
305 if count > 40:
306 # Give up.
307 raise
308
309
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000310def CommandToStr(args):
311 """Converts an arg list into a shell escaped string."""
312 return ' '.join(pipes.quote(arg) for arg in args)
313
314
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000315class Wrapper(object):
316 """Wraps an object, acting as a transparent proxy for all properties by
317 default.
318 """
319 def __init__(self, wrapped):
320 self._wrapped = wrapped
321
322 def __getattr__(self, name):
323 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000324
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000325
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000326class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000327 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000328 def __init__(self, wrapped, delay):
329 super(AutoFlush, self).__init__(wrapped)
330 if not hasattr(self, 'lock'):
331 self.lock = threading.Lock()
332 self.__last_flushed_at = time.time()
333 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000334
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000335 @property
336 def autoflush(self):
337 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000338
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000339 def write(self, out, *args, **kwargs):
340 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000341 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000342 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000343 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000345 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000346 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000347 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000349 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000350 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000351
352
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000353class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000354 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000355 threads with a NN> prefix.
356 """
357 def __init__(self, wrapped, include_zero=False):
358 super(Annotated, self).__init__(wrapped)
359 if not hasattr(self, 'lock'):
360 self.lock = threading.Lock()
361 self.__output_buffers = {}
362 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000363
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000364 @property
365 def annotated(self):
366 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000367
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000368 def write(self, out):
369 index = getattr(threading.currentThread(), 'index', 0)
370 if not index and not self.__include_zero:
371 # Unindexed threads aren't buffered.
372 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000373
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000374 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000375 try:
376 # Use a dummy array to hold the string so the code can be lockless.
377 # Strings are immutable, requiring to keep a lock for the whole dictionary
378 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000379 if not index in self.__output_buffers:
380 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000381 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000383 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000384 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000385
386 # Continue lockless.
387 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000388 while True:
389 # TODO(agable): find both of these with a single pass.
390 cr_loc = obj[0].find('\r')
391 lf_loc = obj[0].find('\n')
392 if cr_loc == lf_loc == -1:
393 break
394 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
395 line, remaining = obj[0].split('\n', 1)
396 if line:
397 self._wrapped.write('%d>%s\n' % (index, line))
398 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
399 line, remaining = obj[0].split('\r', 1)
400 if line:
401 self._wrapped.write('%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000402 obj[0] = remaining
403
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000404 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000405 """Flush buffered output."""
406 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000408 try:
409 # Detect threads no longer existing.
410 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000411 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000412 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000413 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000414 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000415 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000416 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000417 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000418 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000419
420 # Don't keep the lock while writting. Will append \n when it shouldn't.
421 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000422 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000423 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
424 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000425
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000426
427def MakeFileAutoFlush(fileobj, delay=10):
428 autoflush = getattr(fileobj, 'autoflush', None)
429 if autoflush:
430 autoflush.delay = delay
431 return fileobj
432 return AutoFlush(fileobj, delay)
433
434
435def MakeFileAnnotated(fileobj, include_zero=False):
436 if getattr(fileobj, 'annotated', None):
437 return fileobj
438 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000439
440
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000441GCLIENT_CHILDREN = []
442GCLIENT_CHILDREN_LOCK = threading.Lock()
443
444
445class GClientChildren(object):
446 @staticmethod
447 def add(popen_obj):
448 with GCLIENT_CHILDREN_LOCK:
449 GCLIENT_CHILDREN.append(popen_obj)
450
451 @staticmethod
452 def remove(popen_obj):
453 with GCLIENT_CHILDREN_LOCK:
454 GCLIENT_CHILDREN.remove(popen_obj)
455
456 @staticmethod
457 def _attemptToKillChildren():
458 global GCLIENT_CHILDREN
459 with GCLIENT_CHILDREN_LOCK:
460 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
461
462 for zombie in zombies:
463 try:
464 zombie.kill()
465 except OSError:
466 pass
467
468 with GCLIENT_CHILDREN_LOCK:
469 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
470
471 @staticmethod
472 def _areZombies():
473 with GCLIENT_CHILDREN_LOCK:
474 return bool(GCLIENT_CHILDREN)
475
476 @staticmethod
477 def KillAllRemainingChildren():
478 GClientChildren._attemptToKillChildren()
479
480 if GClientChildren._areZombies():
481 time.sleep(0.5)
482 GClientChildren._attemptToKillChildren()
483
484 with GCLIENT_CHILDREN_LOCK:
485 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000486 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000487 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000488 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000489
490
Edward Lemur24146be2019-08-01 21:44:52 +0000491def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
492 show_header=False, always_show_header=False, retry=False,
493 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000494 """Runs a command and calls back a filter function if needed.
495
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000496 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000497 print_stdout: If True, the command's stdout is forwarded to stdout.
498 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000499 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000500 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000501 show_header: Whether to display a header before the command output.
502 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000503 retry: If the process exits non-zero, sleep for a brief interval and try
504 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000505
506 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000507
508 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000509 """
Edward Lemur24146be2019-08-01 21:44:52 +0000510 def show_header_if_necessary(needs_header, attempt):
511 """Show the header at most once."""
512 if not needs_header[0]:
513 return
514
515 needs_header[0] = False
516 # Automatically generated header. We only prepend a newline if
517 # always_show_header is false, since it usually indicates there's an
518 # external progress display, and it's better not to clobber it in that case.
519 header = '' if always_show_header else '\n'
520 header += '________ running \'%s\' in \'%s\'' % (
521 ' '.join(args), kwargs.get('cwd', '.'))
522 if attempt:
523 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
524 header += '\n'
525
526 if print_stdout:
527 sys.stdout.write(header)
528 if filter_fn:
529 filter_fn(header)
530
531 def filter_line(command_output, line_start):
532 """Extract the last line from command output and filter it."""
533 if not filter_fn or line_start is None:
534 return
535 command_output.seek(line_start)
536 filter_fn(command_output.read().decode('utf-8'))
537
538 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
539 # byte inputs and sys.stdout.buffer must be used instead.
540 if print_stdout:
541 sys.stdout.flush()
542 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
543 else:
544 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000545
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000546 sleep_interval = RETRY_INITIAL_SLEEP
547 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000548 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000549 kid = subprocess2.Popen(
550 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
551 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000552
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000553 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000554
Edward Lemur24146be2019-08-01 21:44:52 +0000555 # Store the output of the command regardless of the value of print_stdout or
556 # filter_fn.
557 command_output = io.BytesIO()
558
559 # Passed as a list for "by ref" semantics.
560 needs_header = [show_header]
561 if always_show_header:
562 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000563
564 # Also, we need to forward stdout to prevent weird re-ordering of output.
565 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700566 # normally buffering is done for each line, but if the process requests
567 # input, no end-of-line character is output after the prompt and it would
568 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000569 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000570 line_start = None
571 while True:
572 in_byte = kid.stdout.read(1)
573 is_newline = in_byte in (b'\n', b'\r')
574 if not in_byte:
575 break
576
577 show_header_if_necessary(needs_header, attempt)
578
579 if is_newline:
580 filter_line(command_output, line_start)
581 line_start = None
582 elif line_start is None:
583 line_start = command_output.tell()
584
585 stdout_write(in_byte)
586 command_output.write(in_byte)
587
588 # Flush the rest of buffered output.
589 sys.stdout.flush()
590 if line_start is not None:
591 filter_line(command_output, line_start)
592
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000593 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000594 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000595
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000596 # Don't put this in a 'finally,' since the child may still run if we get
597 # an exception.
598 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000599
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000600 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000601 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000602 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000603
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000604 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000605 return command_output.getvalue()
606
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000607 if not retry:
608 break
Edward Lemur24146be2019-08-01 21:44:52 +0000609
Raul Tambreb946b232019-03-26 14:48:46 +0000610 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
611 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000612 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000613 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000614
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000615 raise subprocess2.CalledProcessError(
616 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000617
618
agable@chromium.org5a306a22014-02-24 22:13:59 +0000619class GitFilter(object):
620 """A filter_fn implementation for quieting down git output messages.
621
622 Allows a custom function to skip certain lines (predicate), and will throttle
623 the output of percentage completed lines to only output every X seconds.
624 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000625 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000626
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000627 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000628 """
629 Args:
630 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
631 XX% complete messages) to only be printed at least |time_throttle|
632 seconds apart.
633 predicate (f(line)): An optional function which is invoked for every line.
634 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000635 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000636 """
Edward Lemur24146be2019-08-01 21:44:52 +0000637 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000638 self.last_time = 0
639 self.time_throttle = time_throttle
640 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000641 self.out_fh = out_fh or sys.stdout
642 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000643
644 def __call__(self, line):
645 # git uses an escape sequence to clear the line; elide it.
Raul Tambreb946b232019-03-26 14:48:46 +0000646 esc = line.find(chr(0o33).encode())
agable@chromium.org5a306a22014-02-24 22:13:59 +0000647 if esc > -1:
648 line = line[:esc]
649 if self.predicate and not self.predicate(line):
650 return
651 now = time.time()
Raul Tambreb946b232019-03-26 14:48:46 +0000652 match = self.PERCENT_RE.match(line.decode())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000653 if match:
654 if match.group(1) != self.progress_prefix:
655 self.progress_prefix = match.group(1)
656 elif now - self.last_time < self.time_throttle:
657 return
658 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000659 if not self.first_line:
660 self.out_fh.write('[%s] ' % Elapsed())
661 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000662 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000663
664
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000665def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000666 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000667
rcui@google.com13595ff2011-10-13 01:25:07 +0000668 Returns nearest upper-level directory with the passed in file.
669 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000670 if not path:
671 path = os.getcwd()
672 path = os.path.realpath(path)
673 while True:
674 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000675 if os.path.exists(file_path):
676 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000677 (new_path, _) = os.path.split(path)
678 if new_path == path:
679 return None
680 path = new_path
681
682
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000683def GetMacWinOrLinux():
684 """Returns 'mac', 'win', or 'linux', matching the current platform."""
685 if sys.platform.startswith(('cygwin', 'win')):
686 return 'win'
687 elif sys.platform.startswith('linux'):
688 return 'linux'
689 elif sys.platform == 'darwin':
690 return 'mac'
691 raise Error('Unknown platform: ' + sys.platform)
692
693
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000694def GetGClientRootAndEntries(path=None):
695 """Returns the gclient root and the dict of entries."""
696 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000697 root = FindFileUpwards(config_file, path)
698 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000699 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000700 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000701 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000702 env = {}
703 execfile(config_path, env)
704 config_dir = os.path.dirname(config_path)
705 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000706
707
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000708def lockedmethod(method):
709 """Method decorator that holds self.lock for the duration of the call."""
710 def inner(self, *args, **kwargs):
711 try:
712 try:
713 self.lock.acquire()
714 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000715 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000716 raise
717 return method(self, *args, **kwargs)
718 finally:
719 self.lock.release()
720 return inner
721
722
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000723class WorkItem(object):
724 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000725 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
726 # As a workaround, use a single lock. Yep you read it right. Single lock for
727 # all the 100 objects.
728 lock = threading.Lock()
729
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000730 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000731 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000732 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000733 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000734 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700735 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000736
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000737 def run(self, work_queue):
738 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000739 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000740 pass
741
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000742 @property
743 def name(self):
744 return self._name
745
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000746
747class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000748 """Runs a set of WorkItem that have interdependencies and were WorkItem are
749 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000750
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200751 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000752 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000753
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000754 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000755 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000756 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000757 """jobs specifies the number of concurrent tasks to allow. progress is a
758 Progress instance."""
759 # Set when a thread is done or a new item is enqueued.
760 self.ready_cond = threading.Condition()
761 # Maximum number of concurrent tasks.
762 self.jobs = jobs
763 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000764 self.queued = []
765 # List of strings representing each Dependency.name that was run.
766 self.ran = []
767 # List of items currently running.
768 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000769 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000770 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000771 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000772 self.progress = progress
773 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000774 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000775
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000776 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000777 self.verbose = verbose
778 self.last_join = None
779 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000780
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000781 def enqueue(self, d):
782 """Enqueue one Dependency to be executed later once its requirements are
783 satisfied.
784 """
785 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000786 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000787 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000788 self.queued.append(d)
789 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000790 if self.jobs == 1:
791 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000792 logging.debug('enqueued(%s)' % d.name)
793 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000794 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000795 self.progress.update(0)
796 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000797 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000798 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000799
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000800 def out_cb(self, _):
801 self.last_subproc_output = datetime.datetime.now()
802 return True
803
804 @staticmethod
805 def format_task_output(task, comment=''):
806 if comment:
807 comment = ' (%s)' % comment
808 if task.start and task.finish:
809 elapsed = ' (Elapsed: %s)' % (
810 str(task.finish - task.start).partition('.')[0])
811 else:
812 elapsed = ''
813 return """
814%s%s%s
815----------------------------------------
816%s
817----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000818 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000819
hinoka885e5b12016-06-08 14:40:09 -0700820 def _is_conflict(self, job):
821 """Checks to see if a job will conflict with another running job."""
822 for running_job in self.running:
823 for used_resource in running_job.item.resources:
824 logging.debug('Checking resource %s' % used_resource)
825 if used_resource in job.resources:
826 return True
827 return False
828
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000829 def flush(self, *args, **kwargs):
830 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000831 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000832 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000833 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000834 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000835 while True:
836 # Check for task to run first, then wait.
837 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000838 if not self.exceptions.empty():
839 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000840 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000841 self._flush_terminated_threads()
842 if (not self.queued and not self.running or
843 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000844 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000845 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000846
847 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000848 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000849 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000850 if (self.ignore_requirements or
851 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700852 if not self._is_conflict(self.queued[i]):
853 # Start one work item: all its requirements are satisfied.
854 self._run_one_task(self.queued.pop(i), args, kwargs)
855 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000856 else:
857 # Couldn't find an item that could run. Break out the outher loop.
858 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000859
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000860 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000861 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000862 break
863 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000864 try:
865 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000866 # If we haven't printed to terminal for a while, but we have received
867 # spew from a suprocess, let the user know we're still progressing.
868 now = datetime.datetime.now()
869 if (now - self.last_join > datetime.timedelta(seconds=60) and
870 self.last_subproc_output > self.last_join):
871 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000872 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000873 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000874 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000875 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000876 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000877 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000878 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000879 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000880 except KeyboardInterrupt:
881 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000882 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000883 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000884 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
885 self.ran), len(self.running)),
886 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000887 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000888 print(
889 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
890 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000891 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000892 print(
893 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000894 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000895 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000896 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000897 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000898
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000899 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000900 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000901 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000902 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000903 # To get back the stack location correctly, the raise a, b, c form must be
904 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000905 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000906 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
907 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000908 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000909 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000910
maruel@chromium.org3742c842010-09-09 19:27:14 +0000911 def _flush_terminated_threads(self):
912 """Flush threads that have terminated."""
913 running = self.running
914 self.running = []
915 for t in running:
916 if t.isAlive():
917 self.running.append(t)
918 else:
919 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000920 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000921 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000922 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000923 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000924 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000925 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000926 if t.item.name in self.ran:
927 raise Error(
928 'gclient is confused, "%s" is already in "%s"' % (
929 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000930 if not t.item.name in self.ran:
931 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000932
933 def _run_one_task(self, task_item, args, kwargs):
934 if self.jobs > 1:
935 # Start the thread.
936 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000937 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000938 self.running.append(new_thread)
939 new_thread.start()
940 else:
941 # Run the 'thread' inside the main thread. Don't try to catch any
942 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000943 try:
944 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000945 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000946 task_item.run(*args, **kwargs)
947 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000948 print(
949 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000950 self.ran.append(task_item.name)
951 if self.verbose:
952 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000953 print('')
954 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000955 if self.progress:
956 self.progress.update(1, ', '.join(t.item.name for t in self.running))
957 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000958 print(
959 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000960 raise
961 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000962 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000963 raise
964
maruel@chromium.org3742c842010-09-09 19:27:14 +0000965
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000966 class _Worker(threading.Thread):
967 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000968 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000969 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000970 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000971 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000972 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000973 self.args = args
974 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000975 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000976
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000977 def run(self):
978 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000979 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000980 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000981 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000982 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000983 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000984 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000985 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000986 print(
987 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000988 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000989 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000990 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000991 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000992 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000993 except Exception:
994 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000995 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000996 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000997 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000998 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000999 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001000 work_queue.ready_cond.acquire()
1001 try:
1002 work_queue.ready_cond.notifyAll()
1003 finally:
1004 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001005
1006
agable92bec4f2016-08-24 09:27:27 -07001007def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001008 """Returns the most plausible editor to use.
1009
1010 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001011 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001012 - core.editor git configuration variable (if supplied by git-cl)
1013 - VISUAL environment variable
1014 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001015 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001016
1017 In the case of git-cl, this matches git's behaviour, except that it does not
1018 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001019 """
agable92bec4f2016-08-24 09:27:27 -07001020 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001021 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001022 editor = os.environ.get('VISUAL')
1023 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001024 editor = os.environ.get('EDITOR')
1025 if not editor:
1026 if sys.platform.startswith('win'):
1027 editor = 'notepad'
1028 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001029 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001030 return editor
1031
1032
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001033def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001034 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001035 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001036 # Make sure CRLF is handled properly by requiring none.
1037 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001038 print(
1039 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001040 fileobj = os.fdopen(file_handle, 'w')
1041 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001042 content = re.sub('\r?\n', '\n', content)
1043 # Some editors complain when the file doesn't end in \n.
1044 if not content.endswith('\n'):
1045 content += '\n'
1046 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001047 fileobj.close()
1048
1049 try:
agable92bec4f2016-08-24 09:27:27 -07001050 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001051 if not editor:
1052 return None
1053 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001054 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1055 # Msysgit requires the usage of 'env' to be present.
1056 cmd = 'env ' + cmd
1057 try:
1058 # shell=True to allow the shell to handle all forms of quotes in
1059 # $EDITOR.
1060 subprocess2.check_call(cmd, shell=True)
1061 except subprocess2.CalledProcessError:
1062 return None
1063 return FileRead(filename)
1064 finally:
1065 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001066
1067
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001068def UpgradeToHttps(url):
1069 """Upgrades random urls to https://.
1070
1071 Do not touch unknown urls like ssh:// or git://.
1072 Do not touch http:// urls with a port number,
1073 Fixes invalid GAE url.
1074 """
1075 if not url:
1076 return url
1077 if not re.match(r'[a-z\-]+\://.*', url):
1078 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1079 # relative url and will use http:///foo. Note that it defaults to http://
1080 # for compatibility with naked url like "localhost:8080".
1081 url = 'http://%s' % url
1082 parsed = list(urlparse.urlparse(url))
1083 # Do not automatically upgrade http to https if a port number is provided.
1084 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1085 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001086 return urlparse.urlunparse(parsed)
1087
1088
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001089def ParseCodereviewSettingsContent(content):
1090 """Process a codereview.settings file properly."""
1091 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1092 try:
1093 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1094 except ValueError:
1095 raise Error(
1096 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001097 def fix_url(key):
1098 if keyvals.get(key):
1099 keyvals[key] = UpgradeToHttps(keyvals[key])
1100 fix_url('CODE_REVIEW_SERVER')
1101 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001102 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001103
1104
1105def NumLocalCpus():
1106 """Returns the number of processors.
1107
dnj@chromium.org530523b2015-01-07 19:54:57 +00001108 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1109 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1110 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001111 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001112 # Surround the entire thing in try/except; no failure here should stop gclient
1113 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001114 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001115 # Use multiprocessing to get CPU count. This may raise
1116 # NotImplementedError.
1117 try:
1118 import multiprocessing
1119 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001120 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001121 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001122 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001123 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1124 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1125
1126 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1127 if 'NUMBER_OF_PROCESSORS' in os.environ:
1128 return int(os.environ['NUMBER_OF_PROCESSORS'])
1129 except Exception as e:
1130 logging.exception("Exception raised while probing CPU count: %s", e)
1131
1132 logging.debug('Failed to get CPU count. Defaulting to 1.')
1133 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001134
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001135
szager@chromium.orgfc616382014-03-18 20:32:04 +00001136def DefaultDeltaBaseCacheLimit():
1137 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1138
1139 The primary constraint is the address space of virtual memory. The cache
1140 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1141 parameter is set too high.
1142 """
1143 if platform.architecture()[0].startswith('64'):
1144 return '2g'
1145 else:
1146 return '512m'
1147
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001148
szager@chromium.orgff113292014-03-25 06:02:08 +00001149def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001150 """Return reasonable default values for configuring git-index-pack.
1151
1152 Experiments suggest that higher values for pack.threads don't improve
1153 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001154 cache_limit = DefaultDeltaBaseCacheLimit()
1155 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1156 if url in THREADED_INDEX_PACK_BLACKLIST:
1157 result.extend(['-c', 'pack.threads=1'])
1158 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001159
1160
1161def FindExecutable(executable):
1162 """This mimics the "which" utility."""
1163 path_folders = os.environ.get('PATH').split(os.pathsep)
1164
1165 for path_folder in path_folders:
1166 target = os.path.join(path_folder, executable)
1167 # Just incase we have some ~/blah paths.
1168 target = os.path.abspath(os.path.expanduser(target))
1169 if os.path.isfile(target) and os.access(target, os.X_OK):
1170 return target
1171 if sys.platform.startswith('win'):
1172 for suffix in ('.bat', '.cmd', '.exe'):
1173 alt_target = target + suffix
1174 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1175 return alt_target
1176 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001177
1178
1179def freeze(obj):
1180 """Takes a generic object ``obj``, and returns an immutable version of it.
1181
1182 Supported types:
1183 * dict / OrderedDict -> FrozenDict
1184 * list -> tuple
1185 * set -> frozenset
1186 * any object with a working __hash__ implementation (assumes that hashable
1187 means immutable)
1188
1189 Will raise TypeError if you pass an object which is not hashable.
1190 """
Edward Lesmes6f64a052018-03-20 17:35:49 -04001191 if isinstance(obj, collections.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001192 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001193 elif isinstance(obj, (list, tuple)):
1194 return tuple(freeze(i) for i in obj)
1195 elif isinstance(obj, set):
1196 return frozenset(freeze(i) for i in obj)
1197 else:
1198 hash(obj)
1199 return obj
1200
1201
1202class FrozenDict(collections.Mapping):
1203 """An immutable OrderedDict.
1204
1205 Modified From: http://stackoverflow.com/a/2704866
1206 """
1207 def __init__(self, *args, **kwargs):
1208 self._d = collections.OrderedDict(*args, **kwargs)
1209
1210 # Calculate the hash immediately so that we know all the items are
1211 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001212 self._hash = functools.reduce(
1213 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001214
1215 def __eq__(self, other):
1216 if not isinstance(other, collections.Mapping):
1217 return NotImplemented
1218 if self is other:
1219 return True
1220 if len(self) != len(other):
1221 return False
1222 for k, v in self.iteritems():
1223 if k not in other or other[k] != v:
1224 return False
1225 return True
1226
1227 def __iter__(self):
1228 return iter(self._d)
1229
1230 def __len__(self):
1231 return len(self._d)
1232
1233 def __getitem__(self, key):
1234 return self._d[key]
1235
1236 def __hash__(self):
1237 return self._hash
1238
1239 def __repr__(self):
1240 return 'FrozenDict(%r)' % (self._d.items(),)