blob: b413a88eb8a5d9150126e057c3ed7fd67787fe02 [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
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00007import codecs
maruel@chromium.org167b9e62009-09-17 17:41:02 +00008import errno
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +00009import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000010import os
maruel@chromium.org3742c842010-09-09 19:27:14 +000011import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000012import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000013import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000015import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000016import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000017import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000018import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000019
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000020import subprocess2
21
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022
maruel@chromium.org66c83e62010-09-07 14:18:45 +000023class Error(Exception):
24 """gclient exception class."""
25 pass
26
27
msb@chromium.orgac915bb2009-11-13 17:03:01 +000028def SplitUrlRevision(url):
29 """Splits url and returns a two-tuple: url, rev"""
30 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000031 # Make sure ssh://user-name@example.com/~/test.git@stable works
32 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000033 components = re.search(regex, url).groups()
34 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000035 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000036 if len(components) == 1:
37 components += [None]
38 return tuple(components)
39
40
floitsch@google.comeaab7842011-04-28 09:07:58 +000041def IsDateRevision(revision):
42 """Returns true if the given revision is of the form "{ ... }"."""
43 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
44
45
46def MakeDateRevision(date):
47 """Returns a revision representing the latest revision before the given
48 date."""
49 return "{" + date + "}"
50
51
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000052def SyntaxErrorToError(filename, e):
53 """Raises a gclient_utils.Error exception with the human readable message"""
54 try:
55 # Try to construct a human readable error message
56 if filename:
57 error_message = 'There is a syntax error in %s\n' % filename
58 else:
59 error_message = 'There is a syntax error\n'
60 error_message += 'Line #%s, character %s: "%s"' % (
61 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
62 except:
63 # Something went wrong, re-raise the original exception
64 raise e
65 else:
66 raise Error(error_message)
67
68
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000069class PrintableObject(object):
70 def __str__(self):
71 output = ''
72 for i in dir(self):
73 if i.startswith('__'):
74 continue
75 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
76 return output
77
78
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000079def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000080 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +000081 # codecs.open() has different behavior than open() on python 2.6 so use
82 # open() and decode manually.
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000083 return f.read().decode('utf-8')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000084
85
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000086def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000087 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000088 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000089
90
maruel@chromium.orgf9040722011-03-09 14:47:51 +000091def rmtree(path):
92 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000093
maruel@chromium.orgf9040722011-03-09 14:47:51 +000094 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000095
96 shutil.rmtree() doesn't work on Windows if any of the files or directories
97 are read-only, which svn repositories and some .svn files are. We need to
98 be able to force the files to be writable (i.e., deletable) as we traverse
99 the tree.
100
101 Even with all this, Windows still sometimes fails to delete a file, citing
102 a permission error (maybe something to do with antivirus scans or disk
103 indexing). The best suggestion any of the user forums had was to wait a
104 bit and try again, so we do that too. It's hand-waving, but sometimes it
105 works. :/
106
107 On POSIX systems, things are a little bit simpler. The modes of the files
108 to be deleted doesn't matter, only the modes of the directories containing
109 them are significant. As the directory tree is traversed, each directory
110 has its mode set appropriately before descending into it. This should
111 result in the entire tree being removed, with the possible exception of
112 *path itself, because nothing attempts to change the mode of its parent.
113 Doing so would be hazardous, as it's not a directory slated for removal.
114 In the ordinary case, this is not a problem: for our purposes, the user
115 will never lack write permission on *path's parent.
116 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000117 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000118 return
119
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000120 if os.path.islink(path) or not os.path.isdir(path):
121 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000123 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000125 win32api = None
126 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000127 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000128 # Unable to import 'XX'
129 # pylint: disable=F0401
130 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000131 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000132 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000133 else:
134 # On POSIX systems, we need the x-bit set on the directory to access it,
135 # the r-bit to see its contents, and the w-bit to remove files from it.
136 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000137 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000138
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000139 def remove(func, subpath):
140 if sys.platform == 'win32':
141 os.chmod(subpath, stat.S_IWRITE)
142 if win32api and win32con:
143 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
144 try:
145 func(subpath)
146 except OSError, e:
147 if e.errno != errno.EACCES or sys.platform != 'win32':
148 raise
149 # Failed to delete, try again after a 100ms sleep.
150 time.sleep(0.1)
151 func(subpath)
152
153 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000154 # If fullpath is a symbolic link that points to a directory, isdir will
155 # be True, but we don't want to descend into that as a directory, we just
156 # want to remove the link. Check islink and treat links as ordinary files
157 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000158 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000160 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000161 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000162 # Recurse.
163 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000164
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000165 remove(os.rmdir, path)
166
167# TODO(maruel): Rename the references.
168RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000169
170
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000171def safe_makedirs(tree):
172 """Creates the directory in a safe manner.
173
174 Because multiple threads can create these directories concurently, trap the
175 exception and pass on.
176 """
177 count = 0
178 while not os.path.exists(tree):
179 count += 1
180 try:
181 os.makedirs(tree)
182 except OSError, e:
183 # 17 POSIX, 183 Windows
184 if e.errno not in (17, 183):
185 raise
186 if count > 40:
187 # Give up.
188 raise
189
190
maruel@chromium.org17d01792010-09-01 18:07:10 +0000191def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
192 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000193
maruel@chromium.org17d01792010-09-01 18:07:10 +0000194 If |always| is True, a message indicating what is being done
195 is printed to stdout all the time even if not output is generated. Otherwise
196 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000197 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000198 stdout = kwargs.get('stdout', None) or sys.stdout
199 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000200 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000201 % (' '.join(args), kwargs.get('cwd', '.')))
202 else:
203 filter_fn = kwargs.get('filter_fn', None)
204 def filter_msg(line):
205 if line is None:
206 stdout.write('\n________ running \'%s\' in \'%s\'\n'
207 % (' '.join(args), kwargs.get('cwd', '.')))
208 elif filter_fn:
209 filter_fn(line)
210 kwargs['filter_fn'] = filter_msg
211 kwargs['call_filter_on_first_line'] = True
212 # Obviously.
213 kwargs['print_stdout'] = True
214 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000215
maruel@chromium.org17d01792010-09-01 18:07:10 +0000216
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000217class Wrapper(object):
218 """Wraps an object, acting as a transparent proxy for all properties by
219 default.
220 """
221 def __init__(self, wrapped):
222 self._wrapped = wrapped
223
224 def __getattr__(self, name):
225 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000226
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000227
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000228class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000229 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000230 def __init__(self, wrapped, delay):
231 super(AutoFlush, self).__init__(wrapped)
232 if not hasattr(self, 'lock'):
233 self.lock = threading.Lock()
234 self.__last_flushed_at = time.time()
235 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000236
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000237 @property
238 def autoflush(self):
239 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000240
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000241 def write(self, out, *args, **kwargs):
242 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000243 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000244 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000245 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000246 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000247 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000248 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000249 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000250 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000251 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000252 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000253
254
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000255class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000256 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000257 threads with a NN> prefix.
258 """
259 def __init__(self, wrapped, include_zero=False):
260 super(Annotated, self).__init__(wrapped)
261 if not hasattr(self, 'lock'):
262 self.lock = threading.Lock()
263 self.__output_buffers = {}
264 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000265
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000266 @property
267 def annotated(self):
268 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000269
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000270 def write(self, out):
271 index = getattr(threading.currentThread(), 'index', 0)
272 if not index and not self.__include_zero:
273 # Unindexed threads aren't buffered.
274 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000275
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000276 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000277 try:
278 # Use a dummy array to hold the string so the code can be lockless.
279 # Strings are immutable, requiring to keep a lock for the whole dictionary
280 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000281 if not index in self.__output_buffers:
282 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000283 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000284 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000285 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000286 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000287
288 # Continue lockless.
289 obj[0] += out
290 while '\n' in obj[0]:
291 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000292 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000293 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000294 obj[0] = remaining
295
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000296 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000297 """Flush buffered output."""
298 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000299 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000300 try:
301 # Detect threads no longer existing.
302 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000303 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000304 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000305 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000306 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000307 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000308 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000309 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000310 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000311
312 # Don't keep the lock while writting. Will append \n when it shouldn't.
313 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000314 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000315 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
316 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000317
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000318
319def MakeFileAutoFlush(fileobj, delay=10):
320 autoflush = getattr(fileobj, 'autoflush', None)
321 if autoflush:
322 autoflush.delay = delay
323 return fileobj
324 return AutoFlush(fileobj, delay)
325
326
327def MakeFileAnnotated(fileobj, include_zero=False):
328 if getattr(fileobj, 'annotated', None):
329 return fileobj
330 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000331
332
maruel@chromium.org17d01792010-09-01 18:07:10 +0000333def CheckCallAndFilter(args, stdout=None, filter_fn=None,
334 print_stdout=None, call_filter_on_first_line=False,
335 **kwargs):
336 """Runs a command and calls back a filter function if needed.
337
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000338 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000339 print_stdout: If True, the command's stdout is forwarded to stdout.
340 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000341 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000342 character trimmed.
343 stdout: Can be any bufferable output.
344
345 stderr is always redirected to stdout.
346 """
347 assert print_stdout or filter_fn
348 stdout = stdout or sys.stdout
349 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000350 kid = subprocess2.Popen(
351 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
352 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000353
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000354 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000355 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000356
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000357 # Also, we need to forward stdout to prevent weird re-ordering of output.
358 # This has to be done on a per byte basis to make sure it is not buffered:
359 # normally buffering is done for each line, but if svn requests input, no
360 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000361 try:
362 in_byte = kid.stdout.read(1)
363 if in_byte:
364 if call_filter_on_first_line:
365 filter_fn(None)
366 in_line = ''
367 while in_byte:
368 if in_byte != '\r':
369 if print_stdout:
370 stdout.write(in_byte)
371 if in_byte != '\n':
372 in_line += in_byte
373 else:
374 filter_fn(in_line)
375 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000376 else:
377 filter_fn(in_line)
378 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000379 in_byte = kid.stdout.read(1)
380 # Flush the rest of buffered output. This is only an issue with
381 # stdout/stderr not ending with a \n.
382 if len(in_line):
383 filter_fn(in_line)
384 rv = kid.wait()
385 except KeyboardInterrupt:
386 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
387 raise
388
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000389 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000390 raise subprocess2.CalledProcessError(
391 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000392 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000393
394
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000395def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000396 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000397 real_from_dir = os.path.realpath(from_dir)
398 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000399 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000400 split_path = os.path.split(path)
401 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000402 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000403 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000404
405 # If we did not find the file in the current directory, make sure we are in a
406 # sub directory that is controlled by this configuration.
407 if path != real_from_dir:
408 entries_filename = os.path.join(path, filename + '_entries')
409 if not os.path.exists(entries_filename):
410 # If .gclient_entries does not exist, a previous call to gclient sync
411 # might have failed. In that case, we cannot verify that the .gclient
412 # is the one we want to use. In order to not to cause too much trouble,
413 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000414 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000415 "file you want to use" % (filename, path))
416 return path
417 scope = {}
418 try:
419 exec(FileRead(entries_filename), scope)
420 except SyntaxError, e:
421 SyntaxErrorToError(filename, e)
422 all_directories = scope['entries'].keys()
423 path_to_check = real_from_dir[len(path)+1:]
424 while path_to_check:
425 if path_to_check in all_directories:
426 return path
427 path_to_check = os.path.dirname(path_to_check)
428 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000429
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000430 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000431 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000432
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000433
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000434def PathDifference(root, subpath):
435 """Returns the difference subpath minus root."""
436 root = os.path.realpath(root)
437 subpath = os.path.realpath(subpath)
438 if not subpath.startswith(root):
439 return None
440 # If the root does not have a trailing \ or /, we add it so the returned
441 # path starts immediately after the seperator regardless of whether it is
442 # provided.
443 root = os.path.join(root, '')
444 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000445
446
447def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000448 """Search upwards from the a directory (default: current) to find a file.
449
450 Returns nearest upper-level directory with the passed in file.
451 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000452 if not path:
453 path = os.getcwd()
454 path = os.path.realpath(path)
455 while True:
456 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000457 if os.path.exists(file_path):
458 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000459 (new_path, _) = os.path.split(path)
460 if new_path == path:
461 return None
462 path = new_path
463
464
465def GetGClientRootAndEntries(path=None):
466 """Returns the gclient root and the dict of entries."""
467 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000468 root = FindFileUpwards(config_file, path)
469 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000470 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000471 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000472 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000473 env = {}
474 execfile(config_path, env)
475 config_dir = os.path.dirname(config_path)
476 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000477
478
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000479def lockedmethod(method):
480 """Method decorator that holds self.lock for the duration of the call."""
481 def inner(self, *args, **kwargs):
482 try:
483 try:
484 self.lock.acquire()
485 except KeyboardInterrupt:
486 print >> sys.stderr, 'Was deadlocked'
487 raise
488 return method(self, *args, **kwargs)
489 finally:
490 self.lock.release()
491 return inner
492
493
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000494class WorkItem(object):
495 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000496 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
497 # As a workaround, use a single lock. Yep you read it right. Single lock for
498 # all the 100 objects.
499 lock = threading.Lock()
500
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000501 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000502 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000503 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000504
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000505 def run(self, work_queue):
506 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000507 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000508 pass
509
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000510 @property
511 def name(self):
512 return self._name
513
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000514
515class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000516 """Runs a set of WorkItem that have interdependencies and were WorkItem are
517 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000518
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000519 In gclient's case, Dependencies sometime needs to be run out of order due to
520 From() keyword. This class manages that all the required dependencies are run
521 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000522
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000523 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000524 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000525 def __init__(self, jobs, progress):
526 """jobs specifies the number of concurrent tasks to allow. progress is a
527 Progress instance."""
528 # Set when a thread is done or a new item is enqueued.
529 self.ready_cond = threading.Condition()
530 # Maximum number of concurrent tasks.
531 self.jobs = jobs
532 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000533 self.queued = []
534 # List of strings representing each Dependency.name that was run.
535 self.ran = []
536 # List of items currently running.
537 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000538 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000539 self.exceptions = Queue.Queue()
540 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000541 self.progress = progress
542 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000543 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000544
545 def enqueue(self, d):
546 """Enqueue one Dependency to be executed later once its requirements are
547 satisfied.
548 """
549 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000550 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000551 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000552 self.queued.append(d)
553 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000554 logging.debug('enqueued(%s)' % d.name)
555 if self.progress:
556 self.progress._total = total + 1
557 self.progress.update(0)
558 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000559 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000560 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000561
562 def flush(self, *args, **kwargs):
563 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000564 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000565 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000566 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000567 while True:
568 # Check for task to run first, then wait.
569 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000570 if not self.exceptions.empty():
571 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000572 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000573 self._flush_terminated_threads()
574 if (not self.queued and not self.running or
575 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000576 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000577 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000578
579 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000580 for i in xrange(len(self.queued)):
581 # Verify its requirements.
582 for r in self.queued[i].requirements:
583 if not r in self.ran:
584 # Requirement not met.
585 break
586 else:
587 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000588 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000589 break
590 else:
591 # Couldn't find an item that could run. Break out the outher loop.
592 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000593
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000594 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000595 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000596 break
597 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000598 try:
599 self.ready_cond.wait(10)
600 except KeyboardInterrupt:
601 # Help debugging by printing some information:
602 print >> sys.stderr, (
603 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
604 'Running: %d') % (
605 self.jobs,
606 len(self.queued),
607 ', '.join(self.ran),
608 len(self.running)))
609 for i in self.queued:
610 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
611 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000612 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000613 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000614 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000615
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000616 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000617 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000618 # To get back the stack location correctly, the raise a, b, c form must be
619 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000620 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000621 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000622 if self.progress:
623 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000624
maruel@chromium.org3742c842010-09-09 19:27:14 +0000625 def _flush_terminated_threads(self):
626 """Flush threads that have terminated."""
627 running = self.running
628 self.running = []
629 for t in running:
630 if t.isAlive():
631 self.running.append(t)
632 else:
633 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000634 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000635 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000636 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000637 if t.item.name in self.ran:
638 raise Error(
639 'gclient is confused, "%s" is already in "%s"' % (
640 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000641 if not t.item.name in self.ran:
642 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000643
644 def _run_one_task(self, task_item, args, kwargs):
645 if self.jobs > 1:
646 # Start the thread.
647 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000648 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000649 self.running.append(new_thread)
650 new_thread.start()
651 else:
652 # Run the 'thread' inside the main thread. Don't try to catch any
653 # exception.
654 task_item.run(*args, **kwargs)
655 self.ran.append(task_item.name)
656 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000657 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000658
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000659 class _Worker(threading.Thread):
660 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000661 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000662 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000663 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000664 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000665 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000666 self.args = args
667 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000668
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000669 def run(self):
670 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000671 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000672 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000673 try:
674 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000675 except Exception:
676 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000677 logging.info('Caught exception in thread %s' % self.item.name)
678 logging.info(str(sys.exc_info()))
679 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000680 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000681
maruel@chromium.org3742c842010-09-09 19:27:14 +0000682 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000683 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000684 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000685 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000686 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000687
688
689def GetEditor(git):
690 """Returns the most plausible editor to use."""
691 if git:
692 editor = os.environ.get('GIT_EDITOR')
693 else:
694 editor = os.environ.get('SVN_EDITOR')
695 if not editor:
696 editor = os.environ.get('EDITOR')
697 if not editor:
698 if sys.platform.startswith('win'):
699 editor = 'notepad'
700 else:
701 editor = 'vim'
702 return editor
703
704
705def RunEditor(content, git):
706 """Opens up the default editor in the system to get the CL description."""
707 file_handle, filename = tempfile.mkstemp(text=True)
708 # Make sure CRLF is handled properly by requiring none.
709 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000710 print >> sys.stderr, (
711 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000712 fileobj = os.fdopen(file_handle, 'w')
713 # Still remove \r if present.
714 fileobj.write(re.sub('\r?\n', '\n', content))
715 fileobj.close()
716
717 try:
718 cmd = '%s %s' % (GetEditor(git), filename)
719 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
720 # Msysgit requires the usage of 'env' to be present.
721 cmd = 'env ' + cmd
722 try:
723 # shell=True to allow the shell to handle all forms of quotes in
724 # $EDITOR.
725 subprocess2.check_call(cmd, shell=True)
726 except subprocess2.CalledProcessError:
727 return None
728 return FileRead(filename)
729 finally:
730 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000731
732
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000733def UpgradeToHttps(url):
734 """Upgrades random urls to https://.
735
736 Do not touch unknown urls like ssh:// or git://.
737 Do not touch http:// urls with a port number,
738 Fixes invalid GAE url.
739 """
740 if not url:
741 return url
742 if not re.match(r'[a-z\-]+\://.*', url):
743 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
744 # relative url and will use http:///foo. Note that it defaults to http://
745 # for compatibility with naked url like "localhost:8080".
746 url = 'http://%s' % url
747 parsed = list(urlparse.urlparse(url))
748 # Do not automatically upgrade http to https if a port number is provided.
749 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
750 parsed[0] = 'https'
751 # Until GAE supports SNI, manually convert the url.
752 if parsed[1] == 'codereview.chromium.org':
753 parsed[1] = 'chromiumcodereview.appspot.com'
754 return urlparse.urlunparse(parsed)
755
756
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000757def ParseCodereviewSettingsContent(content):
758 """Process a codereview.settings file properly."""
759 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
760 try:
761 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
762 except ValueError:
763 raise Error(
764 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000765 def fix_url(key):
766 if keyvals.get(key):
767 keyvals[key] = UpgradeToHttps(keyvals[key])
768 fix_url('CODE_REVIEW_SERVER')
769 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000770 return keyvals