diff --git a/gclient.py b/gclient.py index 9eec5d8b7..d1b7e4a0c 100644 --- a/gclient.py +++ b/gclient.py @@ -1170,6 +1170,10 @@ def Main(argv): """Doesn't parse the arguments here, just find the right subcommand to execute.""" try: + # Make stdout auto-flush so buildbot doesn't kill us during lengthy + # operations. Python as a strong tendency to buffer sys.stdout. + sys.stdout = gclient_utils.MakeFileAutoFlush(sys.stdout) + # Do it late so all commands are listed. CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([ ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip()) @@ -1198,8 +1202,8 @@ def Main(argv): options.entries_filename = options.config_filename + '_entries' if options.jobs < 1: parser.error('--jobs must be 1 or higher') - # Always autoflush so buildbot doesn't kill us during lengthy operations. - options.stdout = gclient_utils.StdoutAutoFlush(sys.stdout) + # TODO(maruel): Temporary, to be removed. + options.stdout = sys.stdout # These hacks need to die. if not hasattr(options, 'revisions'): diff --git a/gclient_utils.py b/gclient_utils.py index a74c6b960..aeaebc271 100644 --- a/gclient_utils.py +++ b/gclient_utils.py @@ -295,31 +295,48 @@ def CheckCallAndFilterAndHeader(args, always=False, **kwargs): return CheckCallAndFilter(args, **kwargs) -class StdoutAutoFlush(object): - """Automatically flush after N seconds.""" - def __init__(self, stdout, delay=10): - self.lock = threading.Lock() - self.stdout = stdout - self.delay = delay - self.last_flushed_at = time.time() - self.stdout.flush() - - def write(self, out): - """Thread-safe.""" - self.stdout.write(out) +def SoftClone(obj): + """Clones an object. copy.copy() doesn't work on 'file' objects.""" + class NewObject(object): pass + new_obj = NewObject() + for member in dir(obj): + if member.startswith('_'): + continue + setattr(new_obj, member, getattr(obj, member)) + return new_obj + + +def MakeFileAutoFlush(fileobj, delay=10): + """Creates a file object clone to automatically flush after N seconds.""" + if hasattr(fileobj, 'last_flushed_at'): + # Already patched. Just update delay. + fileobj.delay = delay + return fileobj + + new_fileobj = SoftClone(fileobj) + new_fileobj.lock = threading.Lock() + new_fileobj.last_flushed_at = time.time() + new_fileobj.delay = delay + new_fileobj.old_auto_flush_write = fileobj.write + # Silence pylint. + new_fileobj.flush = fileobj.flush + + def auto_flush_write(out): + new_fileobj.old_auto_flush_write(out) should_flush = False - self.lock.acquire() + new_fileobj.lock.acquire() try: - if (time.time() - self.last_flushed_at) > self.delay: + if (new_fileobj.delay and + (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay): should_flush = True - self.last_flushed_at = time.time() + new_fileobj.last_flushed_at = time.time() finally: - self.lock.release() + new_fileobj.lock.release() if should_flush: - self.stdout.flush() + new_fileobj.flush() - def flush(self): - self.stdout.flush() + new_fileobj.write = auto_flush_write + return new_fileobj class StdoutAnnotated(object): diff --git a/tests/gclient_utils_test.py b/tests/gclient_utils_test.py index 3b4d471ae..13a92ac7d 100755 --- a/tests/gclient_utils_test.py +++ b/tests/gclient_utils_test.py @@ -25,10 +25,10 @@ class GclientUtilsUnittest(GclientUtilBase): 'CheckCall', 'CheckCallError', 'CheckCallAndFilter', 'CheckCallAndFilterAndHeader', 'Error', 'ExecutionQueue', 'FileRead', 'FileWrite', 'FindFileUpwards', 'FindGclientRoot', - 'GetGClientRootAndEntries', 'GetNamedNodeText', + 'GetGClientRootAndEntries', 'GetNamedNodeText', 'MakeFileAutoFlush', 'GetNodeNamedAttributeText', 'PathDifference', 'ParseXML', 'Popen', - 'PrintableObject', 'RemoveDirectory', 'SplitUrlRevision', - 'StdoutAnnotated', 'StdoutAutoFlush', 'SyntaxErrorToError', 'WorkItem', + 'PrintableObject', 'RemoveDirectory', 'SoftClone', 'SplitUrlRevision', + 'StdoutAnnotated', 'SyntaxErrorToError', 'WorkItem', 'copy', 'errno', 'logging', 'os', 'Queue', 're', 'stat', 'subprocess', 'sys','threading', 'time', 'xml', ]