mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 02:39:26 +02:00
feat(adb): group sub commands to separate objects
This commit is contained in:
parent
7836343829
commit
ba2be7172d
19 changed files with 453 additions and 167 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -10,6 +10,7 @@
|
|||
"addrs",
|
||||
"fluentui",
|
||||
"getprop",
|
||||
"killforward",
|
||||
"lapo",
|
||||
"lstat",
|
||||
"mitm",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { AdbBackend, decodeBase64, encodeBase64 } from '@yume-chan/adb';
|
||||
import { EventEmitter } from '@yume-chan/event';
|
||||
|
||||
export * from './watcher';
|
||||
|
||||
export const WebUsbDeviceFilter: USBDeviceFilter = {
|
||||
classCode: 0xFF,
|
||||
subclassCode: 0x42,
|
||||
|
@ -21,58 +23,15 @@ export function decodeUtf8(buffer: ArrayBuffer): string {
|
|||
}
|
||||
|
||||
export default class AdbWebBackend implements AdbBackend {
|
||||
public static async fromDevice(device: USBDevice): Promise<AdbWebBackend> {
|
||||
await device.open();
|
||||
|
||||
for (const configuration of device.configurations) {
|
||||
for (const interface_ of configuration.interfaces) {
|
||||
for (const alternate of interface_.alternates) {
|
||||
if (alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode &&
|
||||
alternate.interfaceClass === WebUsbDeviceFilter.classCode &&
|
||||
alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode) {
|
||||
if (device.configuration?.configurationValue !== configuration.configurationValue) {
|
||||
await device.selectConfiguration(configuration.configurationValue);
|
||||
}
|
||||
|
||||
if (!interface_.claimed) {
|
||||
await device.claimInterface(interface_.interfaceNumber);
|
||||
}
|
||||
|
||||
if (interface_.alternate.alternateSetting !== alternate.alternateSetting) {
|
||||
await device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
|
||||
}
|
||||
|
||||
let inEndpointNumber: number | undefined;
|
||||
let outEndpointNumber: number | undefined;
|
||||
|
||||
for (const endpoint of alternate.endpoints) {
|
||||
switch (endpoint.direction) {
|
||||
case 'in':
|
||||
inEndpointNumber = endpoint.endpointNumber;
|
||||
if (outEndpointNumber !== undefined) {
|
||||
return new AdbWebBackend(device, inEndpointNumber, outEndpointNumber);
|
||||
}
|
||||
break;
|
||||
case 'out':
|
||||
outEndpointNumber = endpoint.endpointNumber;
|
||||
if (inEndpointNumber !== undefined) {
|
||||
return new AdbWebBackend(device, inEndpointNumber, outEndpointNumber);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unknown error');
|
||||
public static async getDevices(): Promise<AdbWebBackend[]> {
|
||||
const devices = await window.navigator.usb.getDevices();
|
||||
return devices.map(device => new AdbWebBackend(device));
|
||||
}
|
||||
|
||||
public static async pickDevice(): Promise<AdbWebBackend | undefined> {
|
||||
public static async requestDevice(): Promise<AdbWebBackend | undefined> {
|
||||
try {
|
||||
const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] });
|
||||
return AdbWebBackend.fromDevice(device);
|
||||
return new AdbWebBackend(device);
|
||||
} catch (e) {
|
||||
switch (e.name) {
|
||||
case 'NotFoundError':
|
||||
|
@ -85,18 +44,72 @@ export default class AdbWebBackend implements AdbBackend {
|
|||
|
||||
private _device: USBDevice;
|
||||
|
||||
public get name() { return this._device.productName; }
|
||||
public get serial(): string { return this._device.serialNumber!; }
|
||||
|
||||
private readonly onDisconnectedEvent = new EventEmitter<void>();
|
||||
public readonly onDisconnected = this.onDisconnectedEvent.event;
|
||||
public get name(): string { return this._device.productName!; }
|
||||
|
||||
private readonly disconnectEvent = new EventEmitter<void>();
|
||||
public readonly onDisconnected = this.disconnectEvent.event;
|
||||
|
||||
private _inEndpointNumber!: number;
|
||||
private _outEndpointNumber!: number;
|
||||
|
||||
private constructor(device: USBDevice, inEndPointNumber: number, outEndPointNumber: number) {
|
||||
public constructor(device: USBDevice) {
|
||||
this._device = device;
|
||||
this._inEndpointNumber = inEndPointNumber;
|
||||
this._outEndpointNumber = outEndPointNumber;
|
||||
window.navigator.usb.addEventListener('disconnect', this.handleDisconnect);
|
||||
}
|
||||
|
||||
private handleDisconnect = (e: USBConnectionEvent) => {
|
||||
if (e.device === this._device) {
|
||||
this.disconnectEvent.fire();
|
||||
}
|
||||
};
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
if (!this._device.opened) {
|
||||
await this._device.open();
|
||||
}
|
||||
|
||||
for (const configuration of this._device.configurations) {
|
||||
for (const interface_ of configuration.interfaces) {
|
||||
for (const alternate of interface_.alternates) {
|
||||
if (alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode &&
|
||||
alternate.interfaceClass === WebUsbDeviceFilter.classCode &&
|
||||
alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode) {
|
||||
if (this._device.configuration?.configurationValue !== configuration.configurationValue) {
|
||||
await this._device.selectConfiguration(configuration.configurationValue);
|
||||
}
|
||||
|
||||
if (!interface_.claimed) {
|
||||
await this._device.claimInterface(interface_.interfaceNumber);
|
||||
}
|
||||
|
||||
if (interface_.alternate.alternateSetting !== alternate.alternateSetting) {
|
||||
await this._device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
|
||||
}
|
||||
|
||||
for (const endpoint of alternate.endpoints) {
|
||||
switch (endpoint.direction) {
|
||||
case 'in':
|
||||
this._inEndpointNumber = endpoint.endpointNumber;
|
||||
if (this._outEndpointNumber !== undefined) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 'out':
|
||||
this._outEndpointNumber = endpoint.endpointNumber;
|
||||
if (this._inEndpointNumber !== undefined) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unknown error');
|
||||
}
|
||||
|
||||
public *iterateKeys(): Generator<ArrayBuffer, void, void> {
|
||||
|
@ -133,38 +146,23 @@ export default class AdbWebBackend implements AdbBackend {
|
|||
}
|
||||
|
||||
public async write(buffer: ArrayBuffer): Promise<void> {
|
||||
try {
|
||||
await this._device.transferOut(this._outEndpointNumber, buffer);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name === 'NotFoundError') {
|
||||
this.onDisconnectedEvent.fire();
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
await this._device.transferOut(this._outEndpointNumber, buffer);
|
||||
}
|
||||
|
||||
public async read(length: number): Promise<ArrayBuffer> {
|
||||
try {
|
||||
const result = await this._device.transferIn(this._inEndpointNumber, length);
|
||||
const result = await this._device.transferIn(this._inEndpointNumber, length);
|
||||
|
||||
if (result.status === 'stall') {
|
||||
await this._device.clearHalt('in', this._inEndpointNumber);
|
||||
}
|
||||
|
||||
const { buffer } = result.data!;
|
||||
return buffer;
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name === 'NotFoundError') {
|
||||
this.onDisconnectedEvent.fire();
|
||||
}
|
||||
|
||||
throw e;
|
||||
if (result.status === 'stall') {
|
||||
await this._device.clearHalt('in', this._inEndpointNumber);
|
||||
}
|
||||
|
||||
const { buffer } = result.data!;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public async dispose() {
|
||||
this.onDisconnectedEvent.dispose();
|
||||
window.navigator.usb.removeEventListener('disconnect', this.handleDisconnect);
|
||||
this.disconnectEvent.dispose();
|
||||
await this._device.close();
|
||||
}
|
||||
}
|
||||
|
|
15
packages/adb-backend-web/src/watcher.ts
Normal file
15
packages/adb-backend-web/src/watcher.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export class AdbWebBackendWatcher {
|
||||
private callback: () => void;
|
||||
|
||||
public constructor(callback: () => void) {
|
||||
this.callback = callback;
|
||||
|
||||
window.navigator.usb.addEventListener('connect', callback);
|
||||
window.navigator.usb.addEventListener('disconnect', callback);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
window.navigator.usb.removeEventListener('connect', this.callback);
|
||||
window.navigator.usb.removeEventListener('disconnect', this.callback);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import { PromiseResolver } from '@yume-chan/async-operation-manager';
|
|||
import { DisposableList } from '@yume-chan/event';
|
||||
import { AdbAuthenticationHandler, AdbDefaultAuthenticators } from './auth';
|
||||
import { AdbBackend } from './backend';
|
||||
import { AdbReverseCommand, AdbTcpIpCommand } from './commands';
|
||||
import { AdbFeatures } from './features';
|
||||
import { FrameBuffer } from './framebuffer';
|
||||
import { AdbCommand } from './packet';
|
||||
|
@ -16,6 +17,8 @@ export enum AdbPropKey {
|
|||
}
|
||||
|
||||
export class Adb {
|
||||
private packetDispatcher: AdbPacketDispatcher;
|
||||
|
||||
private backend: AdbBackend;
|
||||
public get onDisconnected() { return this.backend.onDisconnected; }
|
||||
|
||||
|
@ -36,29 +39,38 @@ export class Adb {
|
|||
private _features: AdbFeatures[] | undefined;
|
||||
public get features() { return this._features; }
|
||||
|
||||
private packetDispatcher: AdbPacketDispatcher;
|
||||
public readonly tcpip: AdbTcpIpCommand;
|
||||
|
||||
public readonly reverse: AdbReverseCommand;
|
||||
|
||||
public constructor(backend: AdbBackend) {
|
||||
this.backend = backend;
|
||||
|
||||
this.packetDispatcher = new AdbPacketDispatcher(backend);
|
||||
|
||||
this.tcpip = new AdbTcpIpCommand(this);
|
||||
this.reverse = new AdbReverseCommand(this.packetDispatcher);
|
||||
|
||||
backend.onDisconnected(this.dispose, this);
|
||||
}
|
||||
|
||||
public async connect(authenticators = AdbDefaultAuthenticators) {
|
||||
await this.backend.connect?.();
|
||||
this.packetDispatcher.start();
|
||||
|
||||
const version = 0x01000001;
|
||||
|
||||
const features = [
|
||||
'shell_v2',
|
||||
'cmd',
|
||||
AdbFeatures.StatV2,
|
||||
'shell_v2', // 9
|
||||
'cmd', // 7
|
||||
AdbFeatures.StatV2, // 5
|
||||
'ls_v2',
|
||||
'fixed_push_mkdir',
|
||||
'apex',
|
||||
'abb',
|
||||
'fixed_push_symlink_timestamp',
|
||||
'abb_exec',
|
||||
'remount_shell',
|
||||
'fixed_push_mkdir', // 4
|
||||
'apex', // 2
|
||||
'abb', // 8
|
||||
'fixed_push_symlink_timestamp', // 1
|
||||
'abb_exec', // 6
|
||||
'remount_shell', // 3
|
||||
'track_app',
|
||||
'sendrecv_v2',
|
||||
'sendrecv_v2_brotli',
|
||||
|
@ -163,33 +175,6 @@ export class Adb {
|
|||
}
|
||||
}
|
||||
|
||||
public async getDaemonTcpAddresses(): Promise<string[]> {
|
||||
const propAddr = (await this.shell('getprop', 'service.adb.listen_addrs')).trim();
|
||||
if (propAddr) {
|
||||
return propAddr.split(',');
|
||||
}
|
||||
|
||||
let port = (await this.shell('getprop', 'service.adb.tcp.port')).trim();
|
||||
if (port) {
|
||||
return [`0.0.0.0:${port}`];
|
||||
}
|
||||
|
||||
port = (await this.shell('getprop', 'persist.adb.tcp.port')).trim();
|
||||
if (port) {
|
||||
return [`0.0.0.0:${port}`];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public setDaemonTcpPort(port = 5555): Promise<string> {
|
||||
return this.createStreamAndReadAll(`tcpip:${port}`);
|
||||
}
|
||||
|
||||
public disableDaemonTcp(): Promise<string> {
|
||||
return this.createStreamAndReadAll('usb:');
|
||||
}
|
||||
|
||||
public async sync(): Promise<AdbSync> {
|
||||
const stream = await this.createStream('sync:');
|
||||
return new AdbSync(stream, this);
|
||||
|
@ -198,11 +183,7 @@ export class Adb {
|
|||
public async framebuffer(): Promise<FrameBuffer> {
|
||||
const stream = await this.createStream('framebuffer:');
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
return await FrameBuffer.deserialize({
|
||||
read: buffered.read.bind(buffered),
|
||||
encodeUtf8: this.backend.encodeUtf8.bind(this.backend),
|
||||
decodeUtf8: this.backend.decodeUtf8.bind(this.backend),
|
||||
});
|
||||
return FrameBuffer.deserialize(buffered);
|
||||
}
|
||||
|
||||
public async createStream(service: string): Promise<AdbStream> {
|
||||
|
|
|
@ -3,10 +3,14 @@ import { Event } from '@yume-chan/event';
|
|||
export type AdbKeyIterator = Iterator<ArrayBuffer> | AsyncIterator<ArrayBuffer>;
|
||||
|
||||
export interface AdbBackend {
|
||||
readonly serial: string;
|
||||
|
||||
readonly name: string | undefined;
|
||||
|
||||
readonly onDisconnected: Event<void>;
|
||||
|
||||
connect?(): void | Promise<void>;
|
||||
|
||||
iterateKeys(): AdbKeyIterator;
|
||||
|
||||
generateKey(): ArrayBuffer | Promise<ArrayBuffer>;
|
||||
|
|
11
packages/adb/src/commands/base.ts
Normal file
11
packages/adb/src/commands/base.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { AutoDisposable } from '@yume-chan/event';
|
||||
import { Adb } from '../adb';
|
||||
|
||||
export class AdbCommandBase extends AutoDisposable {
|
||||
protected adb: Adb;
|
||||
|
||||
public constructor(adb: Adb) {
|
||||
super();
|
||||
this.adb = adb;
|
||||
}
|
||||
}
|
3
packages/adb/src/commands/index.ts
Normal file
3
packages/adb/src/commands/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './base';
|
||||
export * from './tcpip';
|
||||
export * from './reverse';
|
122
packages/adb/src/commands/reverse.ts
Normal file
122
packages/adb/src/commands/reverse.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
import { AutoDisposable } from '@yume-chan/event';
|
||||
import { Struct } from '@yume-chan/struct';
|
||||
import { Adb } from '../adb';
|
||||
import { AdbPacket } from '../packet';
|
||||
import { AdbBufferedStream, AdbPacketDispatcher, AdbStream } from '../stream';
|
||||
import { AdbCommandBase } from './base';
|
||||
|
||||
export interface AdbReverseHandler {
|
||||
onStream(packet: AdbPacket, stream: AdbStream): void;
|
||||
}
|
||||
|
||||
export interface AdbForwardListener {
|
||||
deviceSerial: string;
|
||||
|
||||
localName: string;
|
||||
|
||||
remoteName: string;
|
||||
}
|
||||
|
||||
const AdbReverseStringResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.string('length', { length: 4 })
|
||||
.string('content', { lengthField: 'length' });
|
||||
|
||||
const AdbReverseErrorResponse =
|
||||
AdbReverseStringResponse
|
||||
.afterParsed((value) => {
|
||||
throw new Error(value.content);
|
||||
});
|
||||
|
||||
export class AdbReverseCommand extends AutoDisposable {
|
||||
private portToHandlerMap = new Map<number, AdbReverseHandler>();
|
||||
|
||||
private devicePortToPortMap = new Map<number, number>();
|
||||
|
||||
private dispatcher: AdbPacketDispatcher;
|
||||
|
||||
private listening = false;
|
||||
|
||||
public constructor(dispatcher: AdbPacketDispatcher) {
|
||||
super();
|
||||
this.dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
public async list(): Promise<AdbForwardListener[]> {
|
||||
const stream = await this.dispatcher.createStream('reverse:list-forward');
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
|
||||
const response = await AdbReverseStringResponse.deserialize(buffered);
|
||||
|
||||
return response.content!.split('\n').map(line => {
|
||||
const [deviceSerial, localName, remoteName] = line.split(' ');
|
||||
return { deviceSerial, localName, remoteName };
|
||||
});
|
||||
}
|
||||
|
||||
public async add(
|
||||
port: number,
|
||||
handler: AdbReverseHandler,
|
||||
devicePort: number = 0,
|
||||
): Promise<number> {
|
||||
if (!this.listening) {
|
||||
this.addDisposable(this.dispatcher.onStream(e => {
|
||||
if (e.handled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const address = this.dispatcher.backend.decodeUtf8(e.packet.payload!);
|
||||
const port = Number.parseInt(address.substring(4));
|
||||
if (this.portToHandlerMap.has(port)) {
|
||||
this.portToHandlerMap.get(port)!.onStream(e.packet, e.stream);
|
||||
e.handled = true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const stream = await this.dispatcher.createStream(`reverse:forward:tcp:${devicePort};tcp:${port}`);
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
|
||||
const success = this.dispatcher.backend.decodeUtf8(await buffered.read(4)) === 'OKAY';
|
||||
if (success) {
|
||||
const response = await AdbReverseStringResponse.deserialize(buffered);
|
||||
|
||||
devicePort = Number.parseInt(response.content!, 10);
|
||||
|
||||
this.portToHandlerMap.set(port, handler);
|
||||
this.devicePortToPortMap.set(devicePort, port);
|
||||
|
||||
return devicePort;
|
||||
} else {
|
||||
return await AdbReverseErrorResponse.deserialize(buffered);
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(devicePort: number): Promise<void> {
|
||||
const stream = await this.dispatcher.createStream(`reverse:killforward:tcp:${devicePort}`);
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
|
||||
const success = this.dispatcher.backend.decodeUtf8(await buffered.read(4)) === 'OKAY';
|
||||
if (success) {
|
||||
if (this.devicePortToPortMap.has(devicePort)) {
|
||||
this.portToHandlerMap.delete(this.devicePortToPortMap.get(devicePort)!);
|
||||
this.devicePortToPortMap.delete(devicePort);
|
||||
}
|
||||
} else {
|
||||
await AdbReverseErrorResponse.deserialize(buffered);
|
||||
}
|
||||
}
|
||||
|
||||
public async removeAll(): Promise<void> {
|
||||
const stream = await this.dispatcher.createStream(`reverse:killforward-all`);
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
|
||||
const success = this.dispatcher.backend.decodeUtf8(await buffered.read(4)) === 'OKAY';
|
||||
if (success) {
|
||||
this.devicePortToPortMap.clear();
|
||||
this.portToHandlerMap.clear();
|
||||
} else {
|
||||
await AdbReverseErrorResponse.deserialize(buffered);
|
||||
}
|
||||
}
|
||||
}
|
45
packages/adb/src/commands/tcpip.ts
Normal file
45
packages/adb/src/commands/tcpip.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { AdbCommandBase } from './base';
|
||||
|
||||
export class AdbTcpIpCommand extends AdbCommandBase {
|
||||
private async getProp(key: string): Promise<string> {
|
||||
const output = await this.adb.shell('getprop', key);
|
||||
return output.trim();
|
||||
}
|
||||
|
||||
public async getAddresses(): Promise<string[]> {
|
||||
const propAddr = await this.getProp('service.adb.listen_addrs');
|
||||
if (propAddr) {
|
||||
return propAddr.split(',');
|
||||
}
|
||||
|
||||
let port = await this.getProp('service.adb.tcp.port');
|
||||
if (port) {
|
||||
return [`0.0.0.0:${port}`];
|
||||
}
|
||||
|
||||
port = await this.getProp('persist.adb.tcp.port');
|
||||
if (port) {
|
||||
return [`0.0.0.0:${port}`];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public async setPort(port: number): Promise<void> {
|
||||
if (port <= 0) {
|
||||
throw new Error(`Invalid port ${port}`);
|
||||
}
|
||||
|
||||
const output = await this.adb.createStreamAndReadAll(`tcpip:${port}`);
|
||||
if (output !== `restarting in TCP mode port: ${port}\n`) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
}
|
||||
|
||||
public async disable(): Promise<void> {
|
||||
const output = await this.adb.createStreamAndReadAll('usb:');
|
||||
if (output !== 'restarting in USB mode\n') {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { StructDeserializationContext } from '@yume-chan/struct';
|
||||
import { AdbStreamBase } from './controller';
|
||||
import { AdbReadableStream } from './readable-stream';
|
||||
import { AdbStream } from './stream';
|
||||
|
@ -60,7 +61,9 @@ export class BufferedStream<T extends Stream> {
|
|||
}
|
||||
}
|
||||
|
||||
export class AdbBufferedStream extends BufferedStream<AdbReadableStream> implements AdbStreamBase {
|
||||
export class AdbBufferedStream
|
||||
extends BufferedStream<AdbReadableStream>
|
||||
implements AdbStreamBase, StructDeserializationContext {
|
||||
public get backend() { return this.stream.backend; }
|
||||
|
||||
public get localId() { return this.stream.localId; }
|
||||
|
@ -74,4 +77,12 @@ export class AdbBufferedStream extends BufferedStream<AdbReadableStream> impleme
|
|||
public write(data: ArrayBuffer): Promise<void> {
|
||||
return this.stream.write(data);
|
||||
}
|
||||
|
||||
public decodeUtf8(buffer: ArrayBuffer): string {
|
||||
return this.backend.decodeUtf8(buffer);
|
||||
}
|
||||
|
||||
public encodeUtf8(input: string): ArrayBuffer {
|
||||
return this.backend.encodeUtf8(input);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,14 +36,13 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
|
||||
public get onError() { return this.errorEvent.event; }
|
||||
|
||||
private _running = true;
|
||||
private _running = false;
|
||||
public get running() { return this._running; }
|
||||
|
||||
public constructor(backend: AdbBackend) {
|
||||
super();
|
||||
|
||||
this.backend = backend;
|
||||
this.receiveLoop();
|
||||
}
|
||||
|
||||
private async receiveLoop() {
|
||||
|
@ -129,7 +128,8 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
const [localId] = this.initializers.add<number>();
|
||||
this.initializers.resolve(localId, undefined);
|
||||
|
||||
const controller = new AdbStreamController(localId, packet.arg0, this);
|
||||
const remoteId = packet.arg0;
|
||||
const controller = new AdbStreamController(localId, remoteId, this);
|
||||
const stream = new AdbStream(controller);
|
||||
|
||||
const args: AdbStreamCreatedEventArgs = {
|
||||
|
@ -141,12 +141,17 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
|
||||
if (args.handled) {
|
||||
this.streams.set(localId, controller);
|
||||
await this.sendPacket(AdbCommand.OK, localId, packet.arg0);
|
||||
await this.sendPacket(AdbCommand.OK, localId, remoteId);
|
||||
} else {
|
||||
await this.sendPacket(AdbCommand.Close, 0, packet.arg0);
|
||||
await this.sendPacket(AdbCommand.Close, 0, remoteId);
|
||||
}
|
||||
}
|
||||
|
||||
public start() {
|
||||
this._running = true;
|
||||
this.receiveLoop();
|
||||
}
|
||||
|
||||
public async createStream(service: string): Promise<AdbStream> {
|
||||
const [localId, initializer] = this.initializers.add<number>();
|
||||
await this.sendPacket(AdbCommand.Open, localId, 0, service);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { DefaultButton, Dialog, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react';
|
||||
import { Adb } from '@yume-chan/adb';
|
||||
import AdbWebBackend from '@yume-chan/adb-backend-web';
|
||||
import { DefaultButton, Dialog, Dropdown, IDropdownOption, PrimaryButton, ProgressIndicator, Stack, TooltipHost } from '@fluentui/react';
|
||||
import { Adb, AdbBackend } from '@yume-chan/adb';
|
||||
import AdbWebBackend, { AdbWebBackendWatcher } from '@yume-chan/adb-backend-web';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { ErrorDialogContext } from './error-dialog';
|
||||
import { withDisplayName } from './utils';
|
||||
|
||||
const DropdownStyles = { dropdown: { width: 300 } };
|
||||
|
||||
interface ConnectProps {
|
||||
device: Adb | undefined;
|
||||
|
||||
|
@ -17,12 +19,66 @@ export default withDisplayName('Connect', ({
|
|||
}: ConnectProps): JSX.Element | null => {
|
||||
const { show: showErrorDialog } = useContext(ErrorDialogContext);
|
||||
|
||||
const [backendOptions, setBackendOptions] = useState<IDropdownOption[]>([]);
|
||||
const [selectedBackend, setSelectedBackend] = useState<AdbBackend | undefined>();
|
||||
useEffect(() => {
|
||||
async function refresh() {
|
||||
const backendList = await AdbWebBackend.getDevices();
|
||||
|
||||
const options = backendList.map(item => ({
|
||||
key: item.serial,
|
||||
text: `${item.serial} ${item.name ? `(${item.name})` : ''}`,
|
||||
data: item,
|
||||
}));
|
||||
setBackendOptions(options);
|
||||
|
||||
setSelectedBackend(old => {
|
||||
if (old && backendList.some(item => item.serial === old.serial)) {
|
||||
return old;
|
||||
}
|
||||
return backendList[0];
|
||||
});
|
||||
};
|
||||
|
||||
refresh();
|
||||
|
||||
const watcher = new AdbWebBackendWatcher(refresh);
|
||||
return () => watcher.dispose();
|
||||
}, []);
|
||||
|
||||
const handleSelectedBackendChange = (
|
||||
_e: React.FormEvent<HTMLDivElement>,
|
||||
option?: IDropdownOption,
|
||||
) => {
|
||||
setSelectedBackend(option?.data as AdbBackend);
|
||||
};
|
||||
|
||||
const requestAccess = useCallback(async () => {
|
||||
const backend = await AdbWebBackend.requestDevice();
|
||||
if (backend) {
|
||||
setBackendOptions(list => {
|
||||
for (const item of list) {
|
||||
if (item.key === backend.serial) {
|
||||
setSelectedBackend(item.data);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedBackend(backend);
|
||||
return [...list, {
|
||||
key: backend.serial,
|
||||
text: `${backend.serial} ${backend.name ? `(${backend.name})` : ''}`,
|
||||
data: backend,
|
||||
}];
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const connect = useCallback(async () => {
|
||||
try {
|
||||
const backend = await AdbWebBackend.pickDevice();
|
||||
if (backend) {
|
||||
const device = new Adb(backend);
|
||||
if (selectedBackend) {
|
||||
const device = new Adb(selectedBackend);
|
||||
try {
|
||||
setConnecting(true);
|
||||
await device.connect();
|
||||
|
@ -37,7 +93,7 @@ export default withDisplayName('Connect', ({
|
|||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
}, [onDeviceChange]);
|
||||
}, [selectedBackend, onDeviceChange]);
|
||||
const disconnect = useCallback(async () => {
|
||||
try {
|
||||
await device!.dispose();
|
||||
|
@ -53,18 +109,42 @@ export default withDisplayName('Connect', ({
|
|||
}, [device, onDeviceChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8, padding: 8 }}>
|
||||
{!device && <StackItem>
|
||||
<PrimaryButton text="Connect" onClick={connect} />
|
||||
</StackItem>}
|
||||
{device && <StackItem>
|
||||
<Stack
|
||||
horizontal
|
||||
verticalAlign="end"
|
||||
tokens={{ childrenGap: 8, padding: 8 }}
|
||||
>
|
||||
<Dropdown
|
||||
disabled={!!device || backendOptions.length === 0}
|
||||
label="Available devices"
|
||||
placeholder="No available devices"
|
||||
options={backendOptions}
|
||||
styles={DropdownStyles}
|
||||
selectedKey={selectedBackend?.serial}
|
||||
onChange={handleSelectedBackendChange}
|
||||
/>
|
||||
|
||||
{!device ? (
|
||||
<>
|
||||
<PrimaryButton
|
||||
text="Connect"
|
||||
disabled={!selectedBackend}
|
||||
primary={!!selectedBackend}
|
||||
onClick={connect}
|
||||
/>
|
||||
<TooltipHost
|
||||
content="WebADB can't connect to anything without your explicit permission."
|
||||
>
|
||||
<DefaultButton
|
||||
text="Add new device"
|
||||
primary={!selectedBackend}
|
||||
onClick={requestAccess}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</>
|
||||
) : (
|
||||
<DefaultButton text="Disconnect" onClick={disconnect} />
|
||||
</StackItem>}
|
||||
<StackItem>
|
||||
{device && `Connected to ${device.name}`}
|
||||
</StackItem>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
hidden={!connecting}
|
||||
|
@ -75,6 +155,6 @@ export default withDisplayName('Connect', ({
|
|||
>
|
||||
<ProgressIndicator />
|
||||
</Dialog>
|
||||
</>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -12,7 +12,8 @@ import FileManager from './routes/file-manager';
|
|||
import FrameBuffer from './routes/framebuffer';
|
||||
import Intro from './routes/intro';
|
||||
import Shell from './routes/shell';
|
||||
import TcpIp from './routes/tcp-ip';
|
||||
import TcpIp from './routes/tcpip';
|
||||
import { CommonStackTokens } from './styles';
|
||||
|
||||
initializeIcons();
|
||||
|
||||
|
@ -105,7 +106,7 @@ function App(): JSX.Element | null {
|
|||
<Separator />
|
||||
</StackItem>
|
||||
<StackItem grow styles={{ root: { minHeight: 0 } }}>
|
||||
<Stack horizontal verticalFill tokens={{ childrenGap: 8 }}>
|
||||
<Stack horizontal verticalFill tokens={CommonStackTokens}>
|
||||
<StackItem>
|
||||
<Nav
|
||||
styles={{ root: { width: 250 } }}
|
||||
|
|
|
@ -9,6 +9,8 @@ const classNames = mergeStyleSets({
|
|||
},
|
||||
});
|
||||
|
||||
const BoldTextStyles = { root: { fontWeight: '600' } };
|
||||
|
||||
interface CopyLinkProps {
|
||||
href: string;
|
||||
}
|
||||
|
@ -56,7 +58,7 @@ export default withDisplayName('Intro', () => {
|
|||
The latest version of Google Chrome (or Microsoft Edge) is recommended for best compatibility.
|
||||
</Text>
|
||||
|
||||
<Text block styles={{ root: { fontWeight: '600' } }}>
|
||||
<Text block styles={BoldTextStyles}>
|
||||
Windows user?
|
||||
</Text>
|
||||
<Text block>
|
||||
|
@ -65,7 +67,7 @@ export default withDisplayName('Intro', () => {
|
|||
.
|
||||
</Text>
|
||||
|
||||
<Text block styles={{ root: { fontWeight: '600' } }}>
|
||||
<Text block styles={BoldTextStyles}>
|
||||
Got "Unable to claim interface" error?
|
||||
</Text>
|
||||
<Text block>
|
||||
|
@ -73,7 +75,7 @@ export default withDisplayName('Intro', () => {
|
|||
1. Make sure ADB server is not running (run `adb kill-server` to stop it).<br />
|
||||
2. Make sure no other Android management tools are running
|
||||
</Text>
|
||||
<Text block styles={{ root: { fontWeight: '600' } }}>
|
||||
<Text block styles={BoldTextStyles}>
|
||||
Got "Access denied" error?
|
||||
</Text>
|
||||
<Text block>
|
||||
|
@ -82,7 +84,7 @@ export default withDisplayName('Intro', () => {
|
|||
https://bugs.chromium.org/p/chromium/issues/detail?id=1127206
|
||||
</Link>
|
||||
</Text>
|
||||
<Text block styles={{ root: { fontWeight: '600' } }}>
|
||||
<Text block styles={BoldTextStyles}>
|
||||
Can I connect my device wirelessly (ADB over WiFi)?
|
||||
</Text>
|
||||
<Text block>
|
||||
|
|
|
@ -8,11 +8,14 @@ import 'xterm/css/xterm.css';
|
|||
import { ResizeObserver, withDisplayName } from '../utils';
|
||||
import { RouteProps } from './type';
|
||||
|
||||
const containerStyle: CSSProperties = {
|
||||
const ResizeObserverStyle: CSSProperties = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
const UpIconProps = { iconName: 'ChevronUp' };
|
||||
const DownIconProps = { iconName: 'ChevronDown' };
|
||||
|
||||
export default withDisplayName('Shell', ({
|
||||
device,
|
||||
}: RouteProps): JSX.Element | null => {
|
||||
|
@ -99,21 +102,21 @@ export default withDisplayName('Shell', ({
|
|||
<StackItem>
|
||||
<IconButton
|
||||
disabled={!findKeyword}
|
||||
iconProps={{ iconName: 'ChevronUp' }}
|
||||
iconProps={UpIconProps}
|
||||
onClick={findPrevious}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<IconButton
|
||||
disabled={!findKeyword}
|
||||
iconProps={{ iconName: 'ChevronDown' }}
|
||||
iconProps={DownIconProps}
|
||||
onClick={findNext}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</StackItem>
|
||||
<StackItem grow styles={{ root: { minHeight: 0 } }}>
|
||||
<ResizeObserver style={containerStyle} onResize={handleResize}>
|
||||
<ResizeObserver style={ResizeObserverStyle} onResize={handleResize}>
|
||||
<div ref={handleContainerRef} style={{ height: '100%' }} />
|
||||
</ResizeObserver>
|
||||
</StackItem>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Label, MessageBar, PrimaryButton, Stack, StackItem, Text, TextField } from '@fluentui/react';
|
||||
import { useId } from '@uifabric/react-hooks';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { CommonStackTokens } from '../styles';
|
||||
import { withDisplayName } from '../utils';
|
||||
import { RouteProps } from './type';
|
||||
|
||||
|
@ -19,7 +20,7 @@ export default withDisplayName('TcpIp', ({
|
|||
return;
|
||||
}
|
||||
|
||||
const result = await device.getDaemonTcpAddresses();
|
||||
const result = await device.tcpip.getAddresses();
|
||||
setTcpAddresses(result);
|
||||
}, [device]);
|
||||
|
||||
|
@ -30,8 +31,7 @@ export default withDisplayName('TcpIp', ({
|
|||
return;
|
||||
}
|
||||
|
||||
const result = await device.setDaemonTcpPort(Number.parseInt(tcpPortValue, 10));
|
||||
console.log(result);
|
||||
await device.tcpip.setPort(Number.parseInt(tcpPortValue, 10));
|
||||
}, [device, tcpPortValue]);
|
||||
|
||||
const disableTcp = useCallback(async () => {
|
||||
|
@ -39,8 +39,7 @@ export default withDisplayName('TcpIp', ({
|
|||
return;
|
||||
}
|
||||
|
||||
const result = await device.disableDaemonTcp();
|
||||
console.log(result);
|
||||
await device.tcpip.disable();
|
||||
}, [device]);
|
||||
|
||||
return (
|
||||
|
@ -55,7 +54,7 @@ export default withDisplayName('TcpIp', ({
|
|||
<Text>Your device will disconnect after changing ADB over WiFi config.</Text>
|
||||
</MessageBar>
|
||||
</StackItem>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={CommonStackTokens}>
|
||||
<StackItem>
|
||||
<PrimaryButton text="Update Status" disabled={!device} onClick={queryTcpAddress} />
|
||||
</StackItem>
|
||||
|
@ -66,7 +65,7 @@ export default withDisplayName('TcpIp', ({
|
|||
: 'Disabled')}
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={CommonStackTokens}>
|
||||
<StackItem>
|
||||
<Label htmlFor={tcpPortInputId}>Port: </Label>
|
||||
</StackItem>
|
1
packages/demo/src/styles.ts
Normal file
1
packages/demo/src/styles.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const CommonStackTokens = { childrenGap: 8 };
|
|
@ -34,7 +34,7 @@ export namespace VariableLengthArray {
|
|||
|
||||
export interface Options<
|
||||
TInit = object,
|
||||
TLengthField extends KeyOfType<TInit, number> = any,
|
||||
TLengthField extends KeyOfType<TInit, number | string> = any,
|
||||
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
|
||||
> extends FieldDescriptorBaseOptions {
|
||||
lengthField: TLengthField;
|
||||
|
@ -131,7 +131,7 @@ export interface VariableLengthArray<
|
|||
TName extends string = string,
|
||||
TType extends Array.SubType = Array.SubType,
|
||||
TInit = object,
|
||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number> = any,
|
||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string> = any,
|
||||
TEmptyBehavior extends VariableLengthArray.EmptyBehavior = VariableLengthArray.EmptyBehavior,
|
||||
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>,
|
||||
TOptions extends VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior> = VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>
|
||||
|
@ -156,7 +156,11 @@ registerFieldTypeDefinition(
|
|||
async deserialize(
|
||||
{ context, field, object }
|
||||
): Promise<{ value: string | ArrayBuffer | undefined, extra?: ArrayBuffer; }> {
|
||||
const length = object[field.options.lengthField];
|
||||
let length = object[field.options.lengthField];
|
||||
if (typeof length === 'string') {
|
||||
length = Number.parseInt(length, 10);
|
||||
}
|
||||
|
||||
if (length === 0) {
|
||||
if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) {
|
||||
switch (field.subType) {
|
||||
|
|
|
@ -39,7 +39,7 @@ interface AddArrayFieldDescriptor<
|
|||
<
|
||||
TName extends string,
|
||||
TType extends Array.SubType,
|
||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number>,
|
||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
|
||||
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
|
||||
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
|
||||
>(
|
||||
|
@ -91,7 +91,7 @@ interface AddArraySubTypeFieldDescriptor<
|
|||
|
||||
<
|
||||
TName extends string,
|
||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number>,
|
||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
|
||||
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
|
||||
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
|
||||
>(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue