Socket
This page describes how to use an ADB socket. See this page for how to implement it in a custom transport.
ADB is a multiplexing protocol, each logical stream is called a socket. A socket is a duplex stream that can be read from and written to.
Each socket also has a service name, which is similar to the hostname and port number in a TCP socket, or the URL in an HTTP request
In Tango, a socket is represented by an AdbSocket
object, while each transport may extend it with additional properties.
interface AdbSocket {
readonly readable: ReadableStream<Uint8Array>;
readonly writable: WritableStream<MaybeConsumable<Uint8Array>>;
readonly service: string;
readonly closed: Promise<void>;
close(): Promise<void>;
}
Create socket
The Adb
class provides a createSocket
method to create a socket. It calls the connect
method on the transport to establish a connection.
declare class Adb {
createSocket(address: string): Promise<AdbSocket>;
}
Most commands create a socket by calling this method internally, so you don't need to call it directly in most cases.
Read from socket
The readable
property on the socket is a ReadableStream
that emits Uint8Array
chunks. It contains the data received from the device. Since ADB socket is a stream protocol, chunks might be split or combined arbitrarily.
One common way to read from the stream is to pipe it to a WritableStream
.
import { WritableStream } from "@yume-chan/stream-extra";
const socket = await adb.createSocket("tcp:1234");
await socket.readable.pipeTo(
new WritableStream({
write(chunk) {
console.log(chunk);
},
})
);
pipeTo
returns a promise that resolves when the stream is closed. If the stream is closed due to an error, the promise will be rejected.
You can also use the getReader
method on the stream to read from it manually.
const reader = socket.readable.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
console.log(value);
}
} finally {
reader.releaseLock();
}
It gives more control over the reading process, but it's more verbose.
Backpressure
The readable
stream implements backpressure, meaning that not reading from the stream will block the other end from producing more data. Because ADB is a multiplexing protocol, blocking one stream will also block all other streams.
If the remaining data is not needed, stream.cancel()
(or stream.getReader().cancel()
if using a reader) can be called to discard them.
Write to socket
The writable
property on the socket is a WritableStream
that accepts both Uint8Array
and Consumable<Uint8Array>
chunks.
See the Consumable pattern page for more information about Consumable
.
To write to the stream, usually you will get a writer from it and call the write
method.
const writer = socket.writable.getWriter();
await writer.write(new Uint8Array([0x01, 0x02, 0x03]));
await writer.write(new Uint8Array([0x04, 0x05, 0x06]));
writer.releaseLock();
There can only be one writer
at a time, so it must be shared between multiple producers, or call writer.releaseLock()
to release the lock so another writer can be created.
If there is already a ReadableStream
, it can be piped into the WritableStream
:
import { ReadableStream } from "@yume-chan/stream-extra";
await new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([0x01, 0x02, 0x03]));
controller.enqueue(new Uint8Array([0x04, 0x05, 0x06]));
controller.close();
},
}).pipeTo(socket.writable);
Piping a ReadableStream
into a WritableStream
will also lock the WritableStream
, so getWriter
cannot be called on it.
The close
method on writable
or writable.getWriter()
only closes the writable stream, not the whole socket, so it can be omitted. Use the close
method on the socket to close the whole socket.
Close socket
Call the close
method on the socket to close it.
If there are still unread data in the readable
stream, the data can still and must be read. Not reading them will cause all sockets to be stalled. Also, the closed
promise will not be resolved until all data has been read.
The closed
promise will be resolved after the device has acknowledged the close.
Not closing a socket will cause a small memory leak, and depends on the other end of the socket, it may not work correctly.
Forward tunnel
Forward tunnel allows you to connect to a socket address on the device from Tango.
While the service name usually points to a service on the ADB daemon, it can also be a socket address on the device.
Device Address
The device address is in the format of <type>:<address>
. ADB Daemon can forward the connection to various types of addresses:
tcp:<port>
: TCPlocalhost:<port>
on devicelocal:<path>
: Unix local domain socket on devicelocalabstract:<name>
: Unix abstract namespace socket on devicelocalfilesystem:<path>
: Unix filesystem namespace socket on devicevsock:<CID>:<port>
: vsock on the given CID and port
Example
import { WritableStream } from "@yume-chan/stream-extra";
const socket = await adb.createSocket("tcp:1234");
await socket.readable.pipeTo(
new WritableStream({
write(chunk) {
console.log(chunk);
},
})
);
await socket.close();
It doesn't work in the same way, but you can use the following command to register a forward tunnel and connect to it:
adb forward tcp:1234 tcp:1234
nc 127.0.0.1 1234
There is no adb forward
command
In Google ADB, forward tunnels must be associated with a local socket address, and the client needs connect to that address to have the connection forwarded to the device.
In Tango, only the last step is needed. createSocket
method uses low-level ADB protocol to create forward tunnel directly.
Daemon Transport
adb forward
series of commands are implemented by Google ADB Server. When using Daemon Transport, there is no Google ADB Server, so no support for those commands.
If your target runtime environment supports listening on a TCP port, you can simulate the behavior of adb forward
by listening on a TCP port, and call connectSocket
for each incoming connection.
Server Transport
It's possible to add adb forward
series of commands to AdbServerClient
class, it just has not been done. Contributions are welcomed!