blob: 0eeb185dc1481335ec9b4e5c885a585760d80485 [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.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +000083 s = f.read()
84 try:
85 return s.decode('utf-8')
86 except UnicodeDecodeError:
87 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000088
89
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000090def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000091 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000092 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000093
94
maruel@chromium.orgf9040722011-03-09 14:47:51 +000095def rmtree(path):
96 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000097
maruel@chromium.orgf9040722011-03-09 14:47:51 +000098 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000099
100 shutil.rmtree() doesn't work on Windows if any of the files or directories
101 are read-only, which svn repositories and some .svn files are. We need to
102 be able to force the files to be writable (i.e., deletable) as we traverse
103 the tree.
104
105 Even with all this, Windows still sometimes fails to delete a file, citing
106 a permission error (maybe something to do with antivirus scans or disk
107 indexing). The best suggestion any of the user forums had was to wait a
108 bit and try again, so we do that too. It's hand-waving, but sometimes it
109 works. :/
110
111 On POSIX systems, things are a little bit simpler. The modes of the files
112 to be deleted doesn't matter, only the modes of the directories containing
113 them are significant. As the directory tree is traversed, each directory
114 has its mode set appropriately before descending into it. This should
115 result in the entire tree being removed, with the possible exception of
116 *path itself, because nothing attempts to change the mode of its parent.
117 Doing so would be hazardous, as it's not a directory slated for removal.
118 In the ordinary case, this is not a problem: for our purposes, the user
119 will never lack write permission on *path's parent.
120 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000121 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122 return
123
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000124 if os.path.islink(path) or not os.path.isdir(path):
125 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000126
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000127 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000128 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000129 win32api = None
130 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000131 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000132 # Unable to import 'XX'
133 # pylint: disable=F0401
134 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000135 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000136 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000137 else:
138 # On POSIX systems, we need the x-bit set on the directory to access it,
139 # the r-bit to see its contents, and the w-bit to remove files from it.
140 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000141 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000142
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000143 def remove(func, subpath):
144 if sys.platform == 'win32':
145 os.chmod(subpath, stat.S_IWRITE)
146 if win32api and win32con:
147 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
148 try:
149 func(subpath)
150 except OSError, e:
151 if e.errno != errno.EACCES or sys.platform != 'win32':
152 raise
153 # Failed to delete, try again after a 100ms sleep.
154 time.sleep(0.1)
155 func(subpath)
156
157 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000158 # If fullpath is a symbolic link that points to a directory, isdir will
159 # be True, but we don't want to descend into that as a directory, we just
160 # want to remove the link. Check islink and treat links as ordinary files
161 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000162 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000163 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000164 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000165 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000166 # Recurse.
167 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000168
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000169 remove(os.rmdir, path)
170
171# TODO(maruel): Rename the references.
172RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000173
174
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000175def safe_makedirs(tree):
176 """Creates the directory in a safe manner.
177
178 Because multiple threads can create these directories concurently, trap the
179 exception and pass on.
180 """
181 count = 0
182 while not os.path.exists(tree):
183 count += 1
184 try:
185 os.makedirs(tree)
186 except OSError, e:
187 # 17 POSIX, 183 Windows
188 if e.errno not in (17, 183):
189 raise
190 if count > 40:
191 # Give up.
192 raise
193
194
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000195def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000196 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000197
maruel@chromium.org17d01792010-09-01 18:07:10 +0000198 If |always| is True, a message indicating what is being done
199 is printed to stdout all the time even if not output is generated. Otherwise
200 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000201 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000202 stdout = kwargs.setdefault('stdout', sys.stdout)
203 if header is None:
204 header = "\n________ running '%s' in '%s'\n" % (
205 ' '.join(args), kwargs.get('cwd', '.'))
206
maruel@chromium.org17d01792010-09-01 18:07:10 +0000207 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000208 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000209 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000210 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000211 def filter_msg(line):
212 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000213 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000214 elif filter_fn:
215 filter_fn(line)
216 kwargs['filter_fn'] = filter_msg
217 kwargs['call_filter_on_first_line'] = True
218 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000219 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000220 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000221
maruel@chromium.org17d01792010-09-01 18:07:10 +0000222
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000223class Wrapper(object):
224 """Wraps an object, acting as a transparent proxy for all properties by
225 default.
226 """
227 def __init__(self, wrapped):
228 self._wrapped = wrapped
229
230 def __getattr__(self, name):
231 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000232
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000233
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000234class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000235 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000236 def __init__(self, wrapped, delay):
237 super(AutoFlush, self).__init__(wrapped)
238 if not hasattr(self, 'lock'):
239 self.lock = threading.Lock()
240 self.__last_flushed_at = time.time()
241 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000242
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000243 @property
244 def autoflush(self):
245 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000246
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000247 def write(self, out, *args, **kwargs):
248 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000249 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000250 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000251 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000252 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000253 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000254 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000255 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000256 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000257 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000258 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000259
260
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000261class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000262 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000263 threads with a NN> prefix.
264 """
265 def __init__(self, wrapped, include_zero=False):
266 super(Annotated, self).__init__(wrapped)
267 if not hasattr(self, 'lock'):
268 self.lock = threading.Lock()
269 self.__output_buffers = {}
270 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000271
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000272 @property
273 def annotated(self):
274 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000275
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000276 def write(self, out):
277 index = getattr(threading.currentThread(), 'index', 0)
278 if not index and not self.__include_zero:
279 # Unindexed threads aren't buffered.
280 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000281
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000282 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000283 try:
284 # Use a dummy array to hold the string so the code can be lockless.
285 # Strings are immutable, requiring to keep a lock for the whole dictionary
286 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000287 if not index in self.__output_buffers:
288 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000289 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000290 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000291 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000292 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000293
294 # Continue lockless.
295 obj[0] += out
296 while '\n' in obj[0]:
297 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000298 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000299 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000300 obj[0] = remaining
301
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000302 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000303 """Flush buffered output."""
304 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000305 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000306 try:
307 # Detect threads no longer existing.
308 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000309 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000310 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000311 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000312 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000313 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000314 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000315 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000316 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000317
318 # Don't keep the lock while writting. Will append \n when it shouldn't.
319 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000320 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000321 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
322 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000323
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000324
325def MakeFileAutoFlush(fileobj, delay=10):
326 autoflush = getattr(fileobj, 'autoflush', None)
327 if autoflush:
328 autoflush.delay = delay
329 return fileobj
330 return AutoFlush(fileobj, delay)
331
332
333def MakeFileAnnotated(fileobj, include_zero=False):
334 if getattr(fileobj, 'annotated', None):
335 return fileobj
336 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000337
338
maruel@chromium.org17d01792010-09-01 18:07:10 +0000339def CheckCallAndFilter(args, stdout=None, filter_fn=None,
340 print_stdout=None, call_filter_on_first_line=False,
341 **kwargs):
342 """Runs a command and calls back a filter function if needed.
343
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000344 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000345 print_stdout: If True, the command's stdout is forwarded to stdout.
346 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000347 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000348 character trimmed.
349 stdout: Can be any bufferable output.
350
351 stderr is always redirected to stdout.
352 """
353 assert print_stdout or filter_fn
354 stdout = stdout or sys.stdout
355 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000356 kid = subprocess2.Popen(
357 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
358 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000359
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000360 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000361 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000362
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000363 # Also, we need to forward stdout to prevent weird re-ordering of output.
364 # This has to be done on a per byte basis to make sure it is not buffered:
365 # normally buffering is done for each line, but if svn requests input, no
366 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000367 try:
368 in_byte = kid.stdout.read(1)
369 if in_byte:
370 if call_filter_on_first_line:
371 filter_fn(None)
372 in_line = ''
373 while in_byte:
374 if in_byte != '\r':
375 if print_stdout:
376 stdout.write(in_byte)
377 if in_byte != '\n':
378 in_line += in_byte
379 else:
380 filter_fn(in_line)
381 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000382 else:
383 filter_fn(in_line)
384 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000385 in_byte = kid.stdout.read(1)
386 # Flush the rest of buffered output. This is only an issue with
387 # stdout/stderr not ending with a \n.
388 if len(in_line):
389 filter_fn(in_line)
390 rv = kid.wait()
391 except KeyboardInterrupt:
392 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
393 raise
394
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000395 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000396 raise subprocess2.CalledProcessError(
397 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000398 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000399
400
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000401def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000402 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000403 real_from_dir = os.path.realpath(from_dir)
404 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000405 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000406 split_path = os.path.split(path)
407 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000408 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000409 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000410
411 # If we did not find the file in the current directory, make sure we are in a
412 # sub directory that is controlled by this configuration.
413 if path != real_from_dir:
414 entries_filename = os.path.join(path, filename + '_entries')
415 if not os.path.exists(entries_filename):
416 # If .gclient_entries does not exist, a previous call to gclient sync
417 # might have failed. In that case, we cannot verify that the .gclient
418 # is the one we want to use. In order to not to cause too much trouble,
419 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000420 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000421 "file you want to use" % (filename, path))
422 return path
423 scope = {}
424 try:
425 exec(FileRead(entries_filename), scope)
426 except SyntaxError, e:
427 SyntaxErrorToError(filename, e)
428 all_directories = scope['entries'].keys()
429 path_to_check = real_from_dir[len(path)+1:]
430 while path_to_check:
431 if path_to_check in all_directories:
432 return path
433 path_to_check = os.path.dirname(path_to_check)
434 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000435
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000436 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000437 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000438
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000439
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000440def PathDifference(root, subpath):
441 """Returns the difference subpath minus root."""
442 root = os.path.realpath(root)
443 subpath = os.path.realpath(subpath)
444 if not subpath.startswith(root):
445 return None
446 # If the root does not have a trailing \ or /, we add it so the returned
447 # path starts immediately after the seperator regardless of whether it is
448 # provided.
449 root = os.path.join(root, '')
450 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000451
452
453def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000454 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000455
rcui@google.com13595ff2011-10-13 01:25:07 +0000456 Returns nearest upper-level directory with the passed in file.
457 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000458 if not path:
459 path = os.getcwd()
460 path = os.path.realpath(path)
461 while True:
462 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000463 if os.path.exists(file_path):
464 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000465 (new_path, _) = os.path.split(path)
466 if new_path == path:
467 return None
468 path = new_path
469
470
471def GetGClientRootAndEntries(path=None):
472 """Returns the gclient root and the dict of entries."""
473 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000474 root = FindFileUpwards(config_file, path)
475 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000476 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000477 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000478 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000479 env = {}
480 execfile(config_path, env)
481 config_dir = os.path.dirname(config_path)
482 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000483
484
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000485def lockedmethod(method):
486 """Method decorator that holds self.lock for the duration of the call."""
487 def inner(self, *args, **kwargs):
488 try:
489 try:
490 self.lock.acquire()
491 except KeyboardInterrupt:
492 print >> sys.stderr, 'Was deadlocked'
493 raise
494 return method(self, *args, **kwargs)
495 finally:
496 self.lock.release()
497 return inner
498
499
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000500class WorkItem(object):
501 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000502 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
503 # As a workaround, use a single lock. Yep you read it right. Single lock for
504 # all the 100 objects.
505 lock = threading.Lock()
506
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000507 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000508 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000509 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000510
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000511 def run(self, work_queue):
512 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000513 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000514 pass
515
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000516 @property
517 def name(self):
518 return self._name
519
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000520
521class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000522 """Runs a set of WorkItem that have interdependencies and were WorkItem are
523 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000524
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000525 In gclient's case, Dependencies sometime needs to be run out of order due to
526 From() keyword. This class manages that all the required dependencies are run
527 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000528
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000529 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000530 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000531 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000532 """jobs specifies the number of concurrent tasks to allow. progress is a
533 Progress instance."""
534 # Set when a thread is done or a new item is enqueued.
535 self.ready_cond = threading.Condition()
536 # Maximum number of concurrent tasks.
537 self.jobs = jobs
538 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000539 self.queued = []
540 # List of strings representing each Dependency.name that was run.
541 self.ran = []
542 # List of items currently running.
543 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000544 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000545 self.exceptions = Queue.Queue()
546 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000547 self.progress = progress
548 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000549 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000550
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000551 self.ignore_requirements = ignore_requirements
552
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000553 def enqueue(self, d):
554 """Enqueue one Dependency to be executed later once its requirements are
555 satisfied.
556 """
557 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000558 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000559 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000560 self.queued.append(d)
561 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000562 logging.debug('enqueued(%s)' % d.name)
563 if self.progress:
564 self.progress._total = total + 1
565 self.progress.update(0)
566 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000567 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000568 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000569
570 def flush(self, *args, **kwargs):
571 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000572 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000573 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000574 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000575 while True:
576 # Check for task to run first, then wait.
577 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000578 if not self.exceptions.empty():
579 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000580 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000581 self._flush_terminated_threads()
582 if (not self.queued and not self.running or
583 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000584 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000585 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000586
587 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000588 for i in xrange(len(self.queued)):
589 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000590 if (self.ignore_requirements or
591 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000592 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000593 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000594 break
595 else:
596 # Couldn't find an item that could run. Break out the outher loop.
597 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000598
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000599 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000600 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000601 break
602 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000603 try:
604 self.ready_cond.wait(10)
605 except KeyboardInterrupt:
606 # Help debugging by printing some information:
607 print >> sys.stderr, (
608 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
609 'Running: %d') % (
610 self.jobs,
611 len(self.queued),
612 ', '.join(self.ran),
613 len(self.running)))
614 for i in self.queued:
615 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
616 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000617 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000618 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000619 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000620
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000621 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000622 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000623 # To get back the stack location correctly, the raise a, b, c form must be
624 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000625 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000626 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000627 if self.progress:
628 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000629
maruel@chromium.org3742c842010-09-09 19:27:14 +0000630 def _flush_terminated_threads(self):
631 """Flush threads that have terminated."""
632 running = self.running
633 self.running = []
634 for t in running:
635 if t.isAlive():
636 self.running.append(t)
637 else:
638 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000639 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000640 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000641 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000642 if t.item.name in self.ran:
643 raise Error(
644 'gclient is confused, "%s" is already in "%s"' % (
645 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000646 if not t.item.name in self.ran:
647 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000648
649 def _run_one_task(self, task_item, args, kwargs):
650 if self.jobs > 1:
651 # Start the thread.
652 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000653 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000654 self.running.append(new_thread)
655 new_thread.start()
656 else:
657 # Run the 'thread' inside the main thread. Don't try to catch any
658 # exception.
659 task_item.run(*args, **kwargs)
660 self.ran.append(task_item.name)
661 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000662 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000663
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000664 class _Worker(threading.Thread):
665 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000666 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000667 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000668 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000669 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000670 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000671 self.args = args
672 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000673
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000674 def run(self):
675 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000676 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000677 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000678 try:
679 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000680 except Exception:
681 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000682 logging.info('Caught exception in thread %s' % self.item.name)
683 logging.info(str(sys.exc_info()))
684 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000685 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000686
maruel@chromium.org3742c842010-09-09 19:27:14 +0000687 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000688 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000689 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000690 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000691 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000692
693
694def GetEditor(git):
695 """Returns the most plausible editor to use."""
696 if git:
697 editor = os.environ.get('GIT_EDITOR')
698 else:
699 editor = os.environ.get('SVN_EDITOR')
700 if not editor:
701 editor = os.environ.get('EDITOR')
702 if not editor:
703 if sys.platform.startswith('win'):
704 editor = 'notepad'
705 else:
706 editor = 'vim'
707 return editor
708
709
710def RunEditor(content, git):
711 """Opens up the default editor in the system to get the CL description."""
712 file_handle, filename = tempfile.mkstemp(text=True)
713 # Make sure CRLF is handled properly by requiring none.
714 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000715 print >> sys.stderr, (
716 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000717 fileobj = os.fdopen(file_handle, 'w')
718 # Still remove \r if present.
719 fileobj.write(re.sub('\r?\n', '\n', content))
720 fileobj.close()
721
722 try:
723 cmd = '%s %s' % (GetEditor(git), filename)
724 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
725 # Msysgit requires the usage of 'env' to be present.
726 cmd = 'env ' + cmd
727 try:
728 # shell=True to allow the shell to handle all forms of quotes in
729 # $EDITOR.
730 subprocess2.check_call(cmd, shell=True)
731 except subprocess2.CalledProcessError:
732 return None
733 return FileRead(filename)
734 finally:
735 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000736
737
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000738def UpgradeToHttps(url):
739 """Upgrades random urls to https://.
740
741 Do not touch unknown urls like ssh:// or git://.
742 Do not touch http:// urls with a port number,
743 Fixes invalid GAE url.
744 """
745 if not url:
746 return url
747 if not re.match(r'[a-z\-]+\://.*', url):
748 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
749 # relative url and will use http:///foo. Note that it defaults to http://
750 # for compatibility with naked url like "localhost:8080".
751 url = 'http://%s' % url
752 parsed = list(urlparse.urlparse(url))
753 # Do not automatically upgrade http to https if a port number is provided.
754 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
755 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000756 return urlparse.urlunparse(parsed)
757
758
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000759def ParseCodereviewSettingsContent(content):
760 """Process a codereview.settings file properly."""
761 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
762 try:
763 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
764 except ValueError:
765 raise Error(
766 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000767 def fix_url(key):
768 if keyvals.get(key):
769 keyvals[key] = UpgradeToHttps(keyvals[key])
770 fix_url('CODE_REVIEW_SERVER')
771 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000772 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000773
774
775def NumLocalCpus():
776 """Returns the number of processors.
777
778 Python on OSX 10.6 raises a NotImplementedError exception.
779 """
780 try:
781 import multiprocessing
782 return multiprocessing.cpu_count()
783 except: # pylint: disable=W0702
784 # Mac OS 10.6 only
785 # pylint: disable=E1101
786 return int(os.sysconf('SC_NPROCESSORS_ONLN'))