1# Copyright 2013 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Module for build host support.""" 6 7import os 8import pipes 9import signal 10import subprocess 11 12import cr 13 14# Controls what verbosity level turns on command trail logging 15_TRAIL_VERBOSITY = 2 16 17def PrintTrail(trail): 18 print 'Command expanded the following variables:' 19 for key, value in trail: 20 if value == None: 21 value = '' 22 print ' ', key, '=', value 23 24 25class Host(cr.Plugin, cr.Plugin.Type): 26 """Base class for implementing cr hosts. 27 28 The host is the main access point to services provided by the machine cr 29 is running on. It exposes information about the machine, and runs external 30 commands on behalf of the actions. 31 """ 32 33 def __init__(self): 34 super(Host, self).__init__() 35 36 def Matches(self): 37 """Detects whether this is the correct host implementation. 38 39 This method is overridden by the concrete implementations. 40 Returns: 41 true if the plugin matches the machine it is running on. 42 """ 43 return False 44 45 @classmethod 46 def Select(cls): 47 for host in cls.Plugins(): 48 if host.Matches(): 49 return host 50 51 def _Execute(self, command, 52 shell=False, capture=False, silent=False, 53 ignore_dry_run=False, return_status=False, 54 ignore_interrupt_signal=False): 55 """This is the only method that launches external programs. 56 57 It is a thin wrapper around subprocess.Popen that handles cr specific 58 issues. The command is expanded in the active context so that variables 59 are substituted. 60 Args: 61 command: the command to run. 62 shell: whether to run the command using the shell. 63 capture: controls wether the output of the command is captured. 64 ignore_dry_run: Normally, if the context is in dry run mode the command is 65 printed but not executed. This flag overrides that behaviour, causing 66 the command to be run anyway. 67 return_status: switches the function to returning the status code rather 68 the output. 69 ignore_interrupt_signal: Ignore the interrupt signal (i.e., Ctrl-C) while 70 the command is running. Useful for letting interactive programs manage 71 Ctrl-C by themselves. 72 Returns: 73 the status if return_status is true, or the output if capture is true, 74 otherwise nothing. 75 """ 76 with cr.context.Trace(): 77 command = [cr.context.Substitute(arg) for arg in command if arg] 78 trail = cr.context.trail 79 if not command: 80 print 'Empty command passed to execute' 81 exit(1) 82 if cr.context.verbose: 83 print ' '.join(command) 84 if cr.context.verbose >= _TRAIL_VERBOSITY: 85 PrintTrail(trail) 86 if ignore_dry_run or not cr.context.dry_run: 87 out = None 88 if capture: 89 out = subprocess.PIPE 90 elif silent: 91 out = open(os.devnull, "w") 92 try: 93 p = subprocess.Popen( 94 command, shell=shell, 95 env={k: str(v) for k, v in cr.context.exported.items()}, 96 stdout=out) 97 except OSError: 98 print 'Failed to exec', command 99 # Don't log the trail if we already have 100 if cr.context.verbose < _TRAIL_VERBOSITY: 101 PrintTrail(trail) 102 exit(1) 103 try: 104 if ignore_interrupt_signal: 105 signal.signal(signal.SIGINT, signal.SIG_IGN) 106 output, _ = p.communicate() 107 finally: 108 if ignore_interrupt_signal: 109 signal.signal(signal.SIGINT, signal.SIG_DFL) 110 if silent: 111 out.close() 112 if return_status: 113 return p.returncode 114 if p.returncode != 0: 115 print 'Error {0} executing command {1}'.format(p.returncode, command) 116 exit(p.returncode) 117 return output or '' 118 return '' 119 120 @cr.Plugin.activemethod 121 def Shell(self, *command): 122 command = ' '.join([pipes.quote(arg) for arg in command]) 123 return self._Execute([command], shell=True, ignore_interrupt_signal=True) 124 125 @cr.Plugin.activemethod 126 def Execute(self, *command): 127 return self._Execute(command, shell=False) 128 129 @cr.Plugin.activemethod 130 def ExecuteSilently(self, *command): 131 return self._Execute(command, shell=False, silent=True) 132 133 @cr.Plugin.activemethod 134 def CaptureShell(self, *command): 135 return self._Execute(command, 136 shell=True, capture=True, ignore_dry_run=True) 137 138 @cr.Plugin.activemethod 139 def Capture(self, *command): 140 return self._Execute(command, capture=True, ignore_dry_run=True) 141 142 @cr.Plugin.activemethod 143 def ExecuteStatus(self, *command): 144 return self._Execute(command, 145 ignore_dry_run=True, return_status=True) 146 147 @cr.Plugin.activemethod 148 def YesNo(self, question, default=True): 149 """Ask the user a yes no question 150 151 This blocks until the user responds. 152 Args: 153 question: The question string to show the user 154 default: True if the default response is Yes 155 Returns: 156 True if the response was yes. 157 """ 158 options = 'Y/n' if default else 'y/N' 159 result = raw_input(question + ' [' + options + '] ').lower() 160 if result == '': 161 return default 162 return result in ['y', 'yes'] 163 164 @classmethod 165 def SearchPath(cls, name, paths=[]): 166 """Searches the PATH for an executable. 167 168 Args: 169 name: the name of the binary to search for. 170 Returns: 171 the set of executables found, or an empty list if none. 172 """ 173 result = [] 174 extensions = [''] 175 extensions.extend(os.environ.get('PATHEXT', '').split(os.pathsep)) 176 paths = [cr.context.Substitute(path) for path in paths if path] 177 paths = paths + os.environ.get('PATH', '').split(os.pathsep) 178 for path in paths: 179 partial = os.path.join(path, name) 180 for extension in extensions: 181 filename = partial + extension 182 if os.path.exists(filename) and filename not in result: 183 result.append(filename) 184 return result 185