David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 1 | # Copyright (c) 2011 The Chromium OS 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 | |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 5 | import logging |
| 6 | import os |
| 7 | import pipes |
| 8 | import threading |
| 9 | |
Christopher Wiley | c8769cf | 2013-04-03 11:40:09 -0700 | [diff] [blame] | 10 | from autotest_lib.client.common_lib import error |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 11 | |
| 12 | class _HelperThread(threading.Thread): |
| 13 | """Make a thread to run the command in.""" |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 14 | def __init__(self, host, cmd): |
| 15 | super(_HelperThread, self).__init__() |
| 16 | self._host = host |
| 17 | self._cmd = cmd |
| 18 | self._result = None |
| 19 | self.daemon = True |
| 20 | |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 21 | |
| 22 | def run(self): |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 23 | logging.info('Helper thread running: %s', self._cmd) |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 24 | # NB: set ignore_status as we're always terminated w/ pkill |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 25 | self._result = self._host.run(self._cmd, ignore_status=True) |
| 26 | |
| 27 | |
| 28 | @property |
| 29 | def result(self): |
| 30 | """ |
| 31 | @returns string result of running our command if the command has |
| 32 | finished, and None otherwise. |
| 33 | |
| 34 | """ |
| 35 | return self._result |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 36 | |
| 37 | |
| 38 | class Command(object): |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 39 | """ |
| 40 | Encapsulates a command run on a remote machine. |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 41 | |
| 42 | Future work is to have this get the PID (by prepending 'echo $$; |
| 43 | exec' to the command and parsing the output). |
| 44 | |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 45 | """ |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 46 | def __init__(self, host, cmd, pkill_argument=None): |
| 47 | """ |
| 48 | Run a command on a remote host in the background. |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 49 | |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 50 | @param host Host object representing the remote machine. |
| 51 | @param cmd String command to run on the remote machine. |
| 52 | @param pkill_argument String argument to pkill to kill the remote |
| 53 | process. |
| 54 | |
| 55 | """ |
| 56 | if pkill_argument is None: |
Christopher Wiley | c8769cf | 2013-04-03 11:40:09 -0700 | [diff] [blame] | 57 | # Attempt to guess what a suitable pkill argument would look like. |
| 58 | pkill_argument = os.path.basename(cmd.split()[0]) |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 59 | self._command_name = pipes.quote(pkill_argument) |
| 60 | self._host = host |
| 61 | self._thread = _HelperThread(self._host, cmd) |
| 62 | self._thread.start() |
| 63 | |
| 64 | |
Christopher Wiley | c8769cf | 2013-04-03 11:40:09 -0700 | [diff] [blame] | 65 | def join(self, signal=None, timeout=5.0): |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 66 | """ |
| 67 | Kills the remote command and waits until it dies. Takes an optional |
| 68 | signal argument to control which signal to send the process to be |
| 69 | killed. |
| 70 | |
| 71 | @param signal Signal string to give to pkill (e.g. SIGNAL_INT). |
Christopher Wiley | c8769cf | 2013-04-03 11:40:09 -0700 | [diff] [blame] | 72 | @param timeout float number of seconds to wait for join to finish. |
| 73 | |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 74 | """ |
| 75 | if signal is None: |
| 76 | signal_arg = '' |
| 77 | else: |
| 78 | # In theory, it should be hard to pass something evil for signal if |
| 79 | # we make sure it's an integer before passing it to pkill. |
| 80 | signal_arg = '-' + str(int(signal)) |
| 81 | |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 82 | # Ignore status because the command may have exited already |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 83 | self._host.run("pkill %s %s" % (signal_arg, self._command_name), |
| 84 | ignore_status=True) |
Christopher Wiley | c8769cf | 2013-04-03 11:40:09 -0700 | [diff] [blame] | 85 | self._thread.join(timeout) |
| 86 | if self._thread.isAlive(): |
| 87 | raise error.TestFail('Failed to kill remote command: %s' % |
| 88 | self._command_name) |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 89 | |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 90 | |
| 91 | def __enter__(self): |
| 92 | return self |
| 93 | |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 94 | |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 95 | def __exit__(self, exception, value, traceback): |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 96 | self.join() |
David Rochberg | 410ea82 | 2011-11-18 11:39:59 -0500 | [diff] [blame] | 97 | return False |
Christopher Wiley | ffffab7 | 2013-03-06 16:14:11 -0800 | [diff] [blame] | 98 | |
| 99 | |
| 100 | @property |
| 101 | def result(self): |
| 102 | """ |
| 103 | @returns string result of running our command if the command has |
| 104 | finished, and None otherwise. |
| 105 | |
| 106 | """ |
| 107 | return self._thread.result |