Home | History | Annotate | Download | only in proc
      1 # Copyright 2016 - The Android Open Source Project
      2 #
      3 # Licensed under the Apache License, Version 2.0 (the "License");
      4 # you may not use this file except in compliance with the License.
      5 # You may obtain a copy of the License at
      6 #
      7 #     http://www.apache.org/licenses/LICENSE-2.0
      8 #
      9 # Unless required by applicable law or agreed to in writing, software
     10 # distributed under the License is distributed on an "AS IS" BASIS,
     11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 # See the License for the specific language governing permissions and
     13 # limitations under the License.
     14 
     15 import logging
     16 import os
     17 import shlex
     18 import sys
     19 import time
     20 
     21 if os.name == 'posix' and sys.version_info[0] < 3:
     22     import subprocess32 as subprocess
     23     DEVNULL = open(os.devnull, 'wb')
     24 else:
     25     import subprocess
     26     # Only exists in python3.3
     27     from subprocess import DEVNULL
     28 
     29 
     30 class Error(Exception):
     31     """Indicates that a command failed, is fatal to the test unless caught."""
     32 
     33     def __init__(self, result):
     34         super(Error, self).__init__(result)
     35         self.result = result
     36 
     37 
     38 class TimeoutError(Error):
     39     """Thrown when a BackgroundJob times out on wait."""
     40 
     41 
     42 class Result(object):
     43     """Command execution result.
     44 
     45     Contains information on subprocess execution after it has exited.
     46 
     47     Attributes:
     48         command: An array containing the command and all arguments that
     49                  was executed.
     50         exit_status: Integer exit code of the process.
     51         stdout_raw: The raw bytes output from standard out.
     52         stderr_raw: The raw bytes output from standard error
     53         duration: How long the process ran for.
     54         did_timeout: True if the program timed out and was killed.
     55     """
     56 
     57     @property
     58     def stdout(self):
     59         """String representation of standard output."""
     60         if not self._stdout_str:
     61             self._stdout_str = self._raw_stdout.decode(encoding=self._encoding)
     62             self._stdout_str = self._stdout_str.strip()
     63         return self._stdout_str
     64 
     65     @property
     66     def stderr(self):
     67         """String representation of standard error."""
     68         if not self._stderr_str:
     69             self._stderr_str = self._raw_stderr.decode(encoding=self._encoding)
     70             self._stderr_str = self._stderr_str.strip()
     71         return self._stderr_str
     72 
     73     def __init__(self,
     74                  command=[],
     75                  stdout=bytes(),
     76                  stderr=bytes(),
     77                  exit_status=None,
     78                  duration=0,
     79                  did_timeout=False,
     80                  encoding='utf-8'):
     81         """
     82         Args:
     83             command: The command that was run. This will be a list containing
     84                      the executed command and all args.
     85             stdout: The raw bytes that standard output gave.
     86             stderr: The raw bytes that standard error gave.
     87             exit_status: The exit status of the command.
     88             duration: How long the command ran.
     89             did_timeout: True if the command timed out.
     90             encoding: The encoding standard that the program uses.
     91         """
     92         self.command = command
     93         self.exit_status = exit_status
     94         self._raw_stdout = stdout
     95         self._raw_stderr = stderr
     96         self._stdout_str = None
     97         self._stderr_str = None
     98         self._encoding = encoding
     99         self.duration = duration
    100         self.did_timeout = did_timeout
    101 
    102     def __repr__(self):
    103         return ('job.Result(command=%r, stdout=%r, stderr=%r, exit_status=%r, '
    104                 'duration=%r, did_timeout=%r, encoding=%r)') % (
    105                     self.command, self._raw_stdout, self._raw_stderr,
    106                     self.exit_status, self.duration, self.did_timeout,
    107                     self._encoding)
    108 
    109 
    110 def run(command,
    111         timeout=60,
    112         ignore_status=False,
    113         env=None,
    114         io_encoding='utf-8'):
    115     """Execute a command in a subproccess and return its output.
    116 
    117     Commands can be either shell commands (given as strings) or the
    118     path and arguments to an executable (given as a list).  This function
    119     will block until the subprocess finishes or times out.
    120 
    121     Args:
    122         command: The command to execute. Can be either a string or a list.
    123         timeout: number seconds to wait for command to finish.
    124         ignore_status: bool True to ignore the exit code of the remote
    125                        subprocess.  Note that if you do ignore status codes,
    126                        you should handle non-zero exit codes explicitly.
    127         env: dict enviroment variables to setup on the remote host.
    128         io_encoding: str unicode encoding of command output.
    129 
    130     Returns:
    131         A job.Result containing the results of the ssh command.
    132 
    133     Raises:
    134         job.TimeoutError: When the remote command took to long to execute.
    135         Error: When the ssh connection failed to be created.
    136         CommandError: Ssh worked, but the command had an error executing.
    137     """
    138     start_time = time.time()
    139     proc = subprocess.Popen(
    140         command,
    141         env=env,
    142         stdout=subprocess.PIPE,
    143         stderr=subprocess.PIPE,
    144         shell=not isinstance(command, list))
    145     # Wait on the process terminating
    146     timed_out = False
    147     out = bytes()
    148     err = bytes()
    149     try:
    150         (out, err) = proc.communicate(timeout=timeout)
    151     except subprocess.TimeoutExpired:
    152         timed_out = True
    153         proc.kill()
    154         proc.wait()
    155 
    156     result = Result(
    157         command=command,
    158         stdout=out,
    159         stderr=err,
    160         exit_status=proc.returncode,
    161         duration=time.time() - start_time,
    162         encoding=io_encoding,
    163         did_timeout=timed_out)
    164     logging.debug(result)
    165 
    166     if timed_out:
    167         logging.error("Command %s with %s timeout setting timed out", command,
    168                       timeout)
    169         raise TimeoutError(result)
    170 
    171     if not ignore_status and proc.returncode != 0:
    172         raise Error(result)
    173 
    174     return result
    175 
    176 
    177 def run_async(command, env=None):
    178     """Execute a command in a subproccess asynchronously.
    179 
    180     It is the callers responsibility to kill/wait on the resulting
    181     subprocess.Popen object.
    182 
    183     Commands can be either shell commands (given as strings) or the
    184     path and arguments to an executable (given as a list).  This function
    185     will not block.
    186 
    187     Args:
    188         command: The command to execute. Can be either a string or a list.
    189         env: dict enviroment variables to setup on the remote host.
    190 
    191     Returns:
    192         A subprocess.Popen object representing the created subprocess.
    193 
    194     """
    195     proc = subprocess.Popen(
    196         command,
    197         env=env,
    198         preexec_fn=os.setpgrp,
    199         shell=not isinstance(command, list),
    200         stdout=DEVNULL,
    201         stderr=subprocess.STDOUT)
    202     logging.debug("command %s started with pid %s", command, proc.pid)
    203     return proc
    204 
    205