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