-
Notifications
You must be signed in to change notification settings - Fork 32
Allow use of the native Android Debug Bridge #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DanTheMan827
wants to merge
18
commits into
Lauriethefish:main
Choose a base branch
from
DanTheMan827:adb-websocket
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
f201be6
Allow use of the native Android Debug Bridge
DanTheMan827 1213f02
Don't scan for devices if a device was chosen
DanTheMan827 7de920d
Update credits
DanTheMan827 86cf8dd
Use specified bridge, and ping every 5 seconds once a device is conne…
DanTheMan827 08b31aa
Improve device connection handling
DanTheMan827 74d06ce
Add OpenLogsButton to device selection and improve CSS layout for con…
DanTheMan827 02040ba
Skip oculus check if bridge detected
DanTheMan827 c73a9eb
Refactor logging to include raw data in console log messages for impr…
DanTheMan827 0dc63e3
Implement disconnect tracking for old adb server versions
DanTheMan827 cd5a664
Refactor sendRequest to work with the chunks as they are sent from th…
DanTheMan827 8cd9e63
Re-factor bridge data into a class
DanTheMan827 9e0b7ea
Auto-connect if bridge and only one device is found
DanTheMan827 ab80467
Update disconnect detection logic
DanTheMan827 2c0ccd7
chore: update dependencies and improve error handling
DanTheMan827 e9280e4
Refactor code to split App component
DanTheMan827 c2493f0
Refactor device and connection management
DanTheMan827 0e8861b
Abstract bridge interface to allow for different bridges
DanTheMan827 2432f7b
Implement __mbfBridge ADB connector
DanTheMan827 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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,254 @@ | ||
| import type { AdbIncomingSocketHandler, AdbServerClient } from "@yume-chan/adb"; | ||
| import { MaybeConsumable, ReadableStream, ReadableWritablePair } from "@yume-chan/stream-extra"; | ||
| import { PromiseResolver } from "@yume-chan/async"; | ||
| import { IBridge } from "./BridgeFactory"; | ||
| import { createElement, ReactNode } from "react"; | ||
| import { PagePinger } from "./PagePinger"; | ||
|
|
||
| /** WebSocket bridge endpoints. */ | ||
| class BridgeData { | ||
| readonly bridge: string; | ||
| readonly websocketAddress: string; | ||
| readonly pingAddress: string; | ||
| readonly isLocal: boolean; | ||
|
|
||
| constructor(bridge: string) { | ||
| // Tsc doesn't know about URL.parse, so we need to cast it to any. | ||
| const parseUrl = (URL as any).parse as (url: string | URL, base?: string | URL) => URL | null; | ||
|
|
||
| const parsed = (/^https?:\/\//i).exec(bridge) ? parseUrl(bridge)! : parseUrl(`http://${bridge}`)!; | ||
| this.bridge = parsed ? parsed.host : "127.0.0.1:25037"; | ||
| this.websocketAddress = `ws${parsed.protocol.toLowerCase() == "https:" ? "s" : ""}://${this.bridge}/bridge`; | ||
| this.pingAddress = `${parsed.protocol}//${this.bridge}/bridge/ping`; | ||
| this.isLocal = parsed != null && ["127.0.0.1", "localhost"].includes(parsed.hostname.toLowerCase()); | ||
| } | ||
| } | ||
|
|
||
| export const bridgeData = (() => { | ||
| const params = new URLSearchParams(location.search); | ||
|
|
||
| if (params.has("bridge") && params.get("bridge") === "") { | ||
| return new BridgeData(location.href); | ||
| } | ||
|
|
||
| if (params.has("bridge")) { | ||
| return new BridgeData(params.get("bridge")!); | ||
| } | ||
| })(); | ||
|
|
||
| /** | ||
| * Checks if the bridge is running by sending a GET request to the ping endpoint. | ||
| * | ||
| * @returns A promise that resolves to true if the bridge is running, false otherwise. | ||
| */ | ||
| async function checkForBridge(address: string, signal?: AbortSignal): Promise<boolean> { | ||
| try { | ||
| const response = await fetch(address, { signal }); | ||
| if (response.ok) { | ||
| // Read the response body | ||
| var text = await response.text(); | ||
| return text === "OK"; | ||
| } | ||
| } catch { | ||
| return false; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Interface representing a socket with readable and writable streams, | ||
| * along with connection details. | ||
| */ | ||
| interface Socket extends ReadableWritablePair<Uint8Array, Uint8Array> { | ||
| extensions: string; | ||
| protocol: string; | ||
| } | ||
|
|
||
| /** | ||
| * Wraps a WebSocket connection into readable and writable streams. | ||
| */ | ||
| class WebSocketConnection { | ||
| public url: string; | ||
| private socket: WebSocket; | ||
| private openDeferred: PromiseResolver<Socket>; | ||
| private closeDeferred: PromiseResolver<{ closeCode: number; reason: string }>; | ||
|
|
||
| /** | ||
| * Initializes a new WebSocket connection. | ||
| * | ||
| * @param url - The WebSocket URL. | ||
| * @param options - Optional protocols. | ||
| */ | ||
| constructor(url: string, options?: { protocols?: string | string[] }) { | ||
| this.url = url; | ||
| this.socket = new WebSocket(url, options?.protocols); | ||
| this.socket.binaryType = "arraybuffer"; | ||
| this.openDeferred = new PromiseResolver<Socket>(); | ||
| this.closeDeferred = new PromiseResolver<{ closeCode: number; reason: string }>(); | ||
|
|
||
| let hasOpened = false; | ||
|
|
||
| // When the socket opens, resolve the openDeferred with connection details. | ||
| this.socket.onopen = () => { | ||
| hasOpened = true; | ||
| this.openDeferred.resolve({ | ||
| extensions: this.socket.extensions, | ||
| protocol: this.socket.protocol, | ||
| readable: new ReadableStream<Uint8Array>({ | ||
| start: (controller) => { | ||
| // Forward incoming messages to the stream controller. | ||
| this.socket.onmessage = (event: MessageEvent) => { | ||
| if (typeof event.data === "string") { | ||
| controller.enqueue(new TextEncoder().encode(event.data)); | ||
| } else { | ||
| controller.enqueue(new Uint8Array(event.data)); | ||
| } | ||
| }; | ||
| // Report errors to the stream controller. | ||
| this.socket.onerror = (ev) => { | ||
| controller.error(new Error("WebSocket error")); | ||
| console.error("WebSocket error", ev); | ||
| }; | ||
| // Close the stream when the socket closes. | ||
| this.socket.onclose = (event) => { | ||
| try { | ||
| controller.close(); | ||
| } catch (error) { | ||
| // Ignore errors during stream close, but logs them. | ||
| console.error(error); | ||
| } | ||
| this.closeDeferred.resolve({ | ||
| closeCode: event.code, | ||
| reason: event.reason, | ||
| }); | ||
| }; | ||
| }, | ||
| }), | ||
| writable: new MaybeConsumable.WritableStream<Uint8Array>({ | ||
| write: async (chunk: Uint8Array) => { | ||
| this.socket.send(chunk); | ||
| }, | ||
| }), | ||
| }); | ||
| }; | ||
|
|
||
| // If an error occurs before the socket opens, reject the openDeferred. | ||
| this.socket.onerror = (ev) => { | ||
| if (!hasOpened) { | ||
| console.error("WebSocket conenction error", ev); | ||
| this.openDeferred.reject(new Error("WebSocket connection error")); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| /** Returns a promise that resolves when the connection is open. */ | ||
| public getOpened(): Promise<Socket> { | ||
| return this.openDeferred.promise; | ||
| } | ||
|
|
||
| /** Returns a promise that resolves when the connection is closed. */ | ||
| public getClosed(): Promise<{ closeCode: number; reason: string }> { | ||
| return this.closeDeferred.promise; | ||
| } | ||
|
|
||
| /** Closes the WebSocket connection. */ | ||
| public close(closeInfo?: { closeCode?: number; reason?: string }): void { | ||
| this.socket.close(closeInfo?.closeCode, closeInfo?.reason); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A `AdbServerClient.ServerConnector` implementation using a WebSocket connection. | ||
| */ | ||
| export class AdbServerWebSocketConnector implements AdbServerClient.ServerConnector { | ||
| constructor(private websocketAddress: string) { } | ||
|
|
||
| /** | ||
| * Connects to the ADB server bridge using a WebSocket connection. | ||
| * | ||
| * @returns A promise that resolves to the ADB server connection. | ||
| */ | ||
| async connect(): Promise<AdbServerClient.ServerConnection> { | ||
| const connection = new WebSocketConnection(this.websocketAddress); | ||
| let timer: ReturnType<typeof setTimeout> | undefined = undefined; | ||
|
|
||
| // Create a timeout promise that rejects after 5000ms. | ||
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| timer = setTimeout(() => { | ||
| console.error("WebSocket connection timed out"); | ||
| reject(new Error("WebSocket connection timed out")); | ||
| }, 5000); | ||
| }); | ||
|
|
||
| // Wait for the connection to open or for the timeout. | ||
| const connectionResult = await Promise.race([ | ||
| connection.getOpened(), | ||
| timeoutPromise, | ||
| ]); | ||
| clearTimeout(timer); | ||
|
|
||
| // Obtain a writer from the writable stream. | ||
| const writer = connectionResult.writable.getWriter(); | ||
| return { | ||
| readable: connectionResult.readable, | ||
| writable: new MaybeConsumable.WritableStream<Uint8Array>({ | ||
| write: (chunk) => writer.write(chunk), | ||
| close: () => writer.close(), | ||
| }), | ||
| close: () => connection.close(), | ||
| closed: connection.getClosed().then(() => undefined), | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Not implemented: Adds a reverse tunnel. | ||
| * | ||
| * @throws Method not implemented. | ||
| */ | ||
| async addReverseTunnel( | ||
| handler: AdbIncomingSocketHandler, | ||
| address?: string | ||
| ): Promise<string> { | ||
| throw new Error("Method not implemented."); | ||
| } | ||
|
|
||
| /** | ||
| * Not implemented: Removes a reverse tunnel. | ||
| * | ||
| * @throws Method not implemented. | ||
| */ | ||
| removeReverseTunnel(address: string): void { | ||
| throw new Error("Method not implemented."); | ||
| } | ||
|
|
||
| /** | ||
| * Not implemented: Clears all reverse tunnels. | ||
| * | ||
| * @throws Method not implemented. | ||
| */ | ||
| clearReverseTunnels(): void { | ||
| throw new Error("Method not implemented."); | ||
| } | ||
| } | ||
|
|
||
| export class WebSocketBridge implements IBridge { | ||
| #bridgeData: BridgeData; | ||
|
|
||
| constructor(bridgeData: BridgeData) { | ||
| this.#bridgeData = bridgeData; | ||
| } | ||
|
|
||
| BridgeSupplement = (function(this: WebSocketBridge): ReactNode { | ||
| const url = this.#bridgeData.pingAddress; | ||
| return createElement(PagePinger, { url, interval: 5000 }); | ||
| }).bind(this); | ||
|
|
||
| async isAvailable(abortController?: AbortController): Promise<boolean> { | ||
| return await checkForBridge(this.#bridgeData.pingAddress, abortController?.signal); | ||
| } | ||
|
|
||
| async getConnector() { | ||
| return new AdbServerWebSocketConnector(this.#bridgeData.websocketAddress); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.