utils.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import subprocess
  2. import time
  3. class ShellUtils():
  4. def __init__(self, directory, outfile, errfile):
  5. '''
  6. outfile: A file-like object to write command output to.
  7. Must have write(string) method. Common choices are
  8. files, sys.stdout, or WrapLogger objects
  9. errfile: See outfile
  10. '''
  11. # Advanced notes: outfile and errfile do *not* have to be
  12. # thread-safe objects. They are only ever written to from one
  13. # thread at a time *unless* someone calls sh_async twice with
  14. # the same ShellUtils
  15. self.directory = directory
  16. self.outfile = outfile
  17. self.errfile = errfile
  18. def sh(self, command, **kwargs):
  19. '''Run a shell command, sending output to outfile and errfile.
  20. Blocks until command exits'''
  21. kwargs.setdefault('cwd', self.directory)
  22. kwargs.setdefault('executable', '/bin/bash')
  23. self.outfile.write("Running %s (cwd=%s)" % (command, kwargs.get('cwd')))
  24. try:
  25. output = subprocess.check_output(command, shell=True, stderr=self.errfile, **kwargs)
  26. if output and output.strip():
  27. self.outfile.write("Output:")
  28. self.outfile.write(output.rstrip('\n'))
  29. else:
  30. self.outfile.write("No Output")
  31. except subprocess.CalledProcessError:
  32. self.errfile.write("Command returned non-zero exit code: %s" % command)
  33. # TODO modify this to start the subcommand as a new process group, so that
  34. # we can automatically kill the entire group!
  35. def sh_async(self, command, initial_logs=True, **kwargs):
  36. '''Run a shell command, sending output to outfile and errfile.
  37. If intial_logs, prints out logs for a few seconds before returning. '''
  38. # TODO add this - '''Continues to send output until command completes'''
  39. kwargs.setdefault('cwd', self.directory)
  40. self.outfile.write("Running %s (cwd=%s)" % (command, kwargs.get('cwd')))
  41. # Open in line-buffered mode (bufsize=1) because NonBlockingStreamReader uses readline anyway
  42. process = subprocess.Popen(command, bufsize=1, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, **kwargs)
  43. nbsr = NonBlockingStreamReader(process.stdout)
  44. nbsr_err = NonBlockingStreamReader(process.stderr)
  45. if initial_logs:
  46. time.sleep(8)
  47. # TODO put this read into a tight loop to prevent deadlock due to
  48. # filling up OS buffers
  49. out = nbsr.read()
  50. if len(out) == 0:
  51. self.outfile.write("No output")
  52. else:
  53. self.outfile.write("Initial Output:")
  54. for line in out:
  55. self.outfile.write(line.rstrip('\n'))
  56. err = nbsr_err.read()
  57. if len(err) != 0:
  58. self.errfile.write("Initial Error Logs:")
  59. for line in err:
  60. self.errfile.write(line.rstrip('\n'))
  61. from threading import Thread
  62. from Queue import Queue, Empty
  63. # TODO - no need to use a daemon, kill this off in stop!
  64. # NOTE - it is safe to use logging module in a multi-threaded
  65. # system, but not safe to log to the same file across multiple
  66. # processes. Our system has two main processes (main and __run_test),
  67. # and lots of minor ones from 'subprocess'. As long as we only use
  68. # one logger inside TestRunner and NonBlockingFoo, sd are good
  69. # Add credit for http://eyalarubas.com/python-subproc-nonblock.html
  70. class NonBlockingStreamReader:
  71. def __init__(self, stream):
  72. self._s = stream
  73. self._q = Queue()
  74. def _populateQueue(stream, queue):
  75. for line in iter(stream.readline, b''):
  76. queue.put(line)
  77. self._t = Thread(target = _populateQueue,
  78. args = (self._s, self._q))
  79. self._t.daemon = True
  80. self._t.start() #start collecting lines from the stream
  81. # TODO - This is only returning one line, if it is available.
  82. def readline(self, timeout = None):
  83. try:
  84. return self._q.get(block = timeout is not None,
  85. timeout = timeout)
  86. except Empty:
  87. return None
  88. def read(self):
  89. lines = []
  90. while True:
  91. line = self.readline(0.1)
  92. if not line:
  93. return lines
  94. lines.append(line)
  95. import tempfile
  96. class WrapLogger():
  97. """
  98. Used to convert a Logger into a file-like object. Adds easy integration
  99. of Logger into subprocess, which takes file parameters for stdout
  100. and stderr.
  101. Use:
  102. (out, err) = WrapLogger(logger, logging.INFO), WrapLogger(logger, logging.ERROR)
  103. subprocess.Popen(command, stdout=out, stderr=err)
  104. Note: When used with subprocess, this cannot guarantee that output will appear
  105. in real time. This is because subprocess tends to bypass the write() method and
  106. access the underlying file directly. This will eventually collect any output
  107. that was sent directly to the file, but it cannot do this in real time.
  108. Practically, this limitation means that WrapLogger is safe to use with
  109. all synchronous subprocess calls, but it will lag heavily with
  110. subprocess.Popen calls
  111. """
  112. # Note - Someone awesome with python could make this fully implement the file
  113. # interface, and remove the real-time limitation
  114. def __init__(self, logger, level):
  115. self.logger = logger
  116. self.level = level
  117. self.file = tempfile.TemporaryFile()
  118. def write(self, message):
  119. self.logger.log(self.level, message)
  120. def __getattr__(self, name):
  121. return getattr(self.file, name)
  122. def __del__(self):
  123. """Grabs any output that was written directly to the file (e.g. bypassing
  124. the write method). Subprocess.call, Popen, etc have a habit of accessing
  125. the file directly for faster writing. See http://bugs.python.org/issue1631
  126. """
  127. self.file.seek(0)
  128. for line in self.file.readlines():
  129. self.logger.log(self.level, line.rstrip('\n'))
  130. class Header():
  131. """
  132. """
  133. def __init__(self, message, top='-', bottom='-'):
  134. self.message = message
  135. self.top = top
  136. self.bottom = bottom
  137. def __str__(self):
  138. topheader = self.top * 80
  139. topheader = topheader[:80]
  140. bottomheader = self.bottom * 80
  141. bottomheader = bottomheader[:80]
  142. return "\n%s\n %s\n%s" % (topheader, self.message, bottomheader)