blob: 9ba6da50c4b7c50926ca13aa937c84d94234f72f [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.orgdae209f2012-07-03 16:08:15 +000080 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
81 return f.read()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000082
83
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000084def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000085 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000086 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000087
88
maruel@chromium.orgf9040722011-03-09 14:47:51 +000089def rmtree(path):
90 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000091
maruel@chromium.orgf9040722011-03-09 14:47:51 +000092 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000093
94 shutil.rmtree() doesn't work on Windows if any of the files or directories
95 are read-only, which svn repositories and some .svn files are. We need to
96 be able to force the files to be writable (i.e., deletable) as we traverse
97 the tree.
98
99 Even with all this, Windows still sometimes fails to delete a file, citing
100 a permission error (maybe something to do with antivirus scans or disk
101 indexing). The best suggestion any of the user forums had was to wait a
102 bit and try again, so we do that too. It's hand-waving, but sometimes it
103 works. :/
104
105 On POSIX systems, things are a little bit simpler. The modes of the files
106 to be deleted doesn't matter, only the modes of the directories containing
107 them are significant. As the directory tree is traversed, each directory
108 has its mode set appropriately before descending into it. This should
109 result in the entire tree being removed, with the possible exception of
110 *path itself, because nothing attempts to change the mode of its parent.
111 Doing so would be hazardous, as it's not a directory slated for removal.
112 In the ordinary case, this is not a problem: for our purposes, the user
113 will never lack write permission on *path's parent.
114 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000115 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000116 return
117
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000118 if os.path.islink(path) or not os.path.isdir(path):
119 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000123 win32api = None
124 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000125 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000126 # Unable to import 'XX'
127 # pylint: disable=F0401
128 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000129 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000130 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000131 else:
132 # On POSIX systems, we need the x-bit set on the directory to access it,
133 # the r-bit to see its contents, and the w-bit to remove files from it.
134 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000135 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000136
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000137 def remove(func, subpath):
138 if sys.platform == 'win32':
139 os.chmod(subpath, stat.S_IWRITE)
140 if win32api and win32con:
141 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
142 try:
143 func(subpath)
144 except OSError, e:
145 if e.errno != errno.EACCES or sys.platform != 'win32':
146 raise
147 # Failed to delete, try again after a 100ms sleep.
148 time.sleep(0.1)
149 func(subpath)
150
151 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000152 # If fullpath is a symbolic link that points to a directory, isdir will
153 # be True, but we don't want to descend into that as a directory, we just
154 # want to remove the link. Check islink and treat links as ordinary files
155 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000156 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000157 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000158 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000160 # Recurse.
161 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000162
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000163 remove(os.rmdir, path)
164
165# TODO(maruel): Rename the references.
166RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000167
168
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000169def safe_makedirs(tree):
170 """Creates the directory in a safe manner.
171
172 Because multiple threads can create these directories concurently, trap the
173 exception and pass on.
174 """
175 count = 0
176 while not os.path.exists(tree):
177 count += 1
178 try:
179 os.makedirs(tree)
180 except OSError, e:
181 # 17 POSIX, 183 Windows
182 if e.errno not in (17, 183):
183 raise
184 if count > 40:
185 # Give up.
186 raise
187
188
maruel@chromium.org17d01792010-09-01 18:07:10 +0000189def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
190 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000191
maruel@chromium.org17d01792010-09-01 18:07:10 +0000192 If |always| is True, a message indicating what is being done
193 is printed to stdout all the time even if not output is generated. Otherwise
194 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000195 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000196 stdout = kwargs.get('stdout', None) or sys.stdout
197 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000198 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000199 % (' '.join(args), kwargs.get('cwd', '.')))
200 else:
201 filter_fn = kwargs.get('filter_fn', None)
202 def filter_msg(line):
203 if line is None:
204 stdout.write('\n________ running \'%s\' in \'%s\'\n'
205 % (' '.join(args), kwargs.get('cwd', '.')))
206 elif filter_fn:
207 filter_fn(line)
208 kwargs['filter_fn'] = filter_msg
209 kwargs['call_filter_on_first_line'] = True
210 # Obviously.
211 kwargs['print_stdout'] = True
212 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000213
maruel@chromium.org17d01792010-09-01 18:07:10 +0000214
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000215class Wrapper(object):
216 """Wraps an object, acting as a transparent proxy for all properties by
217 default.
218 """
219 def __init__(self, wrapped):
220 self._wrapped = wrapped
221
222 def __getattr__(self, name):
223 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000224
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000225
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000226class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000227 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000228 def __init__(self, wrapped, delay):
229 super(AutoFlush, self).__init__(wrapped)
230 if not hasattr(self, 'lock'):
231 self.lock = threading.Lock()
232 self.__last_flushed_at = time.time()
233 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000234
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000235 @property
236 def autoflush(self):
237 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000238
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000239 def write(self, out, *args, **kwargs):
240 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000241 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000242 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000243 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000244 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000245 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000246 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000247 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000248 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000249 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000250 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000251
252
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000253class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000254 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000255 threads with a NN> prefix.
256 """
257 def __init__(self, wrapped, include_zero=False):
258 super(Annotated, self).__init__(wrapped)
259 if not hasattr(self, 'lock'):
260 self.lock = threading.Lock()
261 self.__output_buffers = {}
262 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000263
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000264 @property
265 def annotated(self):
266 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000267
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000268 def write(self, out):
269 index = getattr(threading.currentThread(), 'index', 0)
270 if not index and not self.__include_zero:
271 # Unindexed threads aren't buffered.
272 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000273
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000274 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000275 try:
276 # Use a dummy array to hold the string so the code can be lockless.
277 # Strings are immutable, requiring to keep a lock for the whole dictionary
278 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000279 if not index in self.__output_buffers:
280 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000281 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000282 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000283 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000284 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000285
286 # Continue lockless.
287 obj[0] += out
288 while '\n' in obj[0]:
289 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000290 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000291 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000292 obj[0] = remaining
293
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000294 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000295 """Flush buffered output."""
296 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000297 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000298 try:
299 # Detect threads no longer existing.
300 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000301 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000302 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000303 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000304 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000305 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000306 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000307 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000308 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000309
310 # Don't keep the lock while writting. Will append \n when it shouldn't.
311 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000312 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000313 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
314 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000315
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000316
317def MakeFileAutoFlush(fileobj, delay=10):
318 autoflush = getattr(fileobj, 'autoflush', None)
319 if autoflush:
320 autoflush.delay = delay
321 return fileobj
322 return AutoFlush(fileobj, delay)
323
324
325def MakeFileAnnotated(fileobj, include_zero=False):
326 if getattr(fileobj, 'annotated', None):
327 return fileobj
328 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000329
330
maruel@chromium.org17d01792010-09-01 18:07:10 +0000331def CheckCallAndFilter(args, stdout=None, filter_fn=None,
332 print_stdout=None, call_filter_on_first_line=False,
333 **kwargs):
334 """Runs a command and calls back a filter function if needed.
335
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000336 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000337 print_stdout: If True, the command's stdout is forwarded to stdout.
338 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000339 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000340 character trimmed.
341 stdout: Can be any bufferable output.
342
343 stderr is always redirected to stdout.
344 """
345 assert print_stdout or filter_fn
346 stdout = stdout or sys.stdout
347 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000348 kid = subprocess2.Popen(
349 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
350 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000351
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000352 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000353 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000354
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000355 # Also, we need to forward stdout to prevent weird re-ordering of output.
356 # This has to be done on a per byte basis to make sure it is not buffered:
357 # normally buffering is done for each line, but if svn requests input, no
358 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000359 try:
360 in_byte = kid.stdout.read(1)
361 if in_byte:
362 if call_filter_on_first_line:
363 filter_fn(None)
364 in_line = ''
365 while in_byte:
366 if in_byte != '\r':
367 if print_stdout:
368 stdout.write(in_byte)
369 if in_byte != '\n':
370 in_line += in_byte
371 else:
372 filter_fn(in_line)
373 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000374 else:
375 filter_fn(in_line)
376 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000377 in_byte = kid.stdout.read(1)
378 # Flush the rest of buffered output. This is only an issue with
379 # stdout/stderr not ending with a \n.
380 if len(in_line):
381 filter_fn(in_line)
382 rv = kid.wait()
383 except KeyboardInterrupt:
384 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
385 raise
386
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000387 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000388 raise subprocess2.CalledProcessError(
389 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000390 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000391
392
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000393def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000394 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000395 real_from_dir = os.path.realpath(from_dir)
396 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000397 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000398 split_path = os.path.split(path)
399 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000400 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000401 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000402
403 # If we did not find the file in the current directory, make sure we are in a
404 # sub directory that is controlled by this configuration.
405 if path != real_from_dir:
406 entries_filename = os.path.join(path, filename + '_entries')
407 if not os.path.exists(entries_filename):
408 # If .gclient_entries does not exist, a previous call to gclient sync
409 # might have failed. In that case, we cannot verify that the .gclient
410 # is the one we want to use. In order to not to cause too much trouble,
411 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000412 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000413 "file you want to use" % (filename, path))
414 return path
415 scope = {}
416 try:
417 exec(FileRead(entries_filename), scope)
418 except SyntaxError, e:
419 SyntaxErrorToError(filename, e)
420 all_directories = scope['entries'].keys()
421 path_to_check = real_from_dir[len(path)+1:]
422 while path_to_check:
423 if path_to_check in all_directories:
424 return path
425 path_to_check = os.path.dirname(path_to_check)
426 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000427
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000428 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000429 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000430
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000431
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000432def PathDifference(root, subpath):
433 """Returns the difference subpath minus root."""
434 root = os.path.realpath(root)
435 subpath = os.path.realpath(subpath)
436 if not subpath.startswith(root):
437 return None
438 # If the root does not have a trailing \ or /, we add it so the returned
439 # path starts immediately after the seperator regardless of whether it is
440 # provided.
441 root = os.path.join(root, '')
442 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000443
444
445def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000446 """Search upwards from the a directory (default: current) to find a file.
447
448 Returns nearest upper-level directory with the passed in file.
449 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000450 if not path:
451 path = os.getcwd()
452 path = os.path.realpath(path)
453 while True:
454 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000455 if os.path.exists(file_path):
456 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000457 (new_path, _) = os.path.split(path)
458 if new_path == path:
459 return None
460 path = new_path
461
462
463def GetGClientRootAndEntries(path=None):
464 """Returns the gclient root and the dict of entries."""
465 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000466 root = FindFileUpwards(config_file, path)
467 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000468 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000469 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000470 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000471 env = {}
472 execfile(config_path, env)
473 config_dir = os.path.dirname(config_path)
474 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000475
476
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000477def lockedmethod(method):
478 """Method decorator that holds self.lock for the duration of the call."""
479 def inner(self, *args, **kwargs):
480 try:
481 try:
482 self.lock.acquire()
483 except KeyboardInterrupt:
484 print >> sys.stderr, 'Was deadlocked'
485 raise
486 return method(self, *args, **kwargs)
487 finally:
488 self.lock.release()
489 return inner
490
491
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000492class WorkItem(object):
493 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000494 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
495 # As a workaround, use a single lock. Yep you read it right. Single lock for
496 # all the 100 objects.
497 lock = threading.Lock()
498
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000499 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000500 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000501 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000502
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000503 def run(self, work_queue):
504 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000505 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000506 pass
507
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000508 @property
509 def name(self):
510 return self._name
511
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000512
513class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000514 """Runs a set of WorkItem that have interdependencies and were WorkItem are
515 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000516
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000517 In gclient's case, Dependencies sometime needs to be run out of order due to
518 From() keyword. This class manages that all the required dependencies are run
519 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000520
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000521 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000522 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000523 def __init__(self, jobs, progress):
524 """jobs specifies the number of concurrent tasks to allow. progress is a
525 Progress instance."""
526 # Set when a thread is done or a new item is enqueued.
527 self.ready_cond = threading.Condition()
528 # Maximum number of concurrent tasks.
529 self.jobs = jobs
530 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000531 self.queued = []
532 # List of strings representing each Dependency.name that was run.
533 self.ran = []
534 # List of items currently running.
535 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000536 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000537 self.exceptions = Queue.Queue()
538 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000539 self.progress = progress
540 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000541 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000542
543 def enqueue(self, d):
544 """Enqueue one Dependency to be executed later once its requirements are
545 satisfied.
546 """
547 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000548 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000549 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000550 self.queued.append(d)
551 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000552 logging.debug('enqueued(%s)' % d.name)
553 if self.progress:
554 self.progress._total = total + 1
555 self.progress.update(0)
556 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000557 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000558 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000559
560 def flush(self, *args, **kwargs):
561 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000562 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000563 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000564 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000565 while True:
566 # Check for task to run first, then wait.
567 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000568 if not self.exceptions.empty():
569 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000570 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000571 self._flush_terminated_threads()
572 if (not self.queued and not self.running or
573 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000574 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000575 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000576
577 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000578 for i in xrange(len(self.queued)):
579 # Verify its requirements.
580 for r in self.queued[i].requirements:
581 if not r in self.ran:
582 # Requirement not met.
583 break
584 else:
585 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000586 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000587 break
588 else:
589 # Couldn't find an item that could run. Break out the outher loop.
590 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000591
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000592 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000593 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000594 break
595 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000596 try:
597 self.ready_cond.wait(10)
598 except KeyboardInterrupt:
599 # Help debugging by printing some information:
600 print >> sys.stderr, (
601 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
602 'Running: %d') % (
603 self.jobs,
604 len(self.queued),
605 ', '.join(self.ran),
606 len(self.running)))
607 for i in self.queued:
608 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
609 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000610 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000611 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000612 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000613
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000614 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000615 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000616 # To get back the stack location correctly, the raise a, b, c form must be
617 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000618 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000619 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000620 if self.progress:
621 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000622
maruel@chromium.org3742c842010-09-09 19:27:14 +0000623 def _flush_terminated_threads(self):
624 """Flush threads that have terminated."""
625 running = self.running
626 self.running = []
627 for t in running:
628 if t.isAlive():
629 self.running.append(t)
630 else:
631 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000632 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000633 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000634 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000635 if t.item.name in self.ran:
636 raise Error(
637 'gclient is confused, "%s" is already in "%s"' % (
638 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000639 if not t.item.name in self.ran:
640 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000641
642 def _run_one_task(self, task_item, args, kwargs):
643 if self.jobs > 1:
644 # Start the thread.
645 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000646 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000647 self.running.append(new_thread)
648 new_thread.start()
649 else:
650 # Run the 'thread' inside the main thread. Don't try to catch any
651 # exception.
652 task_item.run(*args, **kwargs)
653 self.ran.append(task_item.name)
654 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000655 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000656
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000657 class _Worker(threading.Thread):
658 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000659 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000660 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000661 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000662 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000663 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000664 self.args = args
665 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000666
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000667 def run(self):
668 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000669 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000670 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000671 try:
672 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000673 except Exception:
674 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000675 logging.info('Caught exception in thread %s' % self.item.name)
676 logging.info(str(sys.exc_info()))
677 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000678 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000679
maruel@chromium.org3742c842010-09-09 19:27:14 +0000680 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000681 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000682 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000683 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000684 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000685
686
687def GetEditor(git):
688 """Returns the most plausible editor to use."""
689 if git:
690 editor = os.environ.get('GIT_EDITOR')
691 else:
692 editor = os.environ.get('SVN_EDITOR')
693 if not editor:
694 editor = os.environ.get('EDITOR')
695 if not editor:
696 if sys.platform.startswith('win'):
697 editor = 'notepad'
698 else:
699 editor = 'vim'
700 return editor
701
702
703def RunEditor(content, git):
704 """Opens up the default editor in the system to get the CL description."""
705 file_handle, filename = tempfile.mkstemp(text=True)
706 # Make sure CRLF is handled properly by requiring none.
707 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000708 print >> sys.stderr, (
709 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000710 fileobj = os.fdopen(file_handle, 'w')
711 # Still remove \r if present.
712 fileobj.write(re.sub('\r?\n', '\n', content))
713 fileobj.close()
714
715 try:
716 cmd = '%s %s' % (GetEditor(git), filename)
717 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
718 # Msysgit requires the usage of 'env' to be present.
719 cmd = 'env ' + cmd
720 try:
721 # shell=True to allow the shell to handle all forms of quotes in
722 # $EDITOR.
723 subprocess2.check_call(cmd, shell=True)
724 except subprocess2.CalledProcessError:
725 return None
726 return FileRead(filename)
727 finally:
728 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000729
730
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000731def UpgradeToHttps(url):
732 """Upgrades random urls to https://.
733
734 Do not touch unknown urls like ssh:// or git://.
735 Do not touch http:// urls with a port number,
736 Fixes invalid GAE url.
737 """
738 if not url:
739 return url
740 if not re.match(r'[a-z\-]+\://.*', url):
741 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
742 # relative url and will use http:///foo. Note that it defaults to http://
743 # for compatibility with naked url like "localhost:8080".
744 url = 'http://%s' % url
745 parsed = list(urlparse.urlparse(url))
746 # Do not automatically upgrade http to https if a port number is provided.
747 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
748 parsed[0] = 'https'
749 # Until GAE supports SNI, manually convert the url.
750 if parsed[1] == 'codereview.chromium.org':
751 parsed[1] = 'chromiumcodereview.appspot.com'
752 return urlparse.urlunparse(parsed)
753
754
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000755def ParseCodereviewSettingsContent(content):
756 """Process a codereview.settings file properly."""
757 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
758 try:
759 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
760 except ValueError:
761 raise Error(
762 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000763 def fix_url(key):
764 if keyvals.get(key):
765 keyvals[key] = UpgradeToHttps(keyvals[key])
766 fix_url('CODE_REVIEW_SERVER')
767 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000768 return keyvals