adb: Fix PTY logic for non-interactive shells.
Change `adb shell` so that interactive sessions use a PTY but
non-interactive do not. This matches `ssh` functionality better
and also enables future work to split stdout/stderr for
non-interactive sessions.
A test to verify this behavior is added to test_device.py with
supporting modifications in device.py.
Bug: http://b/21215503
Change-Id: Ib4ba40df85f82ddef4e0dd557952271c859d1c7b
diff --git a/adb/device.py b/adb/device.py
index a15675b..bc1364b 100644
--- a/adb/device.py
+++ b/adb/device.py
@@ -101,6 +101,19 @@
class AndroidDevice(object):
+ # Delimiter string to indicate the start of the exit code.
+ _RETURN_CODE_DELIMITER = 'x'
+
+ # Follow any shell command with this string to get the exit
+ # status of a program since this isn't propagated by adb.
+ #
+ # The delimiter is needed because `printf 1; echo $?` would print
+ # "10", and we wouldn't be able to distinguish the exit code.
+ _RETURN_CODE_PROBE_STRING = 'echo "{0}$?"'.format(_RETURN_CODE_DELIMITER)
+
+ # Maximum search distance from the output end to find the delimiter.
+ _RETURN_CODE_SEARCH_LENGTH = len('{0}255\n'.format(_RETURN_CODE_DELIMITER))
+
def __init__(self, serial, product=None):
self.serial = serial
self.product = product
@@ -110,40 +123,44 @@
if self.product is not None:
self.adb_cmd.extend(['-p', product])
self._linesep = None
- self._shell_result_pattern = None
@property
def linesep(self):
if self._linesep is None:
- self._linesep = subprocess.check_output(['adb', 'shell', 'echo'])
+ self._linesep = subprocess.check_output(self.adb_cmd +
+ ['shell', 'echo'])
return self._linesep
def _make_shell_cmd(self, user_cmd):
- # Follow any shell command with `; echo; echo $?` to get the exit
- # status of a program since this isn't propagated by adb.
- #
- # The leading newline is needed because `printf 1; echo $?` would print
- # "10", and we wouldn't be able to distinguish the exit code.
- rc_probe = '; echo "\n$?"'
- return self.adb_cmd + ['shell'] + user_cmd + [rc_probe]
+ return (self.adb_cmd + ['shell'] + user_cmd +
+ ['; ' + self._RETURN_CODE_PROBE_STRING])
- def _parse_shell_output(self, out): # pylint: disable=no-self-use
+ def _parse_shell_output(self, out):
+ """Finds the exit code string from shell output.
+
+ Args:
+ out: Shell output string.
+
+ Returns:
+ An (exit_code, output_string) tuple. The output string is
+ cleaned of any additional stuff we appended to find the
+ exit code.
+
+ Raises:
+ RuntimeError: Could not find the exit code in |out|.
+ """
search_text = out
- max_result_len = len('{0}255{0}'.format(self.linesep))
- if len(search_text) > max_result_len:
- # We don't want to regex match over massive amounts of data when we
- # know the part we want is right at the end.
- search_text = search_text[-max_result_len:]
- if self._shell_result_pattern is None:
- self._shell_result_pattern = re.compile(
- r'({0}\d+{0})$'.format(self.linesep), re.MULTILINE)
- m = self._shell_result_pattern.search(search_text)
- if m is None:
+ if len(search_text) > self._RETURN_CODE_SEARCH_LENGTH:
+ # We don't want to search over massive amounts of data when we know
+ # the part we want is right at the end.
+ search_text = search_text[-self._RETURN_CODE_SEARCH_LENGTH:]
+ partition = search_text.rpartition(self._RETURN_CODE_DELIMITER)
+ if partition[1] == '':
raise RuntimeError('Could not find exit status in shell output.')
-
- result_text = m.group(1)
- result = int(result_text.strip())
- out = out[:-len(result_text)] # Trim the result text from the output.
+ result = int(partition[2])
+ # partition[0] won't contain the full text if search_text was truncated,
+ # pull from the original string instead.
+ out = out[:-len(partition[1]) - len(partition[2])]
return result, out
def _simple_call(self, cmd):