Android: 取得 ADB Shell Command 的 Exit Status

透過非零值的 exit status,可以讓 caller 知道過程中是否發生錯誤:

$ adb pull /system/etc/hosts
0 KB/s (25 bytes in 0.076s)
$ echo $?
0
$ adb pull /system/etc/hosts-xxx
remote object '/system/etc/hosts-xxx' does not exist
$ echo $?
1

但是執行 shell commands 時,exit status 卻固定傳回 0:

$ adb shell cat /system/etc/hosts
127.0.0.1                   localhost
$ echo $?
0
$ adb shell cat /system/etc/hosts-xxx
/system/etc/hosts-xxx: No such file or directory
$ echo $?
0

這個問題還存在最新的 Android SDK Platform-tools r11,導致自動化過程中已經發生錯誤了還不知道,網路上有不少人在提這個問題:

所有的解法都回歸到在 shell commands 後面加上 echo $?,然後 caller 再從 output 去解析出 exit status。這個過程很容易出錯,值得包裝成 helper:

adbshell.py

import subprocess

def adb_shell(shell_cmds):
    shell_cmds += '; echo $?'
    cmds = ['adb', 'shell', shell_cmds]
    stdout = subprocess.Popen(cmds, stdout=subprocess.PIPE).communicate()[0]

    lines = stdout.splitlines()
    print repr(stdout), lines
    retcode = int(lines[-1])
    if retcode !=0:
        errmsg = 'failed to execute ADB shell commands (%i)' % retcode
        if len(lines) > 1: errmsg += '\n' + '\n'.join(lines[:-1])
        raise RuntimeError(errmsg)
    return stdout

在 Linux 下做了一下簡單的測試:

$ adb kill-server 1
$ python
>>> from adbshell import adb_shell
>>> print adb_shell('cat /system/etc/hosts')
'* daemon not running. starting it now on port 5037 *\n* daemon started successfully *\n127.0.0.1\t\t    localhost\r\n0\r\n' ['* daemon not running. starting it now on port 5037 *', '* daemon started successfully *', '127.0.0.1\t\t    localhost', '0']
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
127.0.0.1                   localhost
0

>>> print adb_shell('cat /system/etc/hosts')
'127.0.0.1\t\t    localhost\r\n0\r\n' ['127.0.0.1\t\t    localhost', '0']
127.0.0.1                   localhost
0

>>> print adb_shell('cat /system/etc/hosts-xxx')
'/system/etc/hosts-xxx: No such file or directory\r\n1\r\n' ['/system/etc/hosts-xxx: No such file or directory', '1']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "adbshell.py", line 14, in adb_shell
    raise RuntimeError(errmsg)
RuntimeError: failed to execute ADB shell commands (1)
/system/etc/hosts-xxx: No such file or directory
1 刻意先將 ADB server 停掉(然後重新接上設備),觀察第一次連接上設備時會有什麼狀況。結果只是多輸出 ADB server 啟動時的兩行訊息而已。

不過在 Windows 下測試,第一個動作就爆了:

C:\> adb kill-server
C:\> python
>>> from adbshell import adb_shell
>>> print adb_shell('cat /system/etc/hosts')
'127.0.0.1\t\t    localhost\r\r\n0\r\r\n' ['127.0.0.1\t\t    localhost', '', '0'
, '']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "adbshell.py", line 10, in adb_shell
    retcode = int(lines[-1])
ValueError: invalid literal for int() with base 10: ''

不知道為何會有 localhost\r\n0\r\n (Linux) 與 localhost\r\r\n0\r\r\n (Windows) 的差別?不過這個問題可以暫時將 output 尾部的空白截掉:

adbshell.py

import subprocess

def adb_shell(shell_cmds):
    shell_cmds += '; echo $?'
    cmds = ['adb', 'shell', shell_cmds]
    stdout = subprocess.Popen(cmds, stdout=subprocess.PIPE).communicate()[0].rstrip()

    lines = stdout.splitlines()
    print repr(stdout), lines
    retcode = int(lines[-1])
    if retcode !=0:
        errmsg = 'failed to execute ADB shell commands (%i)' % retcode
        if len(lines) > 1: errmsg += '\n' + '\n'.join(lines[:-1])
        raise RuntimeError(errmsg)
    return stdout

在 Windows 下再測試一遍:

>>> from adbshell import adb_shell
>>> print adb_shell('cat /system/etc/hosts')
'127.0.0.1\t\t    localhost\r\r\n0' ['127.0.0.1\t\t    localhost', '', '0']
127.0.0.1                   localhost
0