blob: 35f8b3bf44ef69ed02c72422c73125e7f671f2c1 [file] [log] [blame]
[email protected]99ac1c52012-01-16 14:52:121# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]06617272010-11-04 13:50:502# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
[email protected]5f3eee32009-09-17 00:34:304
[email protected]5aeb7dd2009-11-17 18:09:015"""Generic utils."""
6
[email protected]167b9e62009-09-17 17:41:027import errno
[email protected]d9141bf2009-12-23 16:13:328import logging
[email protected]5f3eee32009-09-17 00:34:309import os
[email protected]3742c842010-09-09 19:27:1410import Queue
[email protected]ac915bb2009-11-13 17:03:0111import re
[email protected]8f9c69f2009-09-17 00:48:2812import stat
[email protected]5f3eee32009-09-17 00:34:3013import sys
[email protected]0e0436a2011-10-25 13:32:4114import tempfile
[email protected]9e5317a2010-08-13 20:35:1115import threading
[email protected]167b9e62009-09-17 17:41:0216import time
[email protected]eb5edbc2012-01-16 17:03:2817import urlparse
[email protected]5f3eee32009-09-17 00:34:3018
[email protected]ca0f8392011-09-08 17:15:1519import subprocess2
20
[email protected]5f3eee32009-09-17 00:34:3021
[email protected]66c83e62010-09-07 14:18:4522class Error(Exception):
23 """gclient exception class."""
24 pass
25
26
[email protected]ac915bb2009-11-13 17:03:0127def SplitUrlRevision(url):
28 """Splits url and returns a two-tuple: url, rev"""
29 if url.startswith('ssh:'):
[email protected]78b8cd12010-10-26 12:47:0730 # Make sure ssh://[email protected]/~/test.git@stable works
31 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
[email protected]ac915bb2009-11-13 17:03:0132 components = re.search(regex, url).groups()
33 else:
[email protected]116704f2010-06-11 17:34:3834 components = url.split('@', 1)
[email protected]ac915bb2009-11-13 17:03:0135 if len(components) == 1:
36 components += [None]
37 return tuple(components)
38
39
[email protected]eaab7842011-04-28 09:07:5840def 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
[email protected]5990f9d2010-07-07 18:02:5851def 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
[email protected]5f3eee32009-09-17 00:34:3068class 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
[email protected]5aeb7dd2009-11-17 18:09:0178def FileRead(filename, mode='rU'):
[email protected]5f3eee32009-09-17 00:34:3079 content = None
[email protected]5aeb7dd2009-11-17 18:09:0180 f = open(filename, mode)
[email protected]5f3eee32009-09-17 00:34:3081 try:
82 content = f.read()
83 finally:
84 f.close()
85 return content
86
87
[email protected]5aeb7dd2009-11-17 18:09:0188def FileWrite(filename, content, mode='w'):
89 f = open(filename, mode)
[email protected]5f3eee32009-09-17 00:34:3090 try:
91 f.write(content)
92 finally:
93 f.close()
94
95
[email protected]f9040722011-03-09 14:47:5196def rmtree(path):
97 """shutil.rmtree() on steroids.
[email protected]5f3eee32009-09-17 00:34:3098
[email protected]f9040722011-03-09 14:47:5199 Recursively removes a directory, even if it's marked read-only.
[email protected]5f3eee32009-09-17 00:34:30100
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 """
[email protected]f9040722011-03-09 14:47:51122 if not os.path.exists(path):
[email protected]5f3eee32009-09-17 00:34:30123 return
124
[email protected]f9040722011-03-09 14:47:51125 if os.path.islink(path) or not os.path.isdir(path):
126 raise Error('Called rmtree(%s) in non-directory' % path)
[email protected]5f3eee32009-09-17 00:34:30127
[email protected]5f3eee32009-09-17 00:34:30128 if sys.platform == 'win32':
[email protected]5f3eee32009-09-17 00:34:30129 # Some people don't have the APIs installed. In that case we'll do without.
[email protected]1edee692011-03-12 19:39:13130 win32api = None
131 win32con = None
[email protected]5f3eee32009-09-17 00:34:30132 try:
[email protected]1edee692011-03-12 19:39:13133 # Unable to import 'XX'
134 # pylint: disable=F0401
135 import win32api, win32con
[email protected]5f3eee32009-09-17 00:34:30136 except ImportError:
[email protected]f9040722011-03-09 14:47:51137 pass
[email protected]5f3eee32009-09-17 00:34:30138 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.
[email protected]f9040722011-03-09 14:47:51142 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
[email protected]5f3eee32009-09-17 00:34:30143
[email protected]f9040722011-03-09 14:47:51144 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):
[email protected]5f3eee32009-09-17 00:34:30159 # 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.
[email protected]f9040722011-03-09 14:47:51163 fullpath = os.path.join(path, fn)
[email protected]5f3eee32009-09-17 00:34:30164 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
[email protected]f9040722011-03-09 14:47:51165 remove(os.remove, fullpath)
[email protected]5f3eee32009-09-17 00:34:30166 else:
[email protected]f9040722011-03-09 14:47:51167 # Recurse.
168 rmtree(fullpath)
[email protected]5f3eee32009-09-17 00:34:30169
[email protected]f9040722011-03-09 14:47:51170 remove(os.rmdir, path)
171
172# TODO(maruel): Rename the references.
173RemoveDirectory = rmtree
[email protected]5f3eee32009-09-17 00:34:30174
175
[email protected]6c48a302011-10-20 23:44:20176def 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
[email protected]17d01792010-09-01 18:07:10196def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
197 """Adds 'header' support to CheckCallAndFilter.
[email protected]5f3eee32009-09-17 00:34:30198
[email protected]17d01792010-09-01 18:07:10199 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.
[email protected]5f3eee32009-09-17 00:34:30202 """
[email protected]17d01792010-09-01 18:07:10203 stdout = kwargs.get('stdout', None) or sys.stdout
204 if always:
[email protected]559c3f82010-08-23 19:26:08205 stdout.write('\n________ running \'%s\' in \'%s\'\n'
[email protected]17d01792010-09-01 18:07:10206 % (' '.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)
[email protected]5f3eee32009-09-17 00:34:30220
[email protected]17d01792010-09-01 18:07:10221
[email protected]042f0e72011-10-23 00:04:35222class 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)
[email protected]db111f72010-09-08 13:36:53231
[email protected]e0de9cb2010-09-17 15:07:14232
[email protected]042f0e72011-10-23 00:04:35233class AutoFlush(Wrapper):
[email protected]e0de9cb2010-09-17 15:07:14234 """Creates a file object clone to automatically flush after N seconds."""
[email protected]042f0e72011-10-23 00:04:35235 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
[email protected]e0de9cb2010-09-17 15:07:14241
[email protected]042f0e72011-10-23 00:04:35242 @property
243 def autoflush(self):
244 return self
[email protected]e0de9cb2010-09-17 15:07:14245
[email protected]042f0e72011-10-23 00:04:35246 def write(self, out, *args, **kwargs):
247 self._wrapped.write(out, *args, **kwargs)
[email protected]db111f72010-09-08 13:36:53248 should_flush = False
[email protected]042f0e72011-10-23 00:04:35249 self.lock.acquire()
[email protected]9c531262010-09-08 13:41:13250 try:
[email protected]042f0e72011-10-23 00:04:35251 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
[email protected]db111f72010-09-08 13:36:53252 should_flush = True
[email protected]042f0e72011-10-23 00:04:35253 self.__last_flushed_at = time.time()
[email protected]9c531262010-09-08 13:41:13254 finally:
[email protected]042f0e72011-10-23 00:04:35255 self.lock.release()
[email protected]db111f72010-09-08 13:36:53256 if should_flush:
[email protected]042f0e72011-10-23 00:04:35257 self.flush()
[email protected]db111f72010-09-08 13:36:53258
259
[email protected]042f0e72011-10-23 00:04:35260class Annotated(Wrapper):
[email protected]4ed34182010-09-17 15:57:47261 """Creates a file object clone to automatically prepends every line in worker
[email protected]042f0e72011-10-23 00:04:35262 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
[email protected]cb1e97a2010-09-09 20:09:20270
[email protected]042f0e72011-10-23 00:04:35271 @property
272 def annotated(self):
273 return self
[email protected]cb1e97a2010-09-09 20:09:20274
[email protected]042f0e72011-10-23 00:04:35275 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)
[email protected]cb1e97a2010-09-09 20:09:20280
[email protected]042f0e72011-10-23 00:04:35281 self.lock.acquire()
[email protected]4ed34182010-09-17 15:57:47282 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.
[email protected]042f0e72011-10-23 00:04:35286 if not index in self.__output_buffers:
287 obj = self.__output_buffers[index] = ['']
[email protected]4ed34182010-09-17 15:57:47288 else:
[email protected]042f0e72011-10-23 00:04:35289 obj = self.__output_buffers[index]
[email protected]4ed34182010-09-17 15:57:47290 finally:
[email protected]042f0e72011-10-23 00:04:35291 self.lock.release()
[email protected]4ed34182010-09-17 15:57:47292
293 # Continue lockless.
294 obj[0] += out
295 while '\n' in obj[0]:
296 line, remaining = obj[0].split('\n', 1)
[email protected]e939bb52011-06-01 22:59:15297 if line:
[email protected]042f0e72011-10-23 00:04:35298 self._wrapped.write('%d>%s\n' % (index, line))
[email protected]4ed34182010-09-17 15:57:47299 obj[0] = remaining
300
[email protected]042f0e72011-10-23 00:04:35301 def flush(self):
[email protected]4ed34182010-09-17 15:57:47302 """Flush buffered output."""
303 orphans = []
[email protected]042f0e72011-10-23 00:04:35304 self.lock.acquire()
[email protected]4ed34182010-09-17 15:57:47305 try:
306 # Detect threads no longer existing.
307 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
[email protected]cb2985f2010-11-03 14:08:31308 indexes = filter(None, indexes)
[email protected]042f0e72011-10-23 00:04:35309 for index in self.__output_buffers:
[email protected]4ed34182010-09-17 15:57:47310 if not index in indexes:
[email protected]042f0e72011-10-23 00:04:35311 orphans.append((index, self.__output_buffers[index][0]))
[email protected]4ed34182010-09-17 15:57:47312 for orphan in orphans:
[email protected]042f0e72011-10-23 00:04:35313 del self.__output_buffers[orphan[0]]
[email protected]4ed34182010-09-17 15:57:47314 finally:
[email protected]042f0e72011-10-23 00:04:35315 self.lock.release()
[email protected]4ed34182010-09-17 15:57:47316
317 # Don't keep the lock while writting. Will append \n when it shouldn't.
318 for orphan in orphans:
[email protected]e939bb52011-06-01 22:59:15319 if orphan[1]:
[email protected]042f0e72011-10-23 00:04:35320 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
321 return self._wrapped.flush()
[email protected]4ed34182010-09-17 15:57:47322
[email protected]042f0e72011-10-23 00:04:35323
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)
[email protected]cb1e97a2010-09-09 20:09:20336
337
[email protected]17d01792010-09-01 18:07:10338def 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
[email protected]57bf78d2011-09-08 18:57:33343 Accepts all subprocess2.Popen() parameters plus:
[email protected]17d01792010-09-01 18:07:10344 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
[email protected]57bf78d2011-09-08 18:57:33346 of the subprocess2's output. Each line has the trailing newline
[email protected]17d01792010-09-01 18:07:10347 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)
[email protected]a82a8ee2011-09-08 18:41:37355 kid = subprocess2.Popen(
356 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
357 **kwargs)
[email protected]5f3eee32009-09-17 00:34:30358
[email protected]57bf78d2011-09-08 18:57:33359 # Do a flush of stdout before we begin reading from the subprocess2's stdout
[email protected]559c3f82010-08-23 19:26:08360 stdout.flush()
[email protected]8ad1cee2010-08-16 19:12:27361
[email protected]5f3eee32009-09-17 00:34:30362 # 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.
[email protected]109cb9d2011-09-14 20:03:11366 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 = ''
[email protected]85d3e3a2011-10-07 17:12:00381 else:
382 filter_fn(in_line)
383 in_line = ''
[email protected]109cb9d2011-09-14 20:03:11384 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
[email protected]5f3eee32009-09-17 00:34:30394 if rv:
[email protected]a82a8ee2011-09-08 18:41:37395 raise subprocess2.CalledProcessError(
396 rv, args, kwargs.get('cwd', None), None, None)
[email protected]17d01792010-09-01 18:07:10397 return 0
[email protected]5f3eee32009-09-17 00:34:30398
399
[email protected]9eda4112010-06-11 18:56:10400def FindGclientRoot(from_dir, filename='.gclient'):
[email protected]a9371762009-12-22 18:27:38401 """Tries to find the gclient root."""
[email protected]20760a52010-09-08 08:47:28402 real_from_dir = os.path.realpath(from_dir)
403 path = real_from_dir
[email protected]9eda4112010-06-11 18:56:10404 while not os.path.exists(os.path.join(path, filename)):
[email protected]3a292682010-08-23 18:54:55405 split_path = os.path.split(path)
406 if not split_path[1]:
[email protected]a9371762009-12-22 18:27:38407 return None
[email protected]3a292682010-08-23 18:54:55408 path = split_path[0]
[email protected]20760a52010-09-08 08:47:28409
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.
[email protected]cb2985f2010-11-03 14:08:31419 print >> sys.stderr, ("%s file in parent directory %s might not be the "
[email protected]20760a52010-09-08 08:47:28420 "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
[email protected]3742c842010-09-09 19:27:14434
[email protected]d9141bf2009-12-23 16:13:32435 logging.info('Found gclient root at ' + path)
[email protected]a9371762009-12-22 18:27:38436 return path
[email protected]3ccbf7e2009-12-22 20:46:42437
[email protected]9eda4112010-06-11 18:56:10438
[email protected]3ccbf7e2009-12-22 20:46:42439def 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):]
[email protected]f43d0192010-04-15 02:36:04450
451
452def FindFileUpwards(filename, path=None):
[email protected]13595ff2011-10-13 01:25:07453 """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 """
[email protected]f43d0192010-04-15 02:36:04457 if not path:
458 path = os.getcwd()
459 path = os.path.realpath(path)
460 while True:
461 file_path = os.path.join(path, filename)
[email protected]13595ff2011-10-13 01:25:07462 if os.path.exists(file_path):
463 return path
[email protected]f43d0192010-04-15 02:36:04464 (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'
[email protected]93a9ee02011-10-18 18:23:58473 root = FindFileUpwards(config_file, path)
474 if not root:
[email protected]116704f2010-06-11 17:34:38475 print "Can't find %s" % config_file
[email protected]f43d0192010-04-15 02:36:04476 return None
[email protected]93a9ee02011-10-18 18:23:58477 config_path = os.path.join(root, config_file)
[email protected]f43d0192010-04-15 02:36:04478 env = {}
479 execfile(config_path, env)
480 config_dir = os.path.dirname(config_path)
481 return config_dir, env['entries']
[email protected]80cbe8b2010-08-13 13:53:07482
483
[email protected]6ca8bf82011-09-19 23:04:30484def 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
[email protected]80cbe8b2010-08-13 13:53:07499class WorkItem(object):
500 """One work item."""
[email protected]4901daf2011-10-20 14:34:47501 # 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
[email protected]6ca8bf82011-09-19 23:04:30506 def __init__(self, name):
[email protected]485dcab2011-09-14 12:48:47507 # A unique string representing this work item.
[email protected]6ca8bf82011-09-19 23:04:30508 self._name = name
[email protected]80cbe8b2010-08-13 13:53:07509
[email protected]77e4eca2010-09-21 13:23:07510 def run(self, work_queue):
511 """work_queue is passed as keyword argument so it should be
[email protected]3742c842010-09-09 19:27:14512 the last parameters of the function when you override it."""
[email protected]80cbe8b2010-08-13 13:53:07513 pass
514
[email protected]6ca8bf82011-09-19 23:04:30515 @property
516 def name(self):
517 return self._name
518
[email protected]80cbe8b2010-08-13 13:53:07519
520class ExecutionQueue(object):
[email protected]9e5317a2010-08-13 20:35:11521 """Runs a set of WorkItem that have interdependencies and were WorkItem are
522 added as they are processed.
[email protected]80cbe8b2010-08-13 13:53:07523
[email protected]9e5317a2010-08-13 20:35:11524 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.
[email protected]80cbe8b2010-08-13 13:53:07527
[email protected]9e5317a2010-08-13 20:35:11528 Methods of this class are thread safe.
[email protected]80cbe8b2010-08-13 13:53:07529 """
[email protected]9e5317a2010-08-13 20:35:11530 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.
[email protected]80cbe8b2010-08-13 13:53:07538 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 = []
[email protected]9e5317a2010-08-13 20:35:11543 # Exceptions thrown if any.
[email protected]3742c842010-09-09 19:27:14544 self.exceptions = Queue.Queue()
545 # Progress status
[email protected]80cbe8b2010-08-13 13:53:07546 self.progress = progress
547 if self.progress:
[email protected]3742c842010-09-09 19:27:14548 self.progress.update(0)
[email protected]80cbe8b2010-08-13 13:53:07549
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)
[email protected]9e5317a2010-08-13 20:35:11555 self.ready_cond.acquire()
[email protected]80cbe8b2010-08-13 13:53:07556 try:
[email protected]80cbe8b2010-08-13 13:53:07557 self.queued.append(d)
558 total = len(self.queued) + len(self.ran) + len(self.running)
[email protected]9e5317a2010-08-13 20:35:11559 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()
[email protected]80cbe8b2010-08-13 13:53:07564 finally:
[email protected]9e5317a2010-08-13 20:35:11565 self.ready_cond.release()
[email protected]80cbe8b2010-08-13 13:53:07566
567 def flush(self, *args, **kwargs):
568 """Runs all enqueued items until all are executed."""
[email protected]3742c842010-09-09 19:27:14569 kwargs['work_queue'] = self
[email protected]9e5317a2010-08-13 20:35:11570 self.ready_cond.acquire()
[email protected]80cbe8b2010-08-13 13:53:07571 try:
[email protected]9e5317a2010-08-13 20:35:11572 while True:
573 # Check for task to run first, then wait.
574 while True:
[email protected]3742c842010-09-09 19:27:14575 if not self.exceptions.empty():
576 # Systematically flush the queue when an exception logged.
[email protected]9e5317a2010-08-13 20:35:11577 self.queued = []
[email protected]3742c842010-09-09 19:27:14578 self._flush_terminated_threads()
579 if (not self.queued and not self.running or
580 self.jobs == len(self.running)):
[email protected]1333cb32011-10-04 23:40:16581 logging.debug('No more worker threads or can\'t queue anything.')
[email protected]9e5317a2010-08-13 20:35:11582 break
[email protected]3742c842010-09-09 19:27:14583
584 # Check for new tasks to start.
[email protected]9e5317a2010-08-13 20:35:11585 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.
[email protected]3742c842010-09-09 19:27:14593 self._run_one_task(self.queued.pop(i), args, kwargs)
[email protected]9e5317a2010-08-13 20:35:11594 break
595 else:
596 # Couldn't find an item that could run. Break out the outher loop.
597 break
[email protected]3742c842010-09-09 19:27:14598
[email protected]9e5317a2010-08-13 20:35:11599 if not self.queued and not self.running:
[email protected]3742c842010-09-09 19:27:14600 # We're done.
[email protected]9e5317a2010-08-13 20:35:11601 break
602 # We need to poll here otherwise Ctrl-C isn't processed.
[email protected]485dcab2011-09-14 12:48:47603 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
[email protected]9e5317a2010-08-13 20:35:11617 # Something happened: self.enqueue() or a thread terminated. Loop again.
[email protected]80cbe8b2010-08-13 13:53:07618 finally:
[email protected]9e5317a2010-08-13 20:35:11619 self.ready_cond.release()
[email protected]3742c842010-09-09 19:27:14620
[email protected]9e5317a2010-08-13 20:35:11621 assert not self.running, 'Now guaranteed to be single-threaded'
[email protected]3742c842010-09-09 19:27:14622 if not self.exceptions.empty():
[email protected]c8d064b2010-08-16 16:46:14623 # 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.
[email protected]3742c842010-09-09 19:27:14625 e = self.exceptions.get()
[email protected]c8d064b2010-08-16 16:46:14626 raise e[0], e[1], e[2]
[email protected]80cbe8b2010-08-13 13:53:07627 if self.progress:
628 self.progress.end()
[email protected]80cbe8b2010-08-13 13:53:07629
[email protected]3742c842010-09-09 19:27:14630 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()
[email protected]042f0e72011-10-23 00:04:35639 sys.stdout.flush()
[email protected]3742c842010-09-09 19:27:14640 if self.progress:
[email protected]55a2eb82010-10-06 23:35:18641 self.progress.update(1, t.item.name)
[email protected]f36c0ee2011-09-14 19:16:47642 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)))
[email protected]acc45672010-09-09 21:21:21646 if not t.item.name in self.ran:
647 self.ran.append(t.item.name)
[email protected]3742c842010-09-09 19:27:14648
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
[email protected]77e4eca2010-09-21 13:23:07653 new_thread = self._Worker(task_item, index, args, kwargs)
[email protected]3742c842010-09-09 19:27:14654 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:
[email protected]55a2eb82010-10-06 23:35:18662 self.progress.update(1, ', '.join(t.item.name for t in self.running))
[email protected]3742c842010-09-09 19:27:14663
[email protected]9e5317a2010-08-13 20:35:11664 class _Worker(threading.Thread):
665 """One thread to execute one WorkItem."""
[email protected]4ed34182010-09-17 15:57:47666 def __init__(self, item, index, args, kwargs):
[email protected]9e5317a2010-08-13 20:35:11667 threading.Thread.__init__(self, name=item.name or 'Worker')
[email protected]1333cb32011-10-04 23:40:16668 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
[email protected]9e5317a2010-08-13 20:35:11669 self.item = item
[email protected]4ed34182010-09-17 15:57:47670 self.index = index
[email protected]3742c842010-09-09 19:27:14671 self.args = args
672 self.kwargs = kwargs
[email protected]80cbe8b2010-08-13 13:53:07673
[email protected]9e5317a2010-08-13 20:35:11674 def run(self):
675 """Runs in its own thread."""
[email protected]1333cb32011-10-04 23:40:16676 logging.debug('_Worker.run(%s)' % self.item.name)
[email protected]3742c842010-09-09 19:27:14677 work_queue = self.kwargs['work_queue']
[email protected]9e5317a2010-08-13 20:35:11678 try:
679 self.item.run(*self.args, **self.kwargs)
[email protected]c8d064b2010-08-16 16:46:14680 except Exception:
681 # Catch exception location.
[email protected]3742c842010-09-09 19:27:14682 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())
[email protected]1333cb32011-10-04 23:40:16685 logging.info('_Worker.run(%s) done' % self.item.name)
[email protected]9e5317a2010-08-13 20:35:11686
[email protected]3742c842010-09-09 19:27:14687 work_queue.ready_cond.acquire()
[email protected]9e5317a2010-08-13 20:35:11688 try:
[email protected]3742c842010-09-09 19:27:14689 work_queue.ready_cond.notifyAll()
[email protected]9e5317a2010-08-13 20:35:11690 finally:
[email protected]3742c842010-09-09 19:27:14691 work_queue.ready_cond.release()
[email protected]0e0436a2011-10-25 13:32:41692
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:
[email protected]0eff22d2011-10-25 16:11:16715 print >> sys.stderr, (
716 '!! Please remove \\r from your change description !!')
[email protected]0e0436a2011-10-25 13:32:41717 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)
[email protected]99ac1c52012-01-16 14:52:12736
737
[email protected]eb5edbc2012-01-16 17:03:28738def 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
[email protected]99ac1c52012-01-16 14:52:12762def 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)
[email protected]eb5edbc2012-01-16 17:03:28770 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')
[email protected]99ac1c52012-01-16 14:52:12775 return keyvals