question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

AdbDeviceTcp.shell() ignores timeouts specfied in AdbDeviceTcp constructor

See original GitHub issue

Description

Hi Jeff,

Great tool, thanks a ton for making it available!

I’m having trouble specifying custom timeouts. It seems that a call to AdbDeviceTcp.shell() uses/propagates the timeout values provided as arguments to AdbDeviceTcp.shell() even when these parameters are omitted. This results in timeout values provided to the AdbDeviceTcp constructor being overwritten. I’m not sure if this is intentional (I assume not), or if I am using the API wrong.

For example:

# Read ADB keys
pubkey_path = pathlib.Path(pubkey_path).expanduser()
privkey_path = pathlib.Path(privkey_path).expanduser()
with open(pubkey_path) as f:
    pubkey = f.read()
with open(privkey_path) as f:
    privkey = f.read()
adbkeypair = PythonRSASigner(pubkey, privkey)

# Connect to Fire TV, specifying custom timeout values
ftv = AdbDeviceTcp(firetv_ip, firetv_port, default_transport_timeout_s=60.0)
ftv.connect(rsa_keys=[adbkeypair], transport_timeout_s=60.0, auth_timeout_s=60.0, read_timeout_s=60.0)

# Perform a (long) sequence of ADB commands (calls to AdbDeviceTcp.shell()), e.g. let's uninstall all 3rd party apps (assuming you have A LOT installed):
pkgs_str = ftv.shell('pm list packages -3')
pkgs = [p.replace('package:', '', 1) for p in pkgs_str.split('\r\n') if p.startswith('package:')]
for pkgname in pkgs:
    print(f'Uninstalling {pkgname} ...')
    ftv.shell(f'pm uninstall {pkgname}')

Using code similar to the example above (multiple calls to AdbDeviceTcp.shell(), although not just uninstall commands), I occasionally get TcpTimeoutExceptions with a message indicating that the (read) timeout was 10 seconds (the default value) rather than the expected 60 seconds (the value I provided in the AdbDeviceTcp constructor and in AdbDeviceTcp.connect(), although the latter may be unrelated as that’s a transport timeout):

adb_shell.exceptions.TcpTimeoutException: Reading from <device_ip>:<device_port> timed out (10.0 seconds)

Platform details (in case it matters)

I’m using adb_shell to control a Fire TV Stick with Alexa Voice Remote (i.e., Fire TV Stick 2nd generation that was reissued early 2019), that runs Fire OS 5 which is based on Android 5.1 (I’m on Fire OS 5.2.7.7 to be exact). This device is rather resource constrained and is in some instances rather slow at responding to ADB commands (including when using the ADB binary directly, not involving adb_shell), thus the default timeout is simply too strict for this particular device.

I’m using the latest adb_shell release:

$ pip show adb_shell
Name: adb-shell
Version: 0.2.3
Summary: A Python implementation of ADB with shell and FileSync functionality.
Home-page: https://github.com/JeffLIrion/adb_shell
Author: Jeff Irion
Author-email: jefflirion@users.noreply.github.com
License: UNKNOWN
Location: /home/username/python_venvs/some_venv/lib/python3.7/site-packages
Requires: rsa, pyasn1, cryptography
Required-by: 

Full stack trace

An example of the full stack trace (I replaced local paths and IP addresses with placeholders):

Traceback (most recent call last):
  File "main.py", line 93, in <module>
    firetv_port=args.firetv_port)
  File "main.py", line 56, in main
    ftv_controller.uninstall_app(p)
  File "/home/username/src/firetv_controller.py", line 214, in uninstall_app
    return self.firetv.shell(f'pm uninstall {pkgname}')
  File "/home/username/python_venvs/some_venv/lib/python3.7/site-packages/adb_shell/adb_device.py", line 397, in shell
    return self._service(b'shell', command.encode('utf8'), transport_timeout_s, read_timeout_s, timeout_s, decode)
  File "/home/username/python_venvs/some_venv/lib/python3.7/site-packages/adb_shell/adb_device.py", line 302, in _service
    return b''.join(self._streaming_command(service, command, adb_info)).decode('utf8')
  File "/home/username/python_venvs/some_venv/lib/python3.7/site-packages/adb_shell/adb_device.py", line 940, in _streaming_command
    for data in self._read_until_close(adb_info):
  File "/home/username/python_venvs/some_venv/lib/python3.7/site-packages/adb_shell/adb_device.py", line 876, in _read_until_close
    cmd, data = self._read_until([constants.CLSE, constants.WRTE], adb_info)
  File "/home/username/python_venvs/some_venv/lib/python3.7/site-packages/adb_shell/adb_device.py", line 832, in _read_until
    cmd, remote_id2, local_id2, data = self._read(expected_cmds, adb_info)
  File "/home/username/python_venvs/some_venv/lib/python3.7/site-packages/adb_shell/adb_device.py", line 765, in _read
    msg = self._transport.bulk_read(constants.MESSAGE_SIZE, adb_info.transport_timeout_s)
  File "/home/username/python_venvs/some_venv/lib/python3.7/site-packages/adb_shell/transport/tcp_transport.py", line 127, in bulk_read
    raise TcpTimeoutException(msg)
adb_shell.exceptions.TcpTimeoutException: Reading from <FIRE_TV_IP>:<FIRE_TV_PORT> timed out (10.0 seconds)

Workaround

I’ve found a hacky workaround that seems to solve the issue for me for now. I manually modified /home/username/python_venvs/some_venv/lib/python3.7/site-packages/adb_shell/adb_device.py, adding the highlighted block of code to AdbDevice.shell():

class AdbDevice(object):
    # ...
    def shell(self, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None, decode=True):
        if not self.available:
            raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established.  (Did you call `AdbDevice.connect()`?)")
         # --- NEW CODE ---
        if self._transport is not None and isinstance(self._transport, TcpTransport):
            if transport_timeout_s is None:
                # Use timeout specified in encapsulated TcpTransport if no overwriting transport timeout specified here.
                transport_timeout_s = self._transport._default_transport_timeout_s
            if read_timeout_s == constants.DEFAULT_READ_TIMEOUT_S and self._transport._default_transport_timeout_s is not None:
                # Use timeout specified in encapsulated TcpTransport if no overwriting transport timeout specified here.
                # Note: does not handle the case where the caller explicitly sets read_timeout_s to constants.DEFAULT_READ_TIMEOUT_S
                read_timeout_s = self._transport._default_transport_timeout_s
        # ----------------
        return self._service(b'shell', command.encode('utf8'), transport_timeout_s, read_timeout_s, timeout_s, decode)

This is probably not the right place for this change - I went with the most straightforward solution that seemed to fit my needs. I’m guessing it should probably go in the AdbDeviceTcp subclass s.t. you don’t have to do type checks, but I don’t understand the codebase well enough to be the judge of that. Also note that the hack seems to fail if the user modifies adb_shell.constants.DEFAULT_READ_TIMEOUT_S, e.g., something like this:

# Attempt to increase the read timeout globally
adb_shell.constants.DEFAULT_READ_TIMEOUT_S = 60.0
# Connect to Fire TV, specifying custom timeout values
ftv = AdbDeviceTcp(firetv_ip, firetv_port, default_transport_timeout_s=60.0)
ftv.connect(rsa_keys=[adbkeypair], transport_timeout_s=60.0, auth_timeout_s=60.0, read_timeout_s=60.0)
pkgname = 'com.my.app'
ftv.shell(f'pm uninstall {pkgname}')

If I recall correctly, AdbDeviceTcp.shell() still gets called with read_timeout_s=10 for this scenario, which makes the if read_timeout_s == constants.DEFAULT_READ_TIMEOUT_S and ... check in the hack above false as constants.DEFAULT_READ_TIMEOUT_S == 60.0.

Once again thanks for providing a great, well-documented tool.

Best, Janus

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
JeffLIrioncommented, Dec 11, 2020

FYI:

This device is rather resource constrained and is in some instances rather slow at responding to ADB commands (including when using the ADB binary directly, not involving adb_shell)

https://www.home-assistant.io/integrations/androidtv/#androidtvlearn_sendevent-for-faster-adb-commands

0reactions
jvmkcommented, Dec 14, 2020

Try providing all three timeout parameters to the shell() method and see if that fixes the problem. You can use the same value for all three.

Did that work?

Hi Jeff,

Yes, I just verified that if I set all 3 timeouts in .shell() to 4.0, the timeout does become 4.0 seconds as expected:

adb_shell.exceptions.TcpTimeoutException: Reading from <IP>:<PORT> timed out (4.0 seconds)

Thanks for guiding me to a cleaner solution!

Best, Janus

Read more comments on GitHub >

github_iconTop Results From Across the Web

adb_shell/adb_device.py at master · JeffLIrion ... - GitHub
A Python implementation of ADB with shell and FileSync functionality. ... if time.time() - start > adb_info.read_timeout_s: break. # Timeout.
Read more >
adb_shell.adb_device module
AdbDevice.streaming_shell(). AdbDeviceTcp. AdbDeviceUsb ... Default timeout in seconds for transport packets, or None. Type. float, None. _io_manager ¶.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found