Skip to content

Update adb connect method to disconnect first - #1072

Closed
kavitha-subr wants to merge 1 commit into
google:masterfrom
kavitha-subr:fix-adb-connect
Closed

kavitha-subr wants to merge 1 commit into
google:masterfrom
kavitha-subr:fix-adb-connect

Conversation

@kavitha-subr

Copy link
Copy Markdown

Tests experience device not found / device offline errors when a adb.root() is performed after a reboot. When adbd restarts as root, the host ADB server likely holds a stale connection causing this issue.

This PR updates introduces a "connect" attempt in cases of such errors for TCP connections. The connect method first attempts to disconnect the connection and then connect to ensure any stale connections are flushed.

@kavitha-subr
kavitha-subr marked this pull request as draft October 14, 2026 12:30

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an adb disconnect method and updates connect to flush stale socket states by disconnecting first. It also adds a reconnection mechanism for TCP devices during adb root retries, along with corresponding unit tests. The review feedback highlights a critical issue where calling self._exec_adb_cmd on an AdbProxy instance with a serial set will incorrectly prepend -s <serial> to global commands like connect and disconnect. The reviewer suggests using a serial-less AdbProxy instance for these commands and updating the tests to verify that they are executed without the serial prefix.

Comment on lines +366 to +368
return self._exec_adb_cmd(
'disconnect', address, shell=False, timeout=None, stderr=None
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Calling self._exec_adb_cmd on an AdbProxy instance that has self.serial set will prepend -s <serial> to the adb disconnect command. Since adb disconnect is a global command and does not target a specific device via -s (and will fail if the device is offline/disconnected), it should be executed using a serial-less AdbProxy instance.

Suggested change
return self._exec_adb_cmd(
'disconnect', address, shell=False, timeout=None, stderr=None
)
return AdbProxy()._exec_adb_cmd(
'disconnect', address, shell=False, timeout=None, stderr=None
)

Comment on lines 390 to 392
stdout = self._exec_adb_cmd(
'connect', address, shell=False, timeout=None, stderr=None
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Calling self._exec_adb_cmd on an AdbProxy instance that has self.serial set will prepend -s <serial> to the adb connect command. Since adb connect is a global command and does not target a specific device via -s (and will fail with a 'device not found' error if the device is offline/disconnected), it should be executed using a serial-less AdbProxy instance.

Suggested change
stdout = self._exec_adb_cmd(
'connect', address, shell=False, timeout=None, stderr=None
)
stdout = AdbProxy()._exec_adb_cmd(
'connect', address, shell=False, timeout=None, stderr=None
)

Comment on lines +564 to +572
def test_connect_ignores_disconnect_error(self, mock_run_command):
mock_address = 'localhost:1234'
mock_run_command.side_effect = [
(1, b'', b'error: no such device'),
(0, f'connected to {mock_address}'.encode('utf-8'), b''),
]
out = adb.AdbProxy().connect(mock_address)
self.assertEqual('connected to localhost:1234', out.decode('utf-8'))
self.assertEqual(mock_run_command.call_count, 2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the test to instantiate AdbProxy with a serial and assert that run_command is called without the -s prefix. This ensures that connect and disconnect remain global commands and do not incorrectly target a specific device serial.

  @mock.patch('mobly.utils.run_command')
  def test_connect_ignores_disconnect_error(self, mock_run_command):
    mock_address = 'localhost:1234'
    mock_run_command.side_effect = [
        (1, b'', b'error: no such device'),
        (0, f'connected to {mock_address}'.encode('utf-8'), b''),
    ]
    out = adb.AdbProxy(mock_address).connect(mock_address)
    self.assertEqual('connected to localhost:1234', out.decode('utf-8'))
    self.assertEqual(mock_run_command.call_count, 2)
    mock_run_command.assert_has_calls([
        mock.call(['adb', 'disconnect', mock_address], shell=False, timeout=None),
        mock.call(['adb', 'connect', mock_address], shell=False, timeout=None),
    ])

@kavitha-subr
kavitha-subr marked this pull request as ready for review October 14, 2026 12:48
@xpconanfan

Copy link
Copy Markdown
Collaborator

Thanks for the detailed investigation and for validating this against your lab. We discussed this on mobly-eng and are going to decline this change; closing the PR. Summary of the reasoning:

The diagnosis doesn't match how the host adb server works. The host adb server does not hold a "stale" TCP transport that blocks reconnection. When adbd restarts (reboot or adb root), the kicked TCP transport is handed to ReconnectHandler in transport.cpp, which re-dials at +250 ms and then every 3 s for up to ~60 s before removing the transport. There is nothing in that path for adb disconnect to flush.

The log in b/483347888#comment3 shows a different primary failure. The first adb root was killed by Mobly's 5 s per-attempt timeout; the device offline is only the retry 15 s later. Since Android P the adb root client itself blocks on wait-for-disconnect followed by wait-for-device (12 s budget), and on a TCP transport that reconnect goes through the 3 s ReconnectHandler cadence plus 1 s wait-for-device polling, so 4–7 s is the normal case on TCP. The same log also shows two other boards already parked offline in adb devices while untouched by the test, which points at the lab's TCP endpoint (e.g. a vsock/TCP proxy in front of adbd that accepts connections before the inner adbd is up, Cuttlefish-style) rather than at Mobly.

The A/B result doesn't isolate the variable. The patched code only runs after the first attempt has already failed and adds a 10 s sleep + disconnect + connect before retrying; any change that lets the device finish restarting before the retry would show the same improvement. A control run with only the root timeout raised (no disconnect/connect) would be needed to attribute the gain to the disconnect.

The change itself is risky for other users. connect() unconditionally running adb disconnect first tears down the transport and every forward and in-flight socket on it (snippet clients, logcat streams, adb forwards) for any caller that uses connect() as "ensure connected" on a healthy device. Inferring TCP-ness from ':' in serial inside AdbProxy.root() is also the wrong layer for transport-specific recovery.

Recommendation: investigate on the SDV side what terminates ip:5555 (proxy vs adbd) and capture ADB_TRACE=transport on the host adb server across a reboot/root cycle. If a TCP-aware reconnect is needed for your devices, it belongs in your device controller or AndroidDevice subclass rather than in AdbProxy.connect()/root().

@xpconanfan

Copy link
Copy Markdown
Collaborator

Closing per the discussion above and on mobly-eng.

@xpconanfan xpconanfan closed this Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants