-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
606 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
position: 3 | ||
label: 'Example: Server-Client Transport' | ||
collapsed: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.