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",
|
"addrs",
|
||||||
"fluentui",
|
"fluentui",
|
||||||
"getprop",
|
"getprop",
|
||||||
|
"killforward",
|
||||||
"lapo",
|
"lapo",
|
||||||
"lstat",
|
"lstat",
|
||||||
"mitm",
|
"mitm",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { AdbBackend, decodeBase64, encodeBase64 } from '@yume-chan/adb';
|
import { AdbBackend, decodeBase64, encodeBase64 } from '@yume-chan/adb';
|
||||||
import { EventEmitter } from '@yume-chan/event';
|
import { EventEmitter } from '@yume-chan/event';
|
||||||
|
|
||||||
|
export * from './watcher';
|
||||||
|
|
||||||
export const WebUsbDeviceFilter: USBDeviceFilter = {
|
export const WebUsbDeviceFilter: USBDeviceFilter = {
|
||||||
classCode: 0xFF,
|
classCode: 0xFF,
|
||||||
subclassCode: 0x42,
|
subclassCode: 0x42,
|
||||||
|
@ -21,58 +23,15 @@ export function decodeUtf8(buffer: ArrayBuffer): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AdbWebBackend implements AdbBackend {
|
export default class AdbWebBackend implements AdbBackend {
|
||||||
public static async fromDevice(device: USBDevice): Promise<AdbWebBackend> {
|
public static async getDevices(): Promise<AdbWebBackend[]> {
|
||||||
await device.open();
|
const devices = await window.navigator.usb.getDevices();
|
||||||
|
return devices.map(device => new AdbWebBackend(device));
|
||||||
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 pickDevice(): Promise<AdbWebBackend | undefined> {
|
public static async requestDevice(): Promise<AdbWebBackend | undefined> {
|
||||||
try {
|
try {
|
||||||
const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] });
|
const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] });
|
||||||
return AdbWebBackend.fromDevice(device);
|
return new AdbWebBackend(device);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
switch (e.name) {
|
switch (e.name) {
|
||||||
case 'NotFoundError':
|
case 'NotFoundError':
|
||||||
|
@ -85,18 +44,72 @@ export default class AdbWebBackend implements AdbBackend {
|
||||||
|
|
||||||
private _device: USBDevice;
|
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 get name(): string { return this._device.productName!; }
|
||||||
public readonly onDisconnected = this.onDisconnectedEvent.event;
|
|
||||||
|
private readonly disconnectEvent = new EventEmitter<void>();
|
||||||
|
public readonly onDisconnected = this.disconnectEvent.event;
|
||||||
|
|
||||||
private _inEndpointNumber!: number;
|
private _inEndpointNumber!: number;
|
||||||
private _outEndpointNumber!: number;
|
private _outEndpointNumber!: number;
|
||||||
|
|
||||||
private constructor(device: USBDevice, inEndPointNumber: number, outEndPointNumber: number) {
|
public constructor(device: USBDevice) {
|
||||||
this._device = device;
|
this._device = device;
|
||||||
this._inEndpointNumber = inEndPointNumber;
|
window.navigator.usb.addEventListener('disconnect', this.handleDisconnect);
|
||||||
this._outEndpointNumber = outEndPointNumber;
|
}
|
||||||
|
|
||||||
|
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> {
|
public *iterateKeys(): Generator<ArrayBuffer, void, void> {
|
||||||
|
@ -133,38 +146,23 @@ export default class AdbWebBackend implements AdbBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async write(buffer: ArrayBuffer): Promise<void> {
|
public async write(buffer: ArrayBuffer): Promise<void> {
|
||||||
try {
|
await this._device.transferOut(this._outEndpointNumber, buffer);
|
||||||
await this._device.transferOut(this._outEndpointNumber, buffer);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error && e.name === 'NotFoundError') {
|
|
||||||
this.onDisconnectedEvent.fire();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async read(length: number): Promise<ArrayBuffer> {
|
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') {
|
if (result.status === 'stall') {
|
||||||
await this._device.clearHalt('in', this._inEndpointNumber);
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { buffer } = result.data!;
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async dispose() {
|
public async dispose() {
|
||||||
this.onDisconnectedEvent.dispose();
|
window.navigator.usb.removeEventListener('disconnect', this.handleDisconnect);
|
||||||
|
this.disconnectEvent.dispose();
|
||||||
await this._device.close();
|
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 { DisposableList } from '@yume-chan/event';
|
||||||
import { AdbAuthenticationHandler, AdbDefaultAuthenticators } from './auth';
|
import { AdbAuthenticationHandler, AdbDefaultAuthenticators } from './auth';
|
||||||
import { AdbBackend } from './backend';
|
import { AdbBackend } from './backend';
|
||||||
|
import { AdbReverseCommand, AdbTcpIpCommand } from './commands';
|
||||||
import { AdbFeatures } from './features';
|
import { AdbFeatures } from './features';
|
||||||
import { FrameBuffer } from './framebuffer';
|
import { FrameBuffer } from './framebuffer';
|
||||||
import { AdbCommand } from './packet';
|
import { AdbCommand } from './packet';
|
||||||
|
@ -16,6 +17,8 @@ export enum AdbPropKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Adb {
|
export class Adb {
|
||||||
|
private packetDispatcher: AdbPacketDispatcher;
|
||||||
|
|
||||||
private backend: AdbBackend;
|
private backend: AdbBackend;
|
||||||
public get onDisconnected() { return this.backend.onDisconnected; }
|
public get onDisconnected() { return this.backend.onDisconnected; }
|
||||||
|
|
||||||
|
@ -36,29 +39,38 @@ export class Adb {
|
||||||
private _features: AdbFeatures[] | undefined;
|
private _features: AdbFeatures[] | undefined;
|
||||||
public get features() { return this._features; }
|
public get features() { return this._features; }
|
||||||
|
|
||||||
private packetDispatcher: AdbPacketDispatcher;
|
public readonly tcpip: AdbTcpIpCommand;
|
||||||
|
|
||||||
|
public readonly reverse: AdbReverseCommand;
|
||||||
|
|
||||||
public constructor(backend: AdbBackend) {
|
public constructor(backend: AdbBackend) {
|
||||||
this.backend = backend;
|
this.backend = backend;
|
||||||
|
|
||||||
this.packetDispatcher = new AdbPacketDispatcher(backend);
|
this.packetDispatcher = new AdbPacketDispatcher(backend);
|
||||||
|
|
||||||
|
this.tcpip = new AdbTcpIpCommand(this);
|
||||||
|
this.reverse = new AdbReverseCommand(this.packetDispatcher);
|
||||||
|
|
||||||
backend.onDisconnected(this.dispose, this);
|
backend.onDisconnected(this.dispose, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connect(authenticators = AdbDefaultAuthenticators) {
|
public async connect(authenticators = AdbDefaultAuthenticators) {
|
||||||
|
await this.backend.connect?.();
|
||||||
|
this.packetDispatcher.start();
|
||||||
|
|
||||||
const version = 0x01000001;
|
const version = 0x01000001;
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
'shell_v2',
|
'shell_v2', // 9
|
||||||
'cmd',
|
'cmd', // 7
|
||||||
AdbFeatures.StatV2,
|
AdbFeatures.StatV2, // 5
|
||||||
'ls_v2',
|
'ls_v2',
|
||||||
'fixed_push_mkdir',
|
'fixed_push_mkdir', // 4
|
||||||
'apex',
|
'apex', // 2
|
||||||
'abb',
|
'abb', // 8
|
||||||
'fixed_push_symlink_timestamp',
|
'fixed_push_symlink_timestamp', // 1
|
||||||
'abb_exec',
|
'abb_exec', // 6
|
||||||
'remount_shell',
|
'remount_shell', // 3
|
||||||
'track_app',
|
'track_app',
|
||||||
'sendrecv_v2',
|
'sendrecv_v2',
|
||||||
'sendrecv_v2_brotli',
|
'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> {
|
public async sync(): Promise<AdbSync> {
|
||||||
const stream = await this.createStream('sync:');
|
const stream = await this.createStream('sync:');
|
||||||
return new AdbSync(stream, this);
|
return new AdbSync(stream, this);
|
||||||
|
@ -198,11 +183,7 @@ export class Adb {
|
||||||
public async framebuffer(): Promise<FrameBuffer> {
|
public async framebuffer(): Promise<FrameBuffer> {
|
||||||
const stream = await this.createStream('framebuffer:');
|
const stream = await this.createStream('framebuffer:');
|
||||||
const buffered = new AdbBufferedStream(stream);
|
const buffered = new AdbBufferedStream(stream);
|
||||||
return await FrameBuffer.deserialize({
|
return FrameBuffer.deserialize(buffered);
|
||||||
read: buffered.read.bind(buffered),
|
|
||||||
encodeUtf8: this.backend.encodeUtf8.bind(this.backend),
|
|
||||||
decodeUtf8: this.backend.decodeUtf8.bind(this.backend),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createStream(service: string): Promise<AdbStream> {
|
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 type AdbKeyIterator = Iterator<ArrayBuffer> | AsyncIterator<ArrayBuffer>;
|
||||||
|
|
||||||
export interface AdbBackend {
|
export interface AdbBackend {
|
||||||
|
readonly serial: string;
|
||||||
|
|
||||||
readonly name: string | undefined;
|
readonly name: string | undefined;
|
||||||
|
|
||||||
readonly onDisconnected: Event<void>;
|
readonly onDisconnected: Event<void>;
|
||||||
|
|
||||||
|
connect?(): void | Promise<void>;
|
||||||
|
|
||||||
iterateKeys(): AdbKeyIterator;
|
iterateKeys(): AdbKeyIterator;
|
||||||
|
|
||||||
generateKey(): ArrayBuffer | Promise<ArrayBuffer>;
|
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 { AdbStreamBase } from './controller';
|
||||||
import { AdbReadableStream } from './readable-stream';
|
import { AdbReadableStream } from './readable-stream';
|
||||||
import { AdbStream } from './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 backend() { return this.stream.backend; }
|
||||||
|
|
||||||
public get localId() { return this.stream.localId; }
|
public get localId() { return this.stream.localId; }
|
||||||
|
@ -74,4 +77,12 @@ export class AdbBufferedStream extends BufferedStream<AdbReadableStream> impleme
|
||||||
public write(data: ArrayBuffer): Promise<void> {
|
public write(data: ArrayBuffer): Promise<void> {
|
||||||
return this.stream.write(data);
|
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>());
|
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
|
||||||
public get onError() { return this.errorEvent.event; }
|
public get onError() { return this.errorEvent.event; }
|
||||||
|
|
||||||
private _running = true;
|
private _running = false;
|
||||||
public get running() { return this._running; }
|
public get running() { return this._running; }
|
||||||
|
|
||||||
public constructor(backend: AdbBackend) {
|
public constructor(backend: AdbBackend) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.backend = backend;
|
this.backend = backend;
|
||||||
this.receiveLoop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async receiveLoop() {
|
private async receiveLoop() {
|
||||||
|
@ -129,7 +128,8 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
const [localId] = this.initializers.add<number>();
|
const [localId] = this.initializers.add<number>();
|
||||||
this.initializers.resolve(localId, undefined);
|
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 stream = new AdbStream(controller);
|
||||||
|
|
||||||
const args: AdbStreamCreatedEventArgs = {
|
const args: AdbStreamCreatedEventArgs = {
|
||||||
|
@ -141,12 +141,17 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
|
|
||||||
if (args.handled) {
|
if (args.handled) {
|
||||||
this.streams.set(localId, controller);
|
this.streams.set(localId, controller);
|
||||||
await this.sendPacket(AdbCommand.OK, localId, packet.arg0);
|
await this.sendPacket(AdbCommand.OK, localId, remoteId);
|
||||||
} else {
|
} 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> {
|
public async createStream(service: string): Promise<AdbStream> {
|
||||||
const [localId, initializer] = this.initializers.add<number>();
|
const [localId, initializer] = this.initializers.add<number>();
|
||||||
await this.sendPacket(AdbCommand.Open, localId, 0, service);
|
await this.sendPacket(AdbCommand.Open, localId, 0, service);
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { DefaultButton, Dialog, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react';
|
import { DefaultButton, Dialog, Dropdown, IDropdownOption, PrimaryButton, ProgressIndicator, Stack, TooltipHost } from '@fluentui/react';
|
||||||
import { Adb } from '@yume-chan/adb';
|
import { Adb, AdbBackend } from '@yume-chan/adb';
|
||||||
import AdbWebBackend from '@yume-chan/adb-backend-web';
|
import AdbWebBackend, { AdbWebBackendWatcher } from '@yume-chan/adb-backend-web';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { ErrorDialogContext } from './error-dialog';
|
import { ErrorDialogContext } from './error-dialog';
|
||||||
import { withDisplayName } from './utils';
|
import { withDisplayName } from './utils';
|
||||||
|
|
||||||
|
const DropdownStyles = { dropdown: { width: 300 } };
|
||||||
|
|
||||||
interface ConnectProps {
|
interface ConnectProps {
|
||||||
device: Adb | undefined;
|
device: Adb | undefined;
|
||||||
|
|
||||||
|
@ -17,12 +19,66 @@ export default withDisplayName('Connect', ({
|
||||||
}: ConnectProps): JSX.Element | null => {
|
}: ConnectProps): JSX.Element | null => {
|
||||||
const { show: showErrorDialog } = useContext(ErrorDialogContext);
|
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 [connecting, setConnecting] = useState(false);
|
||||||
const connect = useCallback(async () => {
|
const connect = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const backend = await AdbWebBackend.pickDevice();
|
if (selectedBackend) {
|
||||||
if (backend) {
|
const device = new Adb(selectedBackend);
|
||||||
const device = new Adb(backend);
|
|
||||||
try {
|
try {
|
||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
await device.connect();
|
await device.connect();
|
||||||
|
@ -37,7 +93,7 @@ export default withDisplayName('Connect', ({
|
||||||
} finally {
|
} finally {
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
}
|
}
|
||||||
}, [onDeviceChange]);
|
}, [selectedBackend, onDeviceChange]);
|
||||||
const disconnect = useCallback(async () => {
|
const disconnect = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await device!.dispose();
|
await device!.dispose();
|
||||||
|
@ -53,18 +109,42 @@ export default withDisplayName('Connect', ({
|
||||||
}, [device, onDeviceChange]);
|
}, [device, onDeviceChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack
|
||||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8, padding: 8 }}>
|
horizontal
|
||||||
{!device && <StackItem>
|
verticalAlign="end"
|
||||||
<PrimaryButton text="Connect" onClick={connect} />
|
tokens={{ childrenGap: 8, padding: 8 }}
|
||||||
</StackItem>}
|
>
|
||||||
{device && <StackItem>
|
<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} />
|
<DefaultButton text="Disconnect" onClick={disconnect} />
|
||||||
</StackItem>}
|
)}
|
||||||
<StackItem>
|
|
||||||
{device && `Connected to ${device.name}`}
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
hidden={!connecting}
|
hidden={!connecting}
|
||||||
|
@ -75,6 +155,6 @@ export default withDisplayName('Connect', ({
|
||||||
>
|
>
|
||||||
<ProgressIndicator />
|
<ProgressIndicator />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,8 @@ import FileManager from './routes/file-manager';
|
||||||
import FrameBuffer from './routes/framebuffer';
|
import FrameBuffer from './routes/framebuffer';
|
||||||
import Intro from './routes/intro';
|
import Intro from './routes/intro';
|
||||||
import Shell from './routes/shell';
|
import Shell from './routes/shell';
|
||||||
import TcpIp from './routes/tcp-ip';
|
import TcpIp from './routes/tcpip';
|
||||||
|
import { CommonStackTokens } from './styles';
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
|
@ -105,7 +106,7 @@ function App(): JSX.Element | null {
|
||||||
<Separator />
|
<Separator />
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem grow styles={{ root: { minHeight: 0 } }}>
|
<StackItem grow styles={{ root: { minHeight: 0 } }}>
|
||||||
<Stack horizontal verticalFill tokens={{ childrenGap: 8 }}>
|
<Stack horizontal verticalFill tokens={CommonStackTokens}>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Nav
|
<Nav
|
||||||
styles={{ root: { width: 250 } }}
|
styles={{ root: { width: 250 } }}
|
||||||
|
|
|
@ -9,6 +9,8 @@ const classNames = mergeStyleSets({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const BoldTextStyles = { root: { fontWeight: '600' } };
|
||||||
|
|
||||||
interface CopyLinkProps {
|
interface CopyLinkProps {
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
@ -56,7 +58,7 @@ export default withDisplayName('Intro', () => {
|
||||||
The latest version of Google Chrome (or Microsoft Edge) is recommended for best compatibility.
|
The latest version of Google Chrome (or Microsoft Edge) is recommended for best compatibility.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text block styles={{ root: { fontWeight: '600' } }}>
|
<Text block styles={BoldTextStyles}>
|
||||||
Windows user?
|
Windows user?
|
||||||
</Text>
|
</Text>
|
||||||
<Text block>
|
<Text block>
|
||||||
|
@ -65,7 +67,7 @@ export default withDisplayName('Intro', () => {
|
||||||
.
|
.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text block styles={{ root: { fontWeight: '600' } }}>
|
<Text block styles={BoldTextStyles}>
|
||||||
Got "Unable to claim interface" error?
|
Got "Unable to claim interface" error?
|
||||||
</Text>
|
</Text>
|
||||||
<Text block>
|
<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 />
|
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
|
2. Make sure no other Android management tools are running
|
||||||
</Text>
|
</Text>
|
||||||
<Text block styles={{ root: { fontWeight: '600' } }}>
|
<Text block styles={BoldTextStyles}>
|
||||||
Got "Access denied" error?
|
Got "Access denied" error?
|
||||||
</Text>
|
</Text>
|
||||||
<Text block>
|
<Text block>
|
||||||
|
@ -82,7 +84,7 @@ export default withDisplayName('Intro', () => {
|
||||||
https://bugs.chromium.org/p/chromium/issues/detail?id=1127206
|
https://bugs.chromium.org/p/chromium/issues/detail?id=1127206
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
<Text block styles={{ root: { fontWeight: '600' } }}>
|
<Text block styles={BoldTextStyles}>
|
||||||
Can I connect my device wirelessly (ADB over WiFi)?
|
Can I connect my device wirelessly (ADB over WiFi)?
|
||||||
</Text>
|
</Text>
|
||||||
<Text block>
|
<Text block>
|
||||||
|
|
|
@ -8,11 +8,14 @@ import 'xterm/css/xterm.css';
|
||||||
import { ResizeObserver, withDisplayName } from '../utils';
|
import { ResizeObserver, withDisplayName } from '../utils';
|
||||||
import { RouteProps } from './type';
|
import { RouteProps } from './type';
|
||||||
|
|
||||||
const containerStyle: CSSProperties = {
|
const ResizeObserverStyle: CSSProperties = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UpIconProps = { iconName: 'ChevronUp' };
|
||||||
|
const DownIconProps = { iconName: 'ChevronDown' };
|
||||||
|
|
||||||
export default withDisplayName('Shell', ({
|
export default withDisplayName('Shell', ({
|
||||||
device,
|
device,
|
||||||
}: RouteProps): JSX.Element | null => {
|
}: RouteProps): JSX.Element | null => {
|
||||||
|
@ -99,21 +102,21 @@ export default withDisplayName('Shell', ({
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={!findKeyword}
|
disabled={!findKeyword}
|
||||||
iconProps={{ iconName: 'ChevronUp' }}
|
iconProps={UpIconProps}
|
||||||
onClick={findPrevious}
|
onClick={findPrevious}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={!findKeyword}
|
disabled={!findKeyword}
|
||||||
iconProps={{ iconName: 'ChevronDown' }}
|
iconProps={DownIconProps}
|
||||||
onClick={findNext}
|
onClick={findNext}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem grow styles={{ root: { minHeight: 0 } }}>
|
<StackItem grow styles={{ root: { minHeight: 0 } }}>
|
||||||
<ResizeObserver style={containerStyle} onResize={handleResize}>
|
<ResizeObserver style={ResizeObserverStyle} onResize={handleResize}>
|
||||||
<div ref={handleContainerRef} style={{ height: '100%' }} />
|
<div ref={handleContainerRef} style={{ height: '100%' }} />
|
||||||
</ResizeObserver>
|
</ResizeObserver>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Label, MessageBar, PrimaryButton, Stack, StackItem, Text, TextField } from '@fluentui/react';
|
import { Label, MessageBar, PrimaryButton, Stack, StackItem, Text, TextField } from '@fluentui/react';
|
||||||
import { useId } from '@uifabric/react-hooks';
|
import { useId } from '@uifabric/react-hooks';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { CommonStackTokens } from '../styles';
|
||||||
import { withDisplayName } from '../utils';
|
import { withDisplayName } from '../utils';
|
||||||
import { RouteProps } from './type';
|
import { RouteProps } from './type';
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ export default withDisplayName('TcpIp', ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await device.getDaemonTcpAddresses();
|
const result = await device.tcpip.getAddresses();
|
||||||
setTcpAddresses(result);
|
setTcpAddresses(result);
|
||||||
}, [device]);
|
}, [device]);
|
||||||
|
|
||||||
|
@ -30,8 +31,7 @@ export default withDisplayName('TcpIp', ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await device.setDaemonTcpPort(Number.parseInt(tcpPortValue, 10));
|
await device.tcpip.setPort(Number.parseInt(tcpPortValue, 10));
|
||||||
console.log(result);
|
|
||||||
}, [device, tcpPortValue]);
|
}, [device, tcpPortValue]);
|
||||||
|
|
||||||
const disableTcp = useCallback(async () => {
|
const disableTcp = useCallback(async () => {
|
||||||
|
@ -39,8 +39,7 @@ export default withDisplayName('TcpIp', ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await device.disableDaemonTcp();
|
await device.tcpip.disable();
|
||||||
console.log(result);
|
|
||||||
}, [device]);
|
}, [device]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -55,7 +54,7 @@ export default withDisplayName('TcpIp', ({
|
||||||
<Text>Your device will disconnect after changing ADB over WiFi config.</Text>
|
<Text>Your device will disconnect after changing ADB over WiFi config.</Text>
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
|
<Stack horizontal verticalAlign="center" tokens={CommonStackTokens}>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<PrimaryButton text="Update Status" disabled={!device} onClick={queryTcpAddress} />
|
<PrimaryButton text="Update Status" disabled={!device} onClick={queryTcpAddress} />
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
@ -66,7 +65,7 @@ export default withDisplayName('TcpIp', ({
|
||||||
: 'Disabled')}
|
: 'Disabled')}
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
|
<Stack horizontal verticalAlign="center" tokens={CommonStackTokens}>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Label htmlFor={tcpPortInputId}>Port: </Label>
|
<Label htmlFor={tcpPortInputId}>Port: </Label>
|
||||||
</StackItem>
|
</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<
|
export interface Options<
|
||||||
TInit = object,
|
TInit = object,
|
||||||
TLengthField extends KeyOfType<TInit, number> = any,
|
TLengthField extends KeyOfType<TInit, number | string> = any,
|
||||||
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
|
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
|
||||||
> extends FieldDescriptorBaseOptions {
|
> extends FieldDescriptorBaseOptions {
|
||||||
lengthField: TLengthField;
|
lengthField: TLengthField;
|
||||||
|
@ -131,7 +131,7 @@ export interface VariableLengthArray<
|
||||||
TName extends string = string,
|
TName extends string = string,
|
||||||
TType extends Array.SubType = Array.SubType,
|
TType extends Array.SubType = Array.SubType,
|
||||||
TInit = object,
|
TInit = object,
|
||||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number> = any,
|
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string> = any,
|
||||||
TEmptyBehavior extends VariableLengthArray.EmptyBehavior = VariableLengthArray.EmptyBehavior,
|
TEmptyBehavior extends VariableLengthArray.EmptyBehavior = VariableLengthArray.EmptyBehavior,
|
||||||
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>,
|
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>,
|
||||||
TOptions extends VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior> = VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>
|
TOptions extends VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior> = VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>
|
||||||
|
@ -156,7 +156,11 @@ registerFieldTypeDefinition(
|
||||||
async deserialize(
|
async deserialize(
|
||||||
{ context, field, object }
|
{ context, field, object }
|
||||||
): Promise<{ value: string | ArrayBuffer | undefined, extra?: ArrayBuffer; }> {
|
): 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 (length === 0) {
|
||||||
if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) {
|
if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) {
|
||||||
switch (field.subType) {
|
switch (field.subType) {
|
||||||
|
|
|
@ -39,7 +39,7 @@ interface AddArrayFieldDescriptor<
|
||||||
<
|
<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TType extends Array.SubType,
|
TType extends Array.SubType,
|
||||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number>,
|
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
|
||||||
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
|
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
|
||||||
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
|
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
|
||||||
>(
|
>(
|
||||||
|
@ -91,7 +91,7 @@ interface AddArraySubTypeFieldDescriptor<
|
||||||
|
|
||||||
<
|
<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number>,
|
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
|
||||||
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
|
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
|
||||||
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
|
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
|
||||||
>(
|
>(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue