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