From 4e5e773f4bc008479a584693b2d0c5d501c1777b Mon Sep 17 00:00:00 2001 From: matthieuxyz Date: Mon, 4 Nov 2019 17:11:23 +0100 Subject: [PATCH 1/2] Workaroud for UnicodeDecodeError --- adb/adb_commands.py | 32 ++++++++++++++++++-- adb/adb_protocol.py | 56 +++++++++++++++++++++++++++++++++- test/adb_test.py | 74 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 151 insertions(+), 11 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 734e31c..6336f23 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -24,8 +24,8 @@ import io import os -import socket import posixpath +import socket from adb import adb_protocol from adb import common @@ -371,9 +371,12 @@ def Shell(self, command, timeout_ms=None): command: Shell command to run timeout_ms: Maximum time to allow the command to run. """ - return self.protocol_handler.Command( + if not isinstance(command, bytes): + command = command.encode('utf-8') + data = self.protocol_handler.BytesCommand( self._handle, service=b'shell', command=command, timeout_ms=timeout_ms) + return data.decode('utf-8') def StreamingShell(self, command, timeout_ms=None): """Run command on the device, yielding each line of output. @@ -389,6 +392,20 @@ def StreamingShell(self, command, timeout_ms=None): self._handle, service=b'shell', command=command, timeout_ms=timeout_ms) + def BytesStreamingShell(self, command, timeout_ms=None): + """Run command on the device, yielding each line of output. + + Args: + command: Command to run on the target. + timeout_ms: Maximum time to allow the command to run. + + Yields: + The responses from the shell command. + """ + return self.protocol_handler.BytesStreamingCommand( + self._handle, service=b'shell', command=command, + timeout_ms=timeout_ms) + def Logcat(self, options, timeout_ms=None): """Run 'shell logcat' and stream the output to stdout. @@ -398,6 +415,17 @@ def Logcat(self, options, timeout_ms=None): """ return self.StreamingShell('logcat %s' % options, timeout_ms) + def NonStreamingLogcat(self, options, timeout_ms=None): + """Run 'shell logcat' and stream the output to stdout. + + Args: + options: Arguments to pass to 'logcat'. + timeout_ms: Maximum time to allow the command to run. + """ + command = ('logcat %s' % options).encode('utf-8') + data = b''.join(self.BytesStreamingShell(command, timeout_ms)) + return data.decode('utf-8') + def InteractiveShell(self, cmd=None, strip_cmd=True, delim=None, strip_delim=True): """Get stdout from the currently open interactive shell and optionally run a command on the device, returning all output. diff --git a/adb/adb_protocol.py b/adb/adb_protocol.py index 4ff28c7..63baa3d 100644 --- a/adb/adb_protocol.py +++ b/adb/adb_protocol.py @@ -19,7 +19,9 @@ import struct import time +import warnings from io import BytesIO + from adb import usb_exceptions # Maximum amount of data in an ADB packet. @@ -431,13 +433,65 @@ def StreamingCommand(cls, usb, service, command='', timeout_ms=None): Yields: The responses from the service. """ + warnings.warn(DeprecationWarning("This method may fail with utf-8 multi-bytes sequences")) if not isinstance(command, bytes): command = command.encode('utf8') connection = cls.Open( usb, destination=b'%s:%s' % (service, command), timeout_ms=timeout_ms) for data in connection.ReadUntilClose(): - yield data.decode('utf8') + # TODO: find a fix for this code without changing public API + yield data.decode('utf-8') + + @classmethod + def BytesCommand(cls, usb, service, command=b'', timeout_ms=None): + """One complete set of USB packets for a single command. + + Sends service:command in a new connection, reading the data for the + response. All the data is held in memory, large responses will be slow and + can fill up memory. + + Args: + usb: USB device handle with BulkRead and BulkWrite methods. + service: The service on the device to talk to. + command: The command to send to the service. + timeout_ms: Timeout for USB packets, in milliseconds. + + Raises: + InterleavedDataError: Multiple streams running over usb. + InvalidCommandError: Got an unexpected response command. + + Returns: + The response from the service. + """ + return b''.join(cls.BytesStreamingCommand(usb, service, command, timeout_ms)) + + @classmethod + def BytesStreamingCommand(cls, usb, service, command=b'', timeout_ms=None): + """One complete set of USB packets for a single command. + + Sends service:command in a new connection, reading the data for the + response. All the data is held in memory, large responses will be slow and + can fill up memory. + + Args: + usb: USB device handle with BulkRead and BulkWrite methods. + service: The service on the device to talk to. + command: The command to send to the service. + timeout_ms: Timeout for USB packets, in milliseconds. + + Raises: + InterleavedDataError: Multiple streams running over usb. + InvalidCommandError: Got an unexpected response command. + + Returns: + The response from the service. + """ + connection = cls.Open( + usb, destination=b'%s:%s' % (service, command), + timeout_ms=timeout_ms) + for data in connection.ReadUntilClose(): + yield data @classmethod def InteractiveShellCommand(cls, conn, cmd=None, strip_cmd=True, delim=None, strip_delim=True, clean_stdout=True): diff --git a/test/adb_test.py b/test/adb_test.py index 0ce1ead..1e356a4 100755 --- a/test/adb_test.py +++ b/test/adb_test.py @@ -14,18 +14,18 @@ # limitations under the License. """Tests for adb.""" -from io import BytesIO import struct import unittest -from mock import mock +from io import BytesIO +from unittest import skip +import common_stub +from mock import mock -from adb import common from adb import adb_commands from adb import adb_protocol -from adb.usb_exceptions import TcpTimeoutException, DeviceNotFoundError -import common_stub - +from adb import common +from adb.usb_exceptions import TcpTimeoutException BANNER = b'blazetest' LOCAL_ID = 1 @@ -140,7 +140,7 @@ def testUninstall(self): def testStreamingResponseShell(self): command = b'keepin it real big' - # expect multiple lines + # expect multiple bulks responses = ['other stuff, ', 'and some words.'] @@ -149,11 +149,69 @@ def testStreamingResponseShell(self): dev = adb_commands.AdbCommands() dev.ConnectDevice(handle=usb, banner=BANNER) response_count = 0 - for (expected,actual) in zip(responses, dev.StreamingShell(command)): + for (expected, actual) in zip(responses, dev.StreamingShell(command)): self.assertEqual(expected, actual) response_count = response_count + 1 self.assertEqual(len(responses), response_count) + @skip('Need fixing') + def testStreamingResponseShellWithMultiBytesSequences(self): + command = b'keepin it real big' + # expect multiple bulks + + responses = [b'\xe2', b'\x81\x82'] # utf-8 encoded split Hiragana A (U+3042) + + usb = self._ExpectCommand(b'shell', command, *responses) + + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + + dev.StreamingShell(command) + + def testShellWithMultiBytesSequences(self): + command = b'keepin it real big' + # expect multiple bulks + + responses = [b'\xe3', b'\x81\x82'] # utf-8 encoded split Hiragana A (U+3042) + + usb = self._ExpectCommand(b'shell', command, *responses) + + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + + res = dev.Shell(command) + self.assertEqual('\u3042', res) + + def testBytesStreamingResponseShell(self): + command = b'keepin it real big' + # expect multiple bulks + + responses = [b'other stuff, ', b'and some words.'] + + usb = self._ExpectCommand(b'shell', command, *responses) + + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + response_count = 0 + for (expected, actual) in zip(responses, dev.BytesStreamingShell(command)): + self.assertEqual(expected, actual) + response_count = response_count + 1 + self.assertEqual(len(responses), response_count) + + def testNonStreamingLogcatWithMultiBytesSequences(self): + command = b'logcat test\xe3\x81\x82' + # expect multiple bulks + + responses = [b'\xe3', b'\x81\x82'] # utf-8 encoded split Hiragana A (U+3042) + + usb = self._ExpectCommand(b'shell', command, *responses) + + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + + res = dev.NonStreamingLogcat('test\u3042') + self.assertEqual(u'\u3042', res) + def testReboot(self): usb = self._ExpectCommand(b'reboot', b'', b'') dev = adb_commands.AdbCommands() From 5f45f7cde9d0a28723674296544392eae00f6ccd Mon Sep 17 00:00:00 2001 From: matthieuxyz Date: Mon, 4 Nov 2019 17:51:53 +0100 Subject: [PATCH 2/2] Fix for python2 --- test/adb_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adb_test.py b/test/adb_test.py index 1e356a4..cd35b5c 100755 --- a/test/adb_test.py +++ b/test/adb_test.py @@ -180,7 +180,7 @@ def testShellWithMultiBytesSequences(self): dev.ConnectDevice(handle=usb, banner=BANNER) res = dev.Shell(command) - self.assertEqual('\u3042', res) + self.assertEqual(u'\u3042', res) def testBytesStreamingResponseShell(self): command = b'keepin it real big' @@ -209,7 +209,7 @@ def testNonStreamingLogcatWithMultiBytesSequences(self): dev = adb_commands.AdbCommands() dev.ConnectDevice(handle=usb, banner=BANNER) - res = dev.NonStreamingLogcat('test\u3042') + res = dev.NonStreamingLogcat(u'test\u3042') self.assertEqual(u'\u3042', res) def testReboot(self):