blob: 35f8b3bf44ef69ed02c72422c73125e7f671f2c1 [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.org167b9e62009-09-17 17:41:02 +00007import errno
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +00008import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00009import os
maruel@chromium.org3742c842010-09-09 19:27:14 +000010import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000011import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000012import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000013import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000014import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000015import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000016import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000017import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000018
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000019import subprocess2
20
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000021
maruel@chromium.org66c83e62010-09-07 14:18:45 +000022class Error(Exception):
23 """gclient exception class."""
24 pass
25
26
msb@chromium.orgac915bb2009-11-13 17:03:01 +000027def SplitUrlRevision(url):
28 """Splits url and returns a two-tuple: url, rev"""
29 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000030 # Make sure ssh://user-name@example.com/~/test.git@stable works
31 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000032 components = re.search(regex, url).groups()
33 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000034 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000035 if len(components) == 1:
36 components += [None]
37 return tuple(components)
38
39
floitsch@google.comeaab7842011-04-28 09:07:58 +000040def IsDateRevision(revision):
41 """Returns true if the given revision is of the form "{ ... }"."""
42 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
43
44
45def MakeDateRevision(date):
46 """Returns a revision representing the latest revision before the given
47 date."""
48 return "{" + date + "}"
49
50
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000051def SyntaxErrorToError(filename, e):
52 """Raises a gclient_utils.Error exception with the human readable message"""
53 try:
54 # Try to construct a human readable error message
55 if filename:
56 error_message = 'There is a syntax error in %s\n' % filename
57 else:
58 error_message = 'There is a syntax error\n'
59 error_message += 'Line #%s, character %s: "%s"' % (
60 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
61 except:
62 # Something went wrong, re-raise the original exception
63 raise e
64 else:
65 raise Error(error_message)
66
67
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000068class PrintableObject(object):
69 def __str__(self):
70 output = ''
71 for i in dir(self):
72 if i.startswith('__'):
73 continue
74 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
75 return output
76
77
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000078def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000079 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000080 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000081 try:
82 content = f.read()
83 finally:
84 f.close()
85 return content
86
87
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000088def FileWrite(filename, content, mode='w'):
89 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000090 try:
91 f.write(content)
92 finally:
93 f.close()
94
95
maruel@chromium.orgf9040722011-03-09 14:47:51 +000096def rmtree(path):
97 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000098
maruel@chromium.orgf9040722011-03-09 14:47:51 +000099 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000100
101 shutil.rmtree() doesn't work on Windows if any of the files or directories
102 are read-only, which svn repositories and some .svn files are. We need to
103 be able to force the files to be writable (i.e., deletable) as we traverse
104 the tree.
105
106 Even with all this, Windows still sometimes fails to delete a file, citing
107 a permission error (maybe something to do with antivirus scans or disk
108 indexing). The best suggestion any of the user forums had was to wait a
109 bit and try again, so we do that too. It's hand-waving, but sometimes it
110 works. :/
111
112 On POSIX systems, things are a little bit simpler. The modes of the files
113 to be deleted doesn't matter, only the modes of the directories containing
114 them are significant. As the directory tree is traversed, each directory
115 has its mode set appropriately before descending into it. This should
116 result in the entire tree being removed, with the possible exception of
117 *path itself, because nothing attempts to change the mode of its parent.
118 Doing so would be hazardous, as it's not a directory slated for removal.
119 In the ordinary case, this is not a problem: for our purposes, the user
120 will never lack write permission on *path's parent.
121 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000122 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000123 return
124
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000125 if os.path.islink(path) or not os.path.isdir(path):
126 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000127
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000128 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000129 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000130 win32api = None
131 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000132 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000133 # Unable to import 'XX'
134 # pylint: disable=F0401
135 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000136 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000137 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000138 else:
139 # On POSIX systems, we need the x-bit set on the directory to access it,
140 # the r-bit to see its contents, and the w-bit to remove files from it.
141 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000142 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000143
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000144 def remove(func, subpath):
145 if sys.platform == 'win32':
146 os.chmod(subpath, stat.S_IWRITE)
147 if win32api and win32con:
148 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
149 try:
150 func(subpath)
151 except OSError, e:
152 if e.errno != errno.EACCES or sys.platform != 'win32':
153 raise
154 # Failed to delete, try again after a 100ms sleep.
155 time.sleep(0.1)
156 func(subpath)
157
158 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159 # If fullpath is a symbolic link that points to a directory, isdir will
160 # be True, but we don't want to descend into that as a directory, we just
161 # want to remove the link. Check islink and treat links as ordinary files
162 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000163 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000164 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000165 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000166 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000167 # Recurse.
168 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000169
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000170 remove(os.rmdir, path)
171
172# TODO(maruel): Rename the references.
173RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000174
175
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000176def safe_makedirs(tree):
177 """Creates the directory in a safe manner.
178
179 Because multiple threads can create these directories concurently, trap the
180 exception and pass on.
181 """
182 count = 0
183 while not os.path.exists(tree):
184 count += 1
185 try:
186 os.makedirs(tree)
187 except OSError, e:
188 # 17 POSIX, 183 Windows
189 if e.errno not in (17, 183):
190 raise
191 if count > 40:
192 # Give up.
193 raise
194
195
maruel@chromium.org17d01792010-09-01 18:07:10 +0000196def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
197 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000198
maruel@chromium.org17d01792010-09-01 18:07:10 +0000199 If |always| is True, a message indicating what is being done
200 is printed to stdout all the time even if not output is generated. Otherwise
201 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000202 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000203 stdout = kwargs.get('stdout', None) or sys.stdout
204 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000205 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000206 % (' '.join(args), kwargs.get('cwd', '.')))
207 else:
208 filter_fn = kwargs.get('filter_fn', None)
209 def filter_msg(line):
210 if line is None:
211 stdout.write('\n________ running \'%s\' in \'%s\'\n'
212 % (' '.join(args), kwargs.get('cwd', '.')))
213 elif filter_fn:
214 filter_fn(line)
215 kwargs['filter_fn'] = filter_msg
216 kwargs['call_filter_on_first_line'] = True
217 # Obviously.
218 kwargs['print_stdout'] = True
219 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220
maruel@chromium.org17d01792010-09-01 18:07:10 +0000221
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000222class Wrapper(object):
223 """Wraps an object, acting as a transparent proxy for all properties by
224 default.
225 """
226 def __init__(self, wrapped):
227 self._wrapped = wrapped
228
229 def __getattr__(self, name):
230 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000231
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000232
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000233class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000234 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000235 def __init__(self, wrapped, delay):
236 super(AutoFlush, self).__init__(wrapped)
237 if not hasattr(self, 'lock'):
238 self.lock = threading.Lock()
239 self.__last_flushed_at = time.time()
240 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000241
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000242 @property
243 def autoflush(self):
244 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000245
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000246 def write(self, out, *args, **kwargs):
247 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000248 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000249 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000250 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000251 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000252 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000253 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000254 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000255 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000256 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000257 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000258
259
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000260class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000261 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000262 threads with a NN> prefix.
263 """
264 def __init__(self, wrapped, include_zero=False):
265 super(Annotated, self).__init__(wrapped)
266 if not hasattr(self, 'lock'):
267 self.lock = threading.Lock()
268 self.__output_buffers = {}
269 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000270
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000271 @property
272 def annotated(self):
273 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000274
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000275 def write(self, out):
276 index = getattr(threading.currentThread(), 'index', 0)
277 if not index and not self.__include_zero:
278 # Unindexed threads aren't buffered.
279 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000280
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000281 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000282 try:
283 # Use a dummy array to hold the string so the code can be lockless.
284 # Strings are immutable, requiring to keep a lock for the whole dictionary
285 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000286 if not index in self.__output_buffers:
287 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000288 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000289 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000290 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000291 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000292
293 # Continue lockless.
294 obj[0] += out
295 while '\n' in obj[0]:
296 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000297 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000298 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000299 obj[0] = remaining
300
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000301 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000302 """Flush buffered output."""
303 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000304 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000305 try:
306 # Detect threads no longer existing.
307 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000308 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000309 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000310 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000311 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000312 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000313 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000314 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000315 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000316
317 # Don't keep the lock while writting. Will append \n when it shouldn't.
318 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000319 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000320 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
321 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000322
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000323
324def MakeFileAutoFlush(fileobj, delay=10):
325 autoflush = getattr(fileobj, 'autoflush', None)
326 if autoflush:
327 autoflush.delay = delay
328 return fileobj
329 return AutoFlush(fileobj, delay)
330
331
332def MakeFileAnnotated(fileobj, include_zero=False):
333 if getattr(fileobj, 'annotated', None):
334 return fileobj
335 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000336
337
maruel@chromium.org17d01792010-09-01 18:07:10 +0000338def CheckCallAndFilter(args, stdout=None, filter_fn=None,
339 print_stdout=None, call_filter_on_first_line=False,
340 **kwargs):
341 """Runs a command and calls back a filter function if needed.
342
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000343 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000344 print_stdout: If True, the command's stdout is forwarded to stdout.
345 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000346 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000347 character trimmed.
348 stdout: Can be any bufferable output.
349
350 stderr is always redirected to stdout.
351 """
352 assert print_stdout or filter_fn
353 stdout = stdout or sys.stdout
354 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000355 kid = subprocess2.Popen(
356 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
357 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000358
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000359 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000360 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000361
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000362 # Also, we need to forward stdout to prevent weird re-ordering of output.
363 # This has to be done on a per byte basis to make sure it is not buffered:
364 # normally buffering is done for each line, but if svn requests input, no
365 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000366 try:
367 in_byte = kid.stdout.read(1)
368 if in_byte:
369 if call_filter_on_first_line:
370 filter_fn(None)
371 in_line = ''
372 while in_byte:
373 if in_byte != '\r':
374 if print_stdout:
375 stdout.write(in_byte)
376 if in_byte != '\n':
377 in_line += in_byte
378 else:
379 filter_fn(in_line)
380 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000381 else:
382 filter_fn(in_line)
383 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000384 in_byte = kid.stdout.read(1)
385 # Flush the rest of buffered output. This is only an issue with
386 # stdout/stderr not ending with a \n.
387 if len(in_line):
388 filter_fn(in_line)
389 rv = kid.wait()
390 except KeyboardInterrupt:
391 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
392 raise
393
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000394 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000395 raise subprocess2.CalledProcessError(
396 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000397 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000398
399
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000400def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000401 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000402 real_from_dir = os.path.realpath(from_dir)
403 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000404 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000405 split_path = os.path.split(path)
406 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000407 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000408 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000409
410 # If we did not find the file in the current directory, make sure we are in a
411 # sub directory that is controlled by this configuration.
412 if path != real_from_dir:
413 entries_filename = os.path.join(path, filename + '_entries')
414 if not os.path.exists(entries_filename):
415 # If .gclient_entries does not exist, a previous call to gclient sync
416 # might have failed. In that case, we cannot verify that the .gclient
417 # is the one we want to use. In order to not to cause too much trouble,
418 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000419 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000420 "file you want to use" % (filename, path))
421 return path
422 scope = {}
423 try:
424 exec(FileRead(entries_filename), scope)
425 except SyntaxError, e:
426 SyntaxErrorToError(filename, e)
427 all_directories = scope['entries'].keys()
428 path_to_check = real_from_dir[len(path)+1:]
429 while path_to_check:
430 if path_to_check in all_directories:
431 return path
432 path_to_check = os.path.dirname(path_to_check)
433 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000434
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000435 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000436 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000437
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000438
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000439def PathDifference(root, subpath):
440 """Returns the difference subpath minus root."""
441 root = os.path.realpath(root)
442 subpath = os.path.realpath(subpath)
443 if not subpath.startswith(root):
444 return None
445 # If the root does not have a trailing \ or /, we add it so the returned
446 # path starts immediately after the seperator regardless of whether it is
447 # provided.
448 root = os.path.join(root, '')
449 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000450
451
452def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000453 """Search upwards from the a directory (default: current) to find a file.
454
455 Returns nearest upper-level directory with the passed in file.
456 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000457 if not path:
458 path = os.getcwd()
459 path = os.path.realpath(path)
460 while True:
461 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000462 if os.path.exists(file_path):
463 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000464 (new_path, _) = os.path.split(path)
465 if new_path == path:
466 return None
467 path = new_path
468
469
470def GetGClientRootAndEntries(path=None):
471 """Returns the gclient root and the dict of entries."""
472 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000473 root = FindFileUpwards(config_file, path)
474 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000475 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000476 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000477 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000478 env = {}
479 execfile(config_path, env)
480 config_dir = os.path.dirname(config_path)
481 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000482
483
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000484def lockedmethod(method):
485 """Method decorator that holds self.lock for the duration of the call."""
486 def inner(self, *args, **kwargs):
487 try:
488 try:
489 self.lock.acquire()
490 except KeyboardInterrupt:
491 print >> sys.stderr, 'Was deadlocked'
492 raise
493 return method(self, *args, **kwargs)
494 finally:
495 self.lock.release()
496 return inner
497
498
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000499class WorkItem(object):
500 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000501 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
502 # As a workaround, use a single lock. Yep you read it right. Single lock for
503 # all the 100 objects.
504 lock = threading.Lock()
505
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000506 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000507 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000508 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000509
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000510 def run(self, work_queue):
511 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000512 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000513 pass
514
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000515 @property
516 def name(self):
517 return self._name
518
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000519
520class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000521 """Runs a set of WorkItem that have interdependencies and were WorkItem are
522 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000523
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000524 In gclient's case, Dependencies sometime needs to be run out of order due to
525 From() keyword. This class manages that all the required dependencies are run
526 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000527
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000528 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000529 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000530 def __init__(self, jobs, progress):
531 """jobs specifies the number of concurrent tasks to allow. progress is a
532 Progress instance."""
533 # Set when a thread is done or a new item is enqueued.
534 self.ready_cond = threading.Condition()
535 # Maximum number of concurrent tasks.
536 self.jobs = jobs
537 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000538 self.queued = []
539 # List of strings representing each Dependency.name that was run.
540 self.ran = []
541 # List of items currently running.
542 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000543 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000544 self.exceptions = Queue.Queue()
545 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000546 self.progress = progress
547 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000548 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000549
550 def enqueue(self, d):
551 """Enqueue one Dependency to be executed later once its requirements are
552 satisfied.
553 """
554 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000555 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000556 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000557 self.queued.append(d)
558 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000559 logging.debug('enqueued(%s)' % d.name)
560 if self.progress:
561 self.progress._total = total + 1
562 self.progress.update(0)
563 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000564 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000565 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000566
567 def flush(self, *args, **kwargs):
568 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000569 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000570 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000571 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000572 while True:
573 # Check for task to run first, then wait.
574 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000575 if not self.exceptions.empty():
576 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000577 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000578 self._flush_terminated_threads()
579 if (not self.queued and not self.running or
580 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000581 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000582 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000583
584 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000585 for i in xrange(len(self.queued)):
586 # Verify its requirements.
587 for r in self.queued[i].requirements:
588 if not r in self.ran:
589 # Requirement not met.
590 break
591 else:
592 # 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'
756 # Until GAE supports SNI, manually convert the url.
757 if parsed[1] == 'codereview.chromium.org':
758 parsed[1] = 'chromiumcodereview.appspot.com'
759 return urlparse.urlunparse(parsed)
760
761
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000762def ParseCodereviewSettingsContent(content):
763 """Process a codereview.settings file properly."""
764 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
765 try:
766 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
767 except ValueError:
768 raise Error(
769 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000770 def fix_url(key):
771 if keyvals.get(key):
772 keyvals[key] = UpgradeToHttps(keyvals[key])
773 fix_url('CODE_REVIEW_SERVER')
774 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000775 return keyvals