1d201506cSStephen Warren# Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved. 2d201506cSStephen Warren# 3d201506cSStephen Warren# SPDX-License-Identifier: GPL-2.0 4d201506cSStephen Warren 5d201506cSStephen Warren# Logic to spawn a sub-process and interact with its stdio. 6d201506cSStephen Warren 7d201506cSStephen Warrenimport os 8d201506cSStephen Warrenimport re 9d201506cSStephen Warrenimport pty 10d201506cSStephen Warrenimport signal 11d201506cSStephen Warrenimport select 12d201506cSStephen Warrenimport time 13d201506cSStephen Warren 14d201506cSStephen Warrenclass Timeout(Exception): 15e8debf39SStephen Warren """An exception sub-class that indicates that a timeout occurred.""" 16d201506cSStephen Warren pass 17d201506cSStephen Warren 18d201506cSStephen Warrenclass Spawn(object): 19e8debf39SStephen Warren """Represents the stdio of a freshly created sub-process. Commands may be 20d201506cSStephen Warren sent to the process, and responses waited for. 21e8debf39SStephen Warren """ 22d201506cSStephen Warren 23d27f2fc1SStephen Warren def __init__(self, args, cwd=None): 24e8debf39SStephen Warren """Spawn (fork/exec) the sub-process. 25d201506cSStephen Warren 26d201506cSStephen Warren Args: 27d27f2fc1SStephen Warren args: array of processs arguments. argv[0] is the command to 28d27f2fc1SStephen Warren execute. 29d27f2fc1SStephen Warren cwd: the directory to run the process in, or None for no change. 30d201506cSStephen Warren 31d201506cSStephen Warren Returns: 32d201506cSStephen Warren Nothing. 33e8debf39SStephen Warren """ 34d201506cSStephen Warren 35d201506cSStephen Warren self.waited = False 36d201506cSStephen Warren self.buf = '' 37d201506cSStephen Warren self.logfile_read = None 38d201506cSStephen Warren self.before = '' 39d201506cSStephen Warren self.after = '' 40d201506cSStephen Warren self.timeout = None 41*085e64ddSStephen Warren # http://stackoverflow.com/questions/7857352/python-regex-to-match-vt100-escape-sequences 42*085e64ddSStephen Warren # Note that re.I doesn't seem to work with this regex (or perhaps the 43*085e64ddSStephen Warren # version of Python in Ubuntu 14.04), hence the inclusion of a-z inside 44*085e64ddSStephen Warren # [] instead. 45*085e64ddSStephen Warren self.re_vt100 = re.compile('(\x1b\[|\x9b)[^@-_a-z]*[@-_a-z]|\x1b[@-_a-z]') 46d201506cSStephen Warren 47d201506cSStephen Warren (self.pid, self.fd) = pty.fork() 48d201506cSStephen Warren if self.pid == 0: 49d201506cSStephen Warren try: 50d201506cSStephen Warren # For some reason, SIGHUP is set to SIG_IGN at this point when 51d201506cSStephen Warren # run under "go" (www.go.cd). Perhaps this happens under any 52d201506cSStephen Warren # background (non-interactive) system? 53d201506cSStephen Warren signal.signal(signal.SIGHUP, signal.SIG_DFL) 54d27f2fc1SStephen Warren if cwd: 55d27f2fc1SStephen Warren os.chdir(cwd) 56d201506cSStephen Warren os.execvp(args[0], args) 57d201506cSStephen Warren except: 58d201506cSStephen Warren print 'CHILD EXECEPTION:' 59d201506cSStephen Warren import traceback 60d201506cSStephen Warren traceback.print_exc() 61d201506cSStephen Warren finally: 62d201506cSStephen Warren os._exit(255) 63d201506cSStephen Warren 6493134e18SStephen Warren try: 65d201506cSStephen Warren self.poll = select.poll() 66d201506cSStephen Warren self.poll.register(self.fd, select.POLLIN | select.POLLPRI | select.POLLERR | select.POLLHUP | select.POLLNVAL) 6793134e18SStephen Warren except: 6893134e18SStephen Warren self.close() 6993134e18SStephen Warren raise 70d201506cSStephen Warren 71d201506cSStephen Warren def kill(self, sig): 72e8debf39SStephen Warren """Send unix signal "sig" to the child process. 73d201506cSStephen Warren 74d201506cSStephen Warren Args: 75d201506cSStephen Warren sig: The signal number to send. 76d201506cSStephen Warren 77d201506cSStephen Warren Returns: 78d201506cSStephen Warren Nothing. 79e8debf39SStephen Warren """ 80d201506cSStephen Warren 81d201506cSStephen Warren os.kill(self.pid, sig) 82d201506cSStephen Warren 83d201506cSStephen Warren def isalive(self): 84e8debf39SStephen Warren """Determine whether the child process is still running. 85d201506cSStephen Warren 86d201506cSStephen Warren Args: 87d201506cSStephen Warren None. 88d201506cSStephen Warren 89d201506cSStephen Warren Returns: 90d201506cSStephen Warren Boolean indicating whether process is alive. 91e8debf39SStephen Warren """ 92d201506cSStephen Warren 93d201506cSStephen Warren if self.waited: 94d201506cSStephen Warren return False 95d201506cSStephen Warren 96d201506cSStephen Warren w = os.waitpid(self.pid, os.WNOHANG) 97d201506cSStephen Warren if w[0] == 0: 98d201506cSStephen Warren return True 99d201506cSStephen Warren 100d201506cSStephen Warren self.waited = True 101d201506cSStephen Warren return False 102d201506cSStephen Warren 103d201506cSStephen Warren def send(self, data): 104e8debf39SStephen Warren """Send data to the sub-process's stdin. 105d201506cSStephen Warren 106d201506cSStephen Warren Args: 107d201506cSStephen Warren data: The data to send to the process. 108d201506cSStephen Warren 109d201506cSStephen Warren Returns: 110d201506cSStephen Warren Nothing. 111e8debf39SStephen Warren """ 112d201506cSStephen Warren 113d201506cSStephen Warren os.write(self.fd, data) 114d201506cSStephen Warren 115d201506cSStephen Warren def expect(self, patterns): 116e8debf39SStephen Warren """Wait for the sub-process to emit specific data. 117d201506cSStephen Warren 118d201506cSStephen Warren This function waits for the process to emit one pattern from the 119d201506cSStephen Warren supplied list of patterns, or for a timeout to occur. 120d201506cSStephen Warren 121d201506cSStephen Warren Args: 122d201506cSStephen Warren patterns: A list of strings or regex objects that we expect to 123d201506cSStephen Warren see in the sub-process' stdout. 124d201506cSStephen Warren 125d201506cSStephen Warren Returns: 126d201506cSStephen Warren The index within the patterns array of the pattern the process 127d201506cSStephen Warren emitted. 128d201506cSStephen Warren 129d201506cSStephen Warren Notable exceptions: 130d201506cSStephen Warren Timeout, if the process did not emit any of the patterns within 131d201506cSStephen Warren the expected time. 132e8debf39SStephen Warren """ 133d201506cSStephen Warren 134d201506cSStephen Warren for pi in xrange(len(patterns)): 135d201506cSStephen Warren if type(patterns[pi]) == type(''): 136d201506cSStephen Warren patterns[pi] = re.compile(patterns[pi]) 137d201506cSStephen Warren 138d314e247SStephen Warren tstart_s = time.time() 139d201506cSStephen Warren try: 140d201506cSStephen Warren while True: 141d201506cSStephen Warren earliest_m = None 142d201506cSStephen Warren earliest_pi = None 143d201506cSStephen Warren for pi in xrange(len(patterns)): 144d201506cSStephen Warren pattern = patterns[pi] 145d201506cSStephen Warren m = pattern.search(self.buf) 146d201506cSStephen Warren if not m: 147d201506cSStephen Warren continue 14844ac762bSStephen Warren if earliest_m and m.start() >= earliest_m.start(): 149d201506cSStephen Warren continue 150d201506cSStephen Warren earliest_m = m 151d201506cSStephen Warren earliest_pi = pi 152d201506cSStephen Warren if earliest_m: 153d201506cSStephen Warren pos = earliest_m.start() 154d8926811SStephen Warren posafter = earliest_m.end() 155d201506cSStephen Warren self.before = self.buf[:pos] 156d201506cSStephen Warren self.after = self.buf[pos:posafter] 157d201506cSStephen Warren self.buf = self.buf[posafter:] 158d201506cSStephen Warren return earliest_pi 159d314e247SStephen Warren tnow_s = time.time() 16089ab8410SStephen Warren if self.timeout: 161d314e247SStephen Warren tdelta_ms = (tnow_s - tstart_s) * 1000 16289ab8410SStephen Warren poll_maxwait = self.timeout - tdelta_ms 163d314e247SStephen Warren if tdelta_ms > self.timeout: 164d314e247SStephen Warren raise Timeout() 16589ab8410SStephen Warren else: 16689ab8410SStephen Warren poll_maxwait = None 16789ab8410SStephen Warren events = self.poll.poll(poll_maxwait) 168d201506cSStephen Warren if not events: 169d201506cSStephen Warren raise Timeout() 170d201506cSStephen Warren c = os.read(self.fd, 1024) 171d201506cSStephen Warren if not c: 172d201506cSStephen Warren raise EOFError() 173d201506cSStephen Warren if self.logfile_read: 174d201506cSStephen Warren self.logfile_read.write(c) 175d201506cSStephen Warren self.buf += c 176*085e64ddSStephen Warren # count=0 is supposed to be the default, which indicates 177*085e64ddSStephen Warren # unlimited substitutions, but in practice the version of 178*085e64ddSStephen Warren # Python in Ubuntu 14.04 appears to default to count=2! 179*085e64ddSStephen Warren self.buf = self.re_vt100.sub('', self.buf, count=1000000) 180d201506cSStephen Warren finally: 181d201506cSStephen Warren if self.logfile_read: 182d201506cSStephen Warren self.logfile_read.flush() 183d201506cSStephen Warren 184d201506cSStephen Warren def close(self): 185e8debf39SStephen Warren """Close the stdio connection to the sub-process. 186d201506cSStephen Warren 187d201506cSStephen Warren This also waits a reasonable time for the sub-process to stop running. 188d201506cSStephen Warren 189d201506cSStephen Warren Args: 190d201506cSStephen Warren None. 191d201506cSStephen Warren 192d201506cSStephen Warren Returns: 193d201506cSStephen Warren Nothing. 194e8debf39SStephen Warren """ 195d201506cSStephen Warren 196d201506cSStephen Warren os.close(self.fd) 197d201506cSStephen Warren for i in xrange(100): 198d201506cSStephen Warren if not self.isalive(): 199d201506cSStephen Warren break 200d201506cSStephen Warren time.sleep(0.1) 201