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