Skip to content

Commit

Permalink
feat(adb): server client mdns services
Browse files Browse the repository at this point in the history
  • Loading branch information
yume-chan committed May 23, 2024
1 parent fdf0a86 commit e9c82f9
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 75 deletions.
19 changes: 9 additions & 10 deletions libraries/adb-server-node-tcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import type { AddressInfo, SocketConnectOpts } from "net";
import { Server, Socket } from "net";

import type {
AdbIncomingSocketHandler,
AdbServerConnection,
AdbServerConnectionOptions,
AdbServerConnector,
} from "@yume-chan/adb";
import type { AdbIncomingSocketHandler, AdbServerClient } from "@yume-chan/adb";
import {
MaybeConsumable,
PushReadableStream,
Expand All @@ -15,7 +10,9 @@ import {
} from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";

function nodeSocketToConnection(socket: Socket): AdbServerConnection {
function nodeSocketToConnection(
socket: Socket,
): AdbServerClient.ServerConnection {
socket.setNoDelay(true);

const closed = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -64,7 +61,9 @@ function nodeSocketToConnection(socket: Socket): AdbServerConnection {
};
}

export class AdbServerNodeTcpConnector implements AdbServerConnector {
export class AdbServerNodeTcpConnector
implements AdbServerClient.ServerConnector
{
readonly spec: SocketConnectOpts;

readonly #listeners = new Map<string, Server>();
Expand All @@ -74,8 +73,8 @@ export class AdbServerNodeTcpConnector implements AdbServerConnector {
}

async connect(
{ unref }: AdbServerConnectionOptions = { unref: false },
): Promise<AdbServerConnection> {
{ unref }: AdbServerClient.ServerConnectionOptions = { unref: false },
): Promise<AdbServerClient.ServerConnection> {
const socket = new Socket();
if (unref) {
socket.unref();
Expand Down
163 changes: 98 additions & 65 deletions libraries/adb/src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,52 +27,6 @@ import { NOOP, hexToNumber, write4HexDigits } from "../utils/index.js";

import { AdbServerTransport } from "./transport.js";

export interface AdbServerConnectionOptions {
unref?: boolean | undefined;
signal?: AbortSignal | undefined;
}

export interface AdbServerConnection
extends ReadableWritablePair<Uint8Array, Uint8Array>,
Closeable {
get closed(): Promise<void>;
}

export interface AdbServerConnector {
connect(
options?: AdbServerConnectionOptions,
): ValueOrPromise<AdbServerConnection>;

addReverseTunnel(
handler: AdbIncomingSocketHandler,
address?: string,
): ValueOrPromise<string>;

removeReverseTunnel(address: string): ValueOrPromise<void>;

clearReverseTunnels(): ValueOrPromise<void>;
}

export interface AdbServerSocket extends AdbSocket {
transportId: bigint;
}

export type AdbServerDeviceSelector =
| { transportId: bigint }
| { serial: string }
| { usb: true }
| { tcp: true }
| undefined;

export interface AdbServerDevice {
serial: string;
authenticating: boolean;
product?: string | undefined;
model?: string | undefined;
device?: string | undefined;
transportId: bigint;
}

function sequenceEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) {
return false;
Expand All @@ -91,11 +45,11 @@ const OKAY = encodeUtf8("OKAY");
const FAIL = encodeUtf8("FAIL");

class AdbServerStream {
#connection: AdbServerConnection;
#connection: AdbServerClient.ServerConnection;
#buffered: BufferedReadableStream;
#writer: WritableStreamDefaultWriter<Uint8Array>;

constructor(connection: AdbServerConnection) {
constructor(connection: AdbServerClient.ServerConnection) {
this.#connection = connection;
this.#buffered = new BufferedReadableStream(connection.readable);
this.#writer = connection.writable.getWriter();
Expand Down Expand Up @@ -191,15 +145,15 @@ class AdbServerStream {
export class AdbServerClient {
static readonly VERSION = 41;

readonly connection: AdbServerConnector;
readonly connection: AdbServerClient.ServerConnector;

constructor(connection: AdbServerConnector) {
constructor(connection: AdbServerClient.ServerConnector) {
this.connection = connection;
}

async createConnection(
request: string,
options?: AdbServerConnectionOptions,
options?: AdbServerClient.ServerConnectionOptions,
): Promise<AdbServerStream> {
const connection = await this.connection.connect(options);
const stream = new AdbServerStream(connection);
Expand Down Expand Up @@ -326,8 +280,8 @@ export class AdbServerClient {
}
}

parseDeviceList(value: string): AdbServerDevice[] {
const devices: AdbServerDevice[] = [];
parseDeviceList(value: string): AdbServerClient.Device[] {
const devices: AdbServerClient.Device[] = [];
for (const line of value.split("\n")) {
if (!line) {
continue;
Expand Down Expand Up @@ -379,7 +333,7 @@ export class AdbServerClient {
/**
* `adb devices -l`
*/
async getDevices(): Promise<AdbServerDevice[]> {
async getDevices(): Promise<AdbServerClient.Device[]> {
const connection = await this.createConnection("host:devices-l");
try {
const response = await connection.readString();
Expand All @@ -398,7 +352,7 @@ export class AdbServerClient {
*/
async *trackDevices(
signal?: AbortSignal,
): AsyncGenerator<AdbServerDevice[], void, void> {
): AsyncGenerator<AdbServerClient.Device[], void, void> {
const connection = await this.createConnection("host:track-devices-l");
try {
while (true) {
Expand All @@ -418,7 +372,40 @@ export class AdbServerClient {
}
}

formatDeviceService(device: AdbServerDeviceSelector, command: string) {
async mDnsCheck() {
const connection = await this.createConnection("host:mdns:check");
try {
const response = await connection.readString();
return !response.startsWith("ERROR:");
} finally {
await connection.dispose();
}
}

async mDnsGetServices() {
const connection = await this.createConnection("host:mdns:services");
try {
const response = await connection.readString();
return response
.split("\n")
.filter(Boolean)
.map((line) => {
const parts = line.split("\t");
return {
name: parts[0]!,
service: parts[1]!,
address: parts[2]!,
};
});
} finally {
await connection.dispose();
}
}

formatDeviceService(
device: AdbServerClient.DeviceSelector,
command: string,
) {
if (!device) {
return `host:${command}`;
}
Expand All @@ -440,7 +427,7 @@ export class AdbServerClient {
/**
* `adb -s <device> reconnect` or `adb reconnect offline`
*/
async reconnectDevice(device: AdbServerDeviceSelector | "offline") {
async reconnectDevice(device: AdbServerClient.DeviceSelector | "offline") {
const connection = await this.createConnection(
device === "offline"
? "host:reconnect-offline"
Expand All @@ -461,7 +448,7 @@ export class AdbServerClient {
* @returns The transport ID of the selected device, and the features supported by the device.
*/
async getDeviceFeatures(
device: AdbServerDeviceSelector,
device: AdbServerClient.DeviceSelector,
): Promise<{ transportId: bigint; features: AdbFeature[] }> {
// On paper, `host:features` is a host service (device features are cached in host),
// so it shouldn't use `createDeviceConnection`,
Expand All @@ -483,7 +470,7 @@ export class AdbServerClient {
device,
"host:features",
);
// Luckily `AdbServerSocket` is compatible with `AdbServerConnection`
// Luckily `AdbServerClient.Socket` is compatible with `AdbServerClient.ServerConnection`
const stream = new AdbServerStream(connection);
try {
const featuresString = await stream.readString();
Expand All @@ -498,12 +485,12 @@ export class AdbServerClient {
* Creates a connection that will forward the service to device.
* @param device The device selector
* @param service The service to forward
* @returns An `AdbServerSocket` that can be used to communicate with the service
* @returns An `AdbServerClient.Socket` that can be used to communicate with the service
*/
async createDeviceConnection(
device: AdbServerDeviceSelector,
device: AdbServerClient.DeviceSelector,
service: string,
): Promise<AdbServerSocket> {
): Promise<AdbServerClient.Socket> {
await this.validateVersion();

let switchService: string;
Expand Down Expand Up @@ -573,9 +560,9 @@ export class AdbServerClient {
* @returns A promise that resolves when the condition is met.
*/
async waitFor(
device: AdbServerDeviceSelector,
device: AdbServerClient.DeviceSelector,
state: "device" | "disconnect",
options?: AdbServerConnectionOptions,
options?: AdbServerClient.ServerConnectionOptions,
): Promise<void> {
let type: string;
if (!device) {
Expand Down Expand Up @@ -608,7 +595,7 @@ export class AdbServerClient {
}

async createTransport(
device: AdbServerDeviceSelector,
device: AdbServerClient.DeviceSelector,
): Promise<AdbServerTransport> {
const { transportId, features } = await this.getDeviceFeatures(device);

Expand Down Expand Up @@ -665,6 +652,52 @@ export async function raceSignal<T>(
}

export namespace AdbServerClient {
export interface ServerConnectionOptions {
unref?: boolean | undefined;
signal?: AbortSignal | undefined;
}

export interface ServerConnection
extends ReadableWritablePair<Uint8Array, Uint8Array>,
Closeable {
get closed(): Promise<void>;
}

export interface ServerConnector {
connect(
options?: ServerConnectionOptions,
): ValueOrPromise<ServerConnection>;

addReverseTunnel(
handler: AdbIncomingSocketHandler,
address?: string,
): ValueOrPromise<string>;

removeReverseTunnel(address: string): ValueOrPromise<void>;

clearReverseTunnels(): ValueOrPromise<void>;
}

export interface Socket extends AdbSocket {
transportId: bigint;
}

export type DeviceSelector =
| { transportId: bigint }
| { serial: string }
| { usb: true }
| { tcp: true }
| undefined;

export interface Device {
serial: string;
authenticating: boolean;
product?: string | undefined;
model?: string | undefined;
device?: string | undefined;
transportId: bigint;
}

export class NetworkError extends Error {
constructor(message: string) {
super(message);
Expand Down

0 comments on commit e9c82f9

Please sign in to comment.