Skip to content

Commit

Permalink
add custom transport example
Browse files Browse the repository at this point in the history
  • Loading branch information
yume-chan committed Jan 12, 2025
1 parent da736eb commit 11edf1f
Show file tree
Hide file tree
Showing 8 changed files with 606 additions and 1 deletion.
3 changes: 3 additions & 0 deletions docs/tango/custom-transport/server-client/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
position: 3
label: 'Example: Server-Client Transport'
collapsed: false
62 changes: 62 additions & 0 deletions docs/tango/custom-transport/server-client/adb.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
sidebar_position: 4
---

# Run ADB commands

## Server

In this example, because the server didn't run any ADB commands, it only creates `AdbTransport`s for the clients to use.

If needed, you can also run ADB commands in the server:

```ts transpile title="server/index.ts"
import { Adb, AdbDaemonTransport } from "@yume-chan/adb";

const devices = new Map<string, Adb>();

// ...

const connection = await device.connect();
const transport = await AdbDaemonTransport.authenticate({
serial,
connection,
credentialStore: CredentialStore,
});

// highlight-start
const adb = new Adb(transport);
devices.set(serial, adb);
// highlight-end

// ...
```

## Client

The client created an `Adb` instance using the custom `AdbTransport`, ran `logcat` command, and printed the output to a `<div>` element.

```ts transpile title="client/device.ts"
import { AdbBanner, Adb } from "@yume-chan/adb";

const container = document.getElementById("app")!;

// ...

const data = await response.json();
const transport = new WebSocketTransport(
serial,
data.maxPayloadSize,
new AdbBanner(data.product, data.model, data.device, data.features)
);

// highlight-start
const adb = new Adb(transport);
const process = await adb.subprocess.spawn("logcat");
for await (const chunk of process.stdout.pipeThrough(new TextDecoderStream())) {
container.textContent += chunk;
}
// highlight-end
```

You can run [other commands](../../../api/index.mdx#adb-from-yume-chanadb) using the `adb` instance.
107 changes: 107 additions & 0 deletions docs/tango/custom-transport/server-client/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Server-Client Transport

This guide shows how to create a custom transport with a server-client architecture.

The complete project can be found at https://github.com/tango-adb/example-server-client.

```mermaid
flowchart LR
Device <-- USB --> Server
Server <-- HTTP/WebSocket --> Client
```

## Server

The server is a Node.js program that:

- Keeps tracking the list of connected devices
- Sends device list changes to connected clients through WebSocket
- Establishes ADB connection to devices when clients request via HTTP
- Forwards ADB sockets between clients and devices through WebSocket

It uses [Daemon Transport over USB connections](../../daemon/usb/index.mdx). Other transports and connection methods can also be used, by referring to the corresponding documentation pages.

A Node.js HTTP `Server` is created to handle HTTP requests, and a `WebSocketServer` from [`ws`](https://npmjs.com/package/ws) is created, sharing the HTTP `Server`, to handle WebSocket requests:

```ts transpile
import { createServer } from "node:http";
import { WebSocketServer } from "ws";

const httpServer = createServer(async (request, response) => {
// TODO: Handle HTTP requests
response.writeHead(404, { "Access-Control-Allow-Origin": "*" }).end();
});

const wsServer = new WebSocketServer({
server: httpServer,
});

wsServer.addListener("connection", async (client, request) => {
const url = new URL(request.url!, "http://localhost");
const segments = url.pathname.substring(1).split("/");

switch (segments[0]) {
// TODO: handle WebSocket request
default:
client.close();
}
});

httpServer.listen(
{
host: "0.0.0.0",
port: 8080,
},
() => {
console.log("Server listening on http://localhost:8080");
}
);
```

## Client

The client is a Web app that:

- Connects to the server to get device list
- Requests the server to establish ADB connection when a device is selected
- Runs ADB commands by sending them to the server through WebSocket

The client doesn't use any UI frameworks. You can use any JavaScript library to implement your own UI.

It has two HTML pages:

* `index.html`: Displays the device list and links to the device page
* `device.html`: Displays the output from `logcat` command to demonstrate the use of ADB commands

## Build and run

This project uses PNPM workspaces. Make sure you have [pnpm](https://pnpm.io) installed.

1. Clone the project

```sh
git clone https://github.com/tango-adb/example-server-client.git
cd example-server-client
```

2. Install dependencies

```sh
pnpm install
```

The server uses [tsx](https://www.npmjs.com/package/tsx) to run TypeScript code in Node.js directly, and the client uses [@babel/standalone](https://www.npmjs.com/package/@babel/standalone) to run TypeScript in Web browsers, so no compile step is needed.

To run the server:

```sh
cd server
npm start
```

To run the client:

```sh
cd client
npm start
```
132 changes: 132 additions & 0 deletions docs/tango/custom-transport/server-client/socket.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
sidebar_position: 3
---

# Create socket

The last step is to create `AdbSocket`s. This example uses one WebSocket connection for each `AdbSocket`.

## Server

Creating `AdbSocket` requires two parameters: the device's `serial` and the socket's `service` (address).

Again, it creates a real `AdbSocket` using the existing `AdbTransport`, then forwards packets between the client WebSocket and the `AdbSocket`.

```ts transpile title="server/index.ts"
wsServer.addListener("connection", async (client, request) => {
const url = new URL(request.url!, "http://localhost");
const segments = url.pathname.substring(1).split("/");

switch (segments[0]) {
// ...
case "device":
{
const [, serial, service] = segments;
if (!serial || !service) {
client.close();
break;
}

const transport = devices.get(serial);
if (!transport) {
client.close();
break;
}

// highlight-start
try {
const socket = await transport.connect(service);

client.binaryType = "arraybuffer";

socket.readable.pipeTo(
new WritableStream({
async write(chunk) {
while (client.bufferedAmount >= 1 * 1024 * 1024) {
await delay(10);
}
client.send(chunk);
},
})
);

const writer = socket.writable.getWriter();
client.addListener("message", async (message) => {
client.pause();
await writer.write(new Uint8Array(message as ArrayBuffer));
client.resume();
});

client.addListener("close", () => {
socket.close();
});
} catch {
client.close();
break;
}
// highlight-end
}
break;
default:
client.close();
}
});
```

## Client

Its custom `AdbTransport` can create a WebSocket connection with `serial` and `service`, then convert the connection back to `AdbSocket`.

This example uses the new [`WebSocketStream`](../../web-stream/websocket.mdx) API.

```ts transpile title="client/transport.ts"
class WebSocketTransport implements AdbTransport {
// ...

#sockets = new Set<WebSocketStream>();

// ...

// highlight-start
async connect(service: string): Promise<AdbSocket> {
const socket = new WebSocketStream(
`ws://localhost:8080/device/${this.serial}/${service}`
);
const open = await socket.opened;
this.#sockets.add(socket);

const writer = open.writable.getWriter();
return {
service,
readable: open.readable.pipeThrough(
new TransformStream<Uint8Array, Uint8Array>({
transform(chunk, controller) {
// Chrome's implementation still gives `ArrayBuffer`
controller.enqueue(new Uint8Array(chunk));
},
})
) as ReadableStream<Uint8Array>,
writable: new MaybeConsumable.WritableStream({
async write(chunk) {
await writer.write(chunk);
},
}),
close() {
socket.close();
},
closed: socket.closed as never as Promise<void>,
};
}

close() {
for (const socket of this.#sockets) {
socket.close();
}
this.#sockets.clear();
this.#disconnected.resolve();
}
// highlight-end
}
```

Note: this client doesn't implement [reverse tunnel](../../../api/adb/reverse/index.mdx). Reverse tunnel requires the server to notify the client about new connections.
Loading

0 comments on commit 11edf1f

Please sign in to comment.