| Index: scripts/run_cmd.py
 | 
| diff --git a/scripts/run_cmd.py b/scripts/run_cmd.py
 | 
| index 4a5faa2e72dd309e29fc83163f4b9b194f172f5d..cd749f66482c9d7336b93c0508d1cda7db48c210 100755
 | 
| --- a/scripts/run_cmd.py
 | 
| +++ b/scripts/run_cmd.py
 | 
| @@ -15,6 +15,7 @@ import pprint
 | 
|  import socket
 | 
|  import subprocess
 | 
|  import sys
 | 
| +import traceback
 | 
|  
 | 
|  buildbot_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
 | 
|                                               os.pardir))
 | 
| @@ -23,8 +24,8 @@ sys.path.append(os.path.join(buildbot_path))
 | 
|  from site_config import slave_hosts_cfg
 | 
|  
 | 
|  
 | 
| -class CommandResults(collections.namedtuple('CommandResults',
 | 
| -                                            'stdout, stderr, returncode')):
 | 
| +class BaseCommandResults(object):
 | 
| +  """Base class for CommandResults classes."""
 | 
|  
 | 
|    # We print this string before and after the important output from the command.
 | 
|    # This makes it easy to ignore output from SSH, shells, etc.
 | 
| @@ -36,9 +37,7 @@ class CommandResults(collections.namedtuple('CommandResults',
 | 
|      Returns:
 | 
|          A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
 | 
|      """
 | 
| -    return (CommandResults.BOOKEND_STR +
 | 
| -            pickle.dumps(self.__dict__).encode('hex') +
 | 
| -            CommandResults.BOOKEND_STR)
 | 
| +    raise NotImplementedError()
 | 
|  
 | 
|    @staticmethod
 | 
|    def decode(results_str):
 | 
| @@ -49,8 +48,53 @@ class CommandResults(collections.namedtuple('CommandResults',
 | 
|      Returns:
 | 
|          A dictionary of results.
 | 
|      """
 | 
| -    return CommandResults(**pickle.loads(
 | 
| -        results_str.split(CommandResults.BOOKEND_STR)[1].decode('hex')))
 | 
| +    decoded_dict = pickle.loads(
 | 
| +        results_str.split(BaseCommandResults.BOOKEND_STR)[1].decode('hex'))
 | 
| +    errors = []
 | 
| +    # First, try to interpret the dict as SingleCommandResults.
 | 
| +    try:
 | 
| +      # This will fail unless decoded_dict has the following set of keys:
 | 
| +      # ('returncode', 'stdout', 'stderr')
 | 
| +      return SingleCommandResults(**decoded_dict)
 | 
| +    except TypeError:
 | 
| +      errors.append(traceback.format_exc())
 | 
| +    # Next, try to interpret the dict as MultiCommandResults.
 | 
| +    try:
 | 
| +      results_dict = {}
 | 
| +      for (slavename, results) in decoded_dict.iteritems():
 | 
| +        results_dict[slavename] = BaseCommandResults.decode(results)
 | 
| +      return MultiCommandResults(results_dict)
 | 
| +    except Exception:
 | 
| +      errors.append(traceback.format_exc())
 | 
| +    raise Exception('Unable to decode CommandResults from dict:\n\n%s\n%s'
 | 
| +                    % ('\n'.join(errors), decoded_dict))
 | 
| +
 | 
| +  def print_results(self, pretty=False):
 | 
| +    """Print the results of a command.
 | 
| +
 | 
| +    Args:
 | 
| +        pretty: bool; whether or not to print in human-readable format.
 | 
| +    """
 | 
| +    if pretty:
 | 
| +      print pprint.pformat(self.__dict__)
 | 
| +    else:
 | 
| +      print self.encode()
 | 
| +
 | 
| +
 | 
| +class SingleCommandResults(collections.namedtuple('CommandResults_tuple',
 | 
| +                                                  'stdout, stderr, returncode'),
 | 
| +                           BaseCommandResults):
 | 
| +  """Results for a single command. Properties: stdout, stderr, and returncode"""
 | 
| +
 | 
| +  def encode(self):
 | 
| +    """Convert the results into a machine-readable string.
 | 
| +
 | 
| +    Returns:
 | 
| +        A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
 | 
| +    """
 | 
| +    return (BaseCommandResults.BOOKEND_STR +
 | 
| +            pickle.dumps(self.__dict__).encode('hex') +
 | 
| +            BaseCommandResults.BOOKEND_STR)
 | 
|  
 | 
|    @staticmethod
 | 
|    def make(stdout='', stderr='', returncode=1):
 | 
| @@ -61,9 +105,9 @@ class CommandResults(collections.namedtuple('CommandResults',
 | 
|          stderr: string; stderr from a command.
 | 
|          returncode: string; return code of a command.
 | 
|      """
 | 
| -    return CommandResults(stdout=stdout,
 | 
| -                          stderr=stderr,
 | 
| -                          returncode=returncode)
 | 
| +    return SingleCommandResults(stdout=stdout,
 | 
| +                                stderr=stderr,
 | 
| +                                returncode=returncode)
 | 
|  
 | 
|    @property
 | 
|    def __dict__(self):
 | 
| @@ -74,16 +118,56 @@ class CommandResults(collections.namedtuple('CommandResults',
 | 
|      """
 | 
|      return dict(self._asdict())
 | 
|  
 | 
| -  def print_results(self, pretty=False):
 | 
| -    """Print the results of a command.
 | 
| +
 | 
| +class MultiCommandResults(BaseCommandResults):
 | 
| +  """Encapsulates CommandResults for multiple buildslaves or hosts.
 | 
| +
 | 
| +  MultiCommandResults can form tree structures whose leaves are instances of
 | 
| +  SingleComamandResults and interior nodes are instances of MultiCommandResults:
 | 
| +
 | 
| +  MultiCommandResults({
 | 
| +      'remote_slave_host_name': MultiCommandResults({
 | 
| +              'slave_name': SingleCommandResults,
 | 
| +              'slave_name2': SingleCommandResults,
 | 
| +          }),
 | 
| +      'local_slave_name': SingleCommandResults,
 | 
| +  })
 | 
| +  """
 | 
| +
 | 
| +  def __init__(self, results):
 | 
| +    """Instantiate the MultiCommandResults.
 | 
|  
 | 
|      Args:
 | 
| -        pretty: bool; whether or not to print in human-readable format.
 | 
| +        results: dict whose keys are slavenames or slave host names and values
 | 
| +            are instances of a BaseCommandResults subclass.
 | 
|      """
 | 
| -    if pretty:
 | 
| -      print pprint.pformat(self.__dict__)
 | 
| -    else:
 | 
| -      print repr(self.encode())
 | 
| +    super(MultiCommandResults, self).__init__()
 | 
| +    self._dict = {}
 | 
| +    for (slavename, result) in results.iteritems():
 | 
| +      if not issubclass(result.__class__, BaseCommandResults):
 | 
| +        raise ValueError('%s is not a subclass of BaseCommandResults.'
 | 
| +                         % result.__class__)
 | 
| +      self._dict[slavename] = result
 | 
| +
 | 
| +  def __getitem__(self, key):
 | 
| +    return self._dict[key]
 | 
| +
 | 
| +  def encode(self):
 | 
| +    """Convert the results into a machine-readable string.
 | 
| +
 | 
| +    Returns:
 | 
| +        A hex-encoded string, bookended by BOOKEND_STR for easy parsing.
 | 
| +    """
 | 
| +    encoded_dict = dict([(key, value.encode())
 | 
| +                         for (key, value) in self._dict.iteritems()])
 | 
| +    return (BaseCommandResults.BOOKEND_STR +
 | 
| +            pickle.dumps(encoded_dict).encode('hex') +
 | 
| +            BaseCommandResults.BOOKEND_STR)
 | 
| +
 | 
| +  @property
 | 
| +  def __dict__(self):
 | 
| +    return dict([(key, value.__dict__)
 | 
| +                 for (key, value) in self._dict.iteritems()])
 | 
|  
 | 
|  
 | 
|  class ResolvableCommandElement(object):
 | 
| @@ -182,12 +266,16 @@ def _get_result(popen):
 | 
|    Args:
 | 
|        popen: subprocess.Popen instance.
 | 
|    Returns:
 | 
| -      A dictionary with stdout, stderr, and returncode as keys.
 | 
| +      CommandResults instance, decoded from the results of the process.
 | 
|    """
 | 
|    stdout, stderr = popen.communicate()
 | 
| -  return CommandResults.make(stdout=stdout,
 | 
| -                             stderr=stderr,
 | 
| -                             returncode=popen.returncode)
 | 
| +  try:
 | 
| +    return BaseCommandResults.decode(stdout)
 | 
| +  except Exception:
 | 
| +    pass
 | 
| +  return SingleCommandResults.make(stdout=stdout,
 | 
| +                                   stderr=stderr,
 | 
| +                                   returncode=popen.returncode)
 | 
|  
 | 
|  
 | 
|  def run(cmd):
 | 
| @@ -196,12 +284,12 @@ def run(cmd):
 | 
|    Args:
 | 
|        cmd: string or list of strings; the command to run.
 | 
|    Returns:
 | 
| -      A dictionary with stdout, stderr, and returncode as keys.
 | 
| +      CommandResults instance, decoded from the results of the command.
 | 
|    """
 | 
|    try:
 | 
|      proc = _launch_cmd(cmd)
 | 
|    except OSError as e:
 | 
| -    return CommandResults.make(stderr=str(e))
 | 
| +    return SingleCommandResults.make(stderr=str(e))
 | 
|    return _get_result(proc)
 | 
|  
 | 
|  
 | 
| @@ -211,9 +299,8 @@ def run_on_local_slaves(cmd):
 | 
|    Args:
 | 
|        cmd: list of strings; the command to run.
 | 
|    Returns:
 | 
| -      A dictionary of results with buildslave names as keys and individual
 | 
| -      result dictionaries (with stdout, stderr, and returncode as keys) as
 | 
| -      values.
 | 
| +      MultiCommandResults instance containing the results of the command on each
 | 
| +      of the local slaves.
 | 
|    """
 | 
|    slave_host = slave_hosts_cfg.get_slave_host_config(socket.gethostname())
 | 
|    slaves = slave_host.slaves
 | 
| @@ -221,12 +308,15 @@ def run_on_local_slaves(cmd):
 | 
|    procs = []
 | 
|    for (slave, _) in slaves:
 | 
|      os.chdir(os.path.join(buildbot_path, slave, 'buildbot'))
 | 
| -    procs.append((slave, _launch_cmd(cmd)))
 | 
| +    try:
 | 
| +      procs.append((slave, _launch_cmd(cmd)))
 | 
| +    except OSError as e:
 | 
| +      results[slave] = SingleCommandResults.make(stderr=str(e))
 | 
|  
 | 
|    for slavename, proc in procs:
 | 
|      results[slavename] = _get_result(proc)
 | 
|  
 | 
| -  return results
 | 
| +  return MultiCommandResults(results)
 | 
|  
 | 
|  
 | 
|  def _launch_on_remote_host(slave_host_name, cmd):
 | 
| @@ -250,66 +340,83 @@ def _launch_on_remote_host(slave_host_name, cmd):
 | 
|                       _fixup_cmd(cmd, slave_host_name))
 | 
|  
 | 
|  
 | 
| -def _get_remote_host_results(slave_host_name, popen):
 | 
| -  """Get the results from a running process. Blocks until the process completes.
 | 
| +def run_on_remote_host(slave_host_name, cmd):
 | 
| +  """Run a command on a remote slave host machine, blocking until completion.
 | 
|  
 | 
|    Args:
 | 
| -      slave_host_name: string; name of the remote host.
 | 
| -      popen: subprocess.Popen instance.
 | 
| +      slave_host_name: string; name of the slave host machine.
 | 
| +      cmd: list of strings or ResolvableCommandElements; the command to run.
 | 
|    Returns:
 | 
| -      A dictionary of results with the remote host machine name as its only key
 | 
| -      and individual result dictionaries (with stdout, stderr, and returncode as
 | 
| -      keys) its value.
 | 
| +      CommandResults instance containing the results of the command.
 | 
|    """
 | 
| -  result = _get_result(popen)
 | 
| -  if result.returncode:
 | 
| -    return { slave_host_name: result }
 | 
| -  try:
 | 
| -    return { slave_host_name: CommandResults.decode(result.stdout) }
 | 
| -  except (pickle.UnpicklingError, IndexError):
 | 
| -    error_msg = 'Could not decode result: %s' % result.stdout
 | 
| -    return { slave_host_name: CommandResults.make(stderr=error_msg) }
 | 
| +  proc = _launch_on_remote_host(slave_host_name, cmd)
 | 
| +  return _get_result(proc)
 | 
|  
 | 
|  
 | 
| -def run_on_remote_host(slave_host_name, cmd):
 | 
| -  """Run a command on a remote slave host machine, blocking until completion.
 | 
| +def _get_remote_slaves_cmd(cmd):
 | 
| +  """Build a command which runs the command on all slaves on a remote host.
 | 
| +
 | 
| +  Args:
 | 
| +      cmd: list of strings or ResolvableCommandElements; the command to run.
 | 
| +  Returns:
 | 
| +      list of strings or ResolvableCommandElements; a command which results in
 | 
| +      the given command being run on all of the slaves on the remote host.
 | 
| +  """
 | 
| +  return ['python',
 | 
| +          ResolvablePath.buildbot_path('scripts',
 | 
| +                                       'run_on_local_slaves.py')] + cmd
 | 
| +
 | 
| +
 | 
| +def run_on_remote_slaves(slave_host_name, cmd):
 | 
| +  """Run a command on each buildslave on a remote slave host machine, blocking
 | 
| +  until completion.
 | 
|  
 | 
|    Args:
 | 
|        slave_host_name: string; name of the slave host machine.
 | 
| -      cmd: list of strings; command to run.
 | 
| +      cmd: list of strings or ResolvableCommandElements; the command to run.
 | 
|    Returns:
 | 
| -      A dictionary of results with the remote host machine name as its only key
 | 
| -      and individual result dictionaries (with stdout, stderr, and returncode as
 | 
| -      keys) its value.
 | 
| +      MultiCommandResults instance with results from each slave on the remote
 | 
| +      host.
 | 
|    """
 | 
| -  proc = _launch_on_remote_host(slave_host_name, cmd)
 | 
| -  return _get_remote_host_results(slave_host_name, proc)
 | 
| +  proc = _launch_on_remote_host(slave_host_name, _get_remote_slaves_cmd(cmd))
 | 
| +  return _get_result(proc)
 | 
|  
 | 
|  
 | 
|  def run_on_all_slave_hosts(cmd):
 | 
|    """Run the given command on all slave hosts, blocking until all complete.
 | 
|  
 | 
|    Args:
 | 
| -      cmd: list of strings; command to run.
 | 
| +      cmd: list of strings or ResolvableCommandElements; the command to run.
 | 
|    Returns:
 | 
| -      A dictionary of results with host machine names as keys and individual
 | 
| -      result dictionaries (with stdout, stderr, and returncode as keys) as
 | 
| -      values.
 | 
| +      MultiCommandResults instance with results from each remote slave host.
 | 
|    """
 | 
|    results = {}
 | 
|    procs = []
 | 
|  
 | 
|    for hostname in slave_hosts_cfg.SLAVE_HOSTS.iterkeys():
 | 
|      if not slave_hosts_cfg.SLAVE_HOSTS[hostname].login_cmd:
 | 
| -      results.update(
 | 
| -          {hostname: CommandResults.make(stderr='No procedure for login.')})
 | 
| +      results.update({
 | 
| +          hostname: SingleCommandResults.make(stderr='No procedure for login.'),
 | 
| +      })
 | 
|      else:
 | 
|        procs.append((hostname, _launch_on_remote_host(hostname, cmd)))
 | 
|  
 | 
|    for slavename, proc in procs:
 | 
| -    results.update(_get_remote_host_results(slavename, proc))
 | 
| +    results[slavename] = _get_result(proc)
 | 
| +
 | 
| +  return MultiCommandResults(results)
 | 
| +
 | 
|  
 | 
| -  return results
 | 
| +def run_on_all_slaves_on_all_hosts(cmd):
 | 
| +  """Run the given command on all slaves on all hosts. Blocks until completion.
 | 
| +
 | 
| +  Args:
 | 
| +      cmd: list of strings or ResolvableCommandElements; the command to run.
 | 
| +  Returns:
 | 
| +      MultiCommandResults instance with results from each slave on each remote
 | 
| +      slave host.
 | 
| +  """
 | 
| +  return run_on_all_slave_hosts(_get_remote_slaves_cmd(cmd))
 | 
|  
 | 
|  
 | 
|  def parse_args(positional_args=None):
 | 
| 
 |