|
| 1 | +# adb/commands.py - Run executables on an Android device -*- python -*- |
| 2 | +# |
| 3 | +# This source file is part of the Swift.org open source project |
| 4 | +# |
| 5 | +# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors |
| 6 | +# Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +# |
| 8 | +# See http://swift.org/LICENSE.txt for license information |
| 9 | +# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +# |
| 11 | +# ---------------------------------------------------------------------------- |
| 12 | +# |
| 13 | +# Push executables to an Android device and run them, capturing their output |
| 14 | +# and exit code. |
| 15 | +# |
| 16 | +# ---------------------------------------------------------------------------- |
| 17 | + |
| 18 | +from __future__ import print_function |
| 19 | + |
| 20 | +import subprocess |
| 21 | +import tempfile |
| 22 | +import uuid |
| 23 | + |
| 24 | + |
| 25 | +# A temporary directory on the Android device. |
| 26 | +DEVICE_TEMP_DIR = '/data/local/tmp' |
| 27 | + |
| 28 | + |
| 29 | +def shell(args): |
| 30 | + """ |
| 31 | + Execute 'adb shell' with the given arguments. |
| 32 | +
|
| 33 | + Raise an exception if 'adb shell' returns a non-zero exit code. |
| 34 | + Note that this only occurs if communication with the connected device |
| 35 | + fails, not if the command run on the device fails. |
| 36 | + """ |
| 37 | + return subprocess.check_output(['adb', 'shell'] + args) |
| 38 | + |
| 39 | + |
| 40 | +def rmdir(path): |
| 41 | + """Remove all files in the device directory at `path`.""" |
| 42 | + shell(['rm', '-rf', '{}/*'.format(path)]) |
| 43 | + |
| 44 | + |
| 45 | +def push(local_path, device_path): |
| 46 | + """Move the file at the given local path to the path on the device.""" |
| 47 | + return subprocess.check_output(['adb', 'push', local_path, device_path], |
| 48 | + stderr=subprocess.STDOUT).strip() |
| 49 | + |
| 50 | + |
| 51 | +def reboot(): |
| 52 | + """Reboot the connected Android device, waiting for it to return online.""" |
| 53 | + subprocess.check_call(['adb', 'reboot']) |
| 54 | + subprocess.check_call(['adb', 'wait-for-device']) |
| 55 | + |
| 56 | + |
| 57 | +def _create_executable_on_device(device_path, contents): |
| 58 | + _, tmp = tempfile.mkstemp() |
| 59 | + with open(tmp, 'w') as f: |
| 60 | + f.write(contents) |
| 61 | + push(tmp, device_path) |
| 62 | + shell(['chmod', '755', device_path]) |
| 63 | + |
| 64 | + |
| 65 | +def execute_on_device(executable_path, executable_arguments): |
| 66 | + """ |
| 67 | + Run an executable on an Android device. |
| 68 | +
|
| 69 | + Push an executable at the given 'executable_path' to an Android device, |
| 70 | + then execute that executable on the device, passing any additional |
| 71 | + 'executable_arguments'. Return 0 if the executable succeeded when run on |
| 72 | + device, and 1 otherwise. |
| 73 | +
|
| 74 | + This function is not as simple as calling 'adb shell', for two reasons: |
| 75 | +
|
| 76 | + 1. 'adb shell' can only take input up to a certain length, so it fails for |
| 77 | + long executable names or when a large amount of arguments are passed to |
| 78 | + the executable. This function attempts to limit the size of any string |
| 79 | + passed to 'adb shell'. |
| 80 | + 2. 'adb shell' ignores the exit code of any command it runs. This function |
| 81 | + therefore uses its own mechanisms to determine whether the executable |
| 82 | + had a successful exit code when run on device. |
| 83 | + """ |
| 84 | + # We'll be running the executable in a temporary directory in |
| 85 | + # /data/local/tmp. `adb shell` has trouble with commands that |
| 86 | + # exceed a certain length, so to err on the safe side we only |
| 87 | + # use the first 10 characters of the UUID. |
| 88 | + uuid_dir = '{}/{}'.format(DEVICE_TEMP_DIR, str(uuid.uuid4())[:10]) |
| 89 | + shell(['mkdir', '-p', uuid_dir]) |
| 90 | + |
| 91 | + # `adb` can only handle commands under a certain length. No matter what the |
| 92 | + # original executable's name, on device we call it `__executable`. |
| 93 | + executable = '{}/__executable'.format(uuid_dir) |
| 94 | + push(executable_path, executable) |
| 95 | + |
| 96 | + # When running the executable on the device, we need to pass it the same |
| 97 | + # arguments, as well as specify the correct LD_LIBRARY_PATH. Save these |
| 98 | + # to a file we can easily call multiple times. |
| 99 | + executable_with_args = '{}/__executable_with_args'.format(uuid_dir) |
| 100 | + _create_executable_on_device( |
| 101 | + executable_with_args, |
| 102 | + 'LD_LIBRARY_PATH={uuid_dir}:{tmp_dir} ' |
| 103 | + '{executable} {executable_arguments}'.format( |
| 104 | + uuid_dir=uuid_dir, |
| 105 | + tmp_dir=DEVICE_TEMP_DIR, |
| 106 | + executable=executable, |
| 107 | + executable_arguments=' '.join(executable_arguments))) |
| 108 | + |
| 109 | + # Write the output from the test executable to a file named '__stdout', and |
| 110 | + # if the test executable succeeds, write 'SUCCEEDED' to a file |
| 111 | + # named '__succeeded'. We do this because `adb shell` does not report |
| 112 | + # the exit code of the command it executes on the device, so instead we |
| 113 | + # check the '__succeeded' file for our string. |
| 114 | + executable_stdout = '{}/__stdout'.format(uuid_dir) |
| 115 | + succeeded_token = 'SUCCEEDED' |
| 116 | + executable_succeeded = '{}/__succeeded'.format(uuid_dir) |
| 117 | + executable_piped = '{}/__executable_piped'.format(uuid_dir) |
| 118 | + _create_executable_on_device( |
| 119 | + executable_piped, |
| 120 | + '{executable_with_args} > {executable_stdout} && ' |
| 121 | + 'echo "{succeeded_token}" > {executable_succeeded}'.format( |
| 122 | + executable_with_args=executable_with_args, |
| 123 | + executable_stdout=executable_stdout, |
| 124 | + succeeded_token=succeeded_token, |
| 125 | + executable_succeeded=executable_succeeded)) |
| 126 | + |
| 127 | + # We've pushed everything we need to the device. |
| 128 | + # Now execute the wrapper script. |
| 129 | + shell([executable_piped]) |
| 130 | + |
| 131 | + # Grab the results of running the executable on device. |
| 132 | + stdout = shell(['cat', executable_stdout]) |
| 133 | + exitcode = shell(['cat', executable_succeeded]) |
| 134 | + if not exitcode.startswith(succeeded_token): |
| 135 | + debug_command = '$ adb shell {}'.format(executable_with_args) |
| 136 | + print('Executable exited with a non-zero code on the Android device.\n' |
| 137 | + 'Device stdout:\n' |
| 138 | + '{stdout}\n' |
| 139 | + 'To debug, run:\n' |
| 140 | + '{debug_command}\n'.format( |
| 141 | + stdout=stdout, |
| 142 | + debug_command=debug_command)) |
| 143 | + |
| 144 | + # Exit early so that the output isn't passed to FileCheck, nor are any |
| 145 | + # temporary directories removed; this allows the user to re-run |
| 146 | + # the executable on the device. |
| 147 | + return 1 |
| 148 | + |
| 149 | + print(stdout) |
| 150 | + |
| 151 | + shell(['rm', '-rf', uuid_dir]) |
| 152 | + return 0 |
0 commit comments