mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 01:39:21 +02:00
refactor: remove side effects
This commit is contained in:
parent
a335c1495c
commit
92511c63de
12 changed files with 241 additions and 194 deletions
|
@ -22,6 +22,7 @@ import {
|
|||
import type { ExactReadable } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
||||
|
||||
import { DeviceBusyError as _DeviceBusyError } from "./error.js";
|
||||
import type { UsbInterfaceFilter, UsbInterfaceIdentifier } from "./utils.js";
|
||||
import { findUsbEndpoints, getSerialNumber, isErrorName } from "./utils.js";
|
||||
|
||||
|
@ -254,6 +255,8 @@ export class AdbDaemonWebUsbConnection
|
|||
}
|
||||
|
||||
export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
|
||||
static DeviceBusyError = _DeviceBusyError;
|
||||
|
||||
#interface: UsbInterfaceIdentifier;
|
||||
#usbManager: USB;
|
||||
|
||||
|
@ -348,11 +351,5 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
|
|||
}
|
||||
|
||||
export namespace AdbDaemonWebUsbDevice {
|
||||
export class DeviceBusyError extends Error {
|
||||
constructor(cause?: Error) {
|
||||
super("The device is already in used by another program", {
|
||||
cause,
|
||||
});
|
||||
}
|
||||
}
|
||||
export type DeviceBusyError = _DeviceBusyError;
|
||||
}
|
||||
|
|
7
libraries/adb-daemon-webusb/src/error.ts
Normal file
7
libraries/adb-daemon-webusb/src/error.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export class DeviceBusyError extends Error {
|
||||
constructor(cause?: Error) {
|
||||
super("The device is already in used by another program", {
|
||||
cause,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,11 +18,11 @@ export class AdbDaemonWebUsbDeviceManager {
|
|||
*
|
||||
* May be `undefined` if current runtime does not support WebUSB.
|
||||
*/
|
||||
static readonly BROWSER =
|
||||
typeof globalThis.navigator !== "undefined" &&
|
||||
!!globalThis.navigator.usb
|
||||
static readonly BROWSER = /* #__PURE__ */ (() => {
|
||||
typeof globalThis.navigator !== "undefined" && globalThis.navigator.usb
|
||||
? new AdbDaemonWebUsbDeviceManager(globalThis.navigator.usb)
|
||||
: undefined;
|
||||
})();
|
||||
|
||||
#usbManager: USB;
|
||||
|
||||
|
|
|
@ -15,16 +15,27 @@ import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
|
|||
import { AdbBanner } from "../banner.js";
|
||||
import type { DeviceObserver as DeviceObserverBase } from "../device-observer.js";
|
||||
import type { AdbFeature } from "../features.js";
|
||||
import { hexToNumber, sequenceEqual } from "../utils/index.js";
|
||||
import { hexToNumber } from "../utils/index.js";
|
||||
|
||||
import {
|
||||
MDnsCommands,
|
||||
WirelessCommands,
|
||||
AlreadyConnectedError as _AlreadyConnectedError,
|
||||
NetworkError as _NetworkError,
|
||||
UnauthorizedError as _UnauthorizedError,
|
||||
} from "./commands/index.js";
|
||||
import { AdbServerDeviceObserverOwner } from "./observer.js";
|
||||
import { AdbServerStream, FAIL } from "./stream.js";
|
||||
import { AdbServerStream } from "./stream.js";
|
||||
import { AdbServerTransport } from "./transport.js";
|
||||
|
||||
/**
|
||||
* Client for the ADB Server.
|
||||
*/
|
||||
export class AdbServerClient {
|
||||
static NetworkError = _NetworkError;
|
||||
static UnauthorizedError = _UnauthorizedError;
|
||||
static AlreadyConnectedError = _AlreadyConnectedError;
|
||||
|
||||
static parseDeviceList(value: string): AdbServerClient.Device[] {
|
||||
const devices: AdbServerClient.Device[] = [];
|
||||
for (const line of value.split("\n")) {
|
||||
|
@ -99,8 +110,8 @@ export class AdbServerClient {
|
|||
|
||||
readonly connector: AdbServerClient.ServerConnector;
|
||||
|
||||
readonly wireless = new AdbServerClient.WirelessCommands(this);
|
||||
readonly mDns = new AdbServerClient.MDnsCommands(this);
|
||||
readonly wireless = new WirelessCommands(this);
|
||||
readonly mDns = new MDnsCommands(this);
|
||||
#observerOwner = new AdbServerDeviceObserverOwner(this);
|
||||
|
||||
constructor(connector: AdbServerClient.ServerConnector) {
|
||||
|
@ -537,138 +548,11 @@ export namespace AdbServerClient {
|
|||
transportId: bigint;
|
||||
}
|
||||
|
||||
export class NetworkError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "NetworkError";
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "UnauthorizedError";
|
||||
}
|
||||
}
|
||||
|
||||
export class AlreadyConnectedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "AlreadyConnectedError";
|
||||
}
|
||||
}
|
||||
|
||||
export class WirelessCommands {
|
||||
#client: AdbServerClient;
|
||||
|
||||
constructor(client: AdbServerClient) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* `adb pair <password> <address>`
|
||||
*/
|
||||
async pair(address: string, password: string): Promise<void> {
|
||||
const connection = await this.#client.createConnection(
|
||||
`host:pair:${password}:${address}`,
|
||||
);
|
||||
try {
|
||||
const response = await connection.readExactly(4);
|
||||
// `response` is either `FAIL`, or 4 hex digits for length of the string
|
||||
if (sequenceEqual(response, FAIL)) {
|
||||
throw new Error(await connection.readString());
|
||||
}
|
||||
const length = hexToNumber(response);
|
||||
// Ignore the string as it's always `Successful ...`
|
||||
await connection.readExactly(length);
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `adb connect <address>`
|
||||
*/
|
||||
async connect(address: string): Promise<void> {
|
||||
const connection = await this.#client.createConnection(
|
||||
`host:connect:${address}`,
|
||||
);
|
||||
try {
|
||||
const response = await connection.readString();
|
||||
switch (response) {
|
||||
case `already connected to ${address}`:
|
||||
throw new AdbServerClient.AlreadyConnectedError(
|
||||
response,
|
||||
);
|
||||
case `failed to connect to ${address}`: // `adb pair` mode not authorized
|
||||
case `failed to authenticate to ${address}`: // `adb tcpip` mode not authorized
|
||||
throw new AdbServerClient.UnauthorizedError(response);
|
||||
case `connected to ${address}`:
|
||||
return;
|
||||
default:
|
||||
throw new AdbServerClient.NetworkError(response);
|
||||
}
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `adb disconnect <address>`
|
||||
*/
|
||||
async disconnect(address: string): Promise<void> {
|
||||
const connection = await this.#client.createConnection(
|
||||
`host:disconnect:${address}`,
|
||||
);
|
||||
try {
|
||||
await connection.readString();
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MDnsCommands {
|
||||
#client: AdbServerClient;
|
||||
|
||||
constructor(client: AdbServerClient) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async check() {
|
||||
const connection =
|
||||
await this.#client.createConnection("host:mdns:check");
|
||||
try {
|
||||
const response = await connection.readString();
|
||||
return !response.startsWith("ERROR:");
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async getServices() {
|
||||
const connection =
|
||||
await this.#client.createConnection("host:mdns:services");
|
||||
try {
|
||||
const response = await connection.readString();
|
||||
return response
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const parts = line.split("\t");
|
||||
return {
|
||||
name: parts[0]!,
|
||||
service: parts[1]!,
|
||||
address: parts[2]!,
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeviceObserver extends DeviceObserverBase<Device> {
|
||||
onError: Event<Error>;
|
||||
}
|
||||
|
||||
export type NetworkError = _NetworkError;
|
||||
export type UnauthorizedError = _UnauthorizedError;
|
||||
export type AlreadyConnectedError = _AlreadyConnectedError;
|
||||
}
|
||||
|
|
2
libraries/adb/src/server/commands/index.ts
Normal file
2
libraries/adb/src/server/commands/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./m-dns.js";
|
||||
export * from "./wireless.js";
|
43
libraries/adb/src/server/commands/m-dns.ts
Normal file
43
libraries/adb/src/server/commands/m-dns.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
// cspell:ignore mdns
|
||||
|
||||
import type { AdbServerClient } from "../client.js";
|
||||
|
||||
export class MDnsCommands {
|
||||
#client: AdbServerClient;
|
||||
|
||||
constructor(client: AdbServerClient) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async check() {
|
||||
const connection =
|
||||
await this.#client.createConnection("host:mdns:check");
|
||||
try {
|
||||
const response = await connection.readString();
|
||||
return !response.startsWith("ERROR:");
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async getServices() {
|
||||
const connection =
|
||||
await this.#client.createConnection("host:mdns:services");
|
||||
try {
|
||||
const response = await connection.readString();
|
||||
return response
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const parts = line.split("\t");
|
||||
return {
|
||||
name: parts[0]!,
|
||||
service: parts[1]!,
|
||||
address: parts[2]!,
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
}
|
95
libraries/adb/src/server/commands/wireless.ts
Normal file
95
libraries/adb/src/server/commands/wireless.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
// cspell:ignore tport
|
||||
|
||||
import { hexToNumber, sequenceEqual } from "../../utils/index.js";
|
||||
import type { AdbServerClient } from "../client.js";
|
||||
|
||||
import { FAIL } from "../stream.js";
|
||||
|
||||
export class NetworkError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "NetworkError";
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "UnauthorizedError";
|
||||
}
|
||||
}
|
||||
|
||||
export class AlreadyConnectedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "AlreadyConnectedError";
|
||||
}
|
||||
}
|
||||
|
||||
export class WirelessCommands {
|
||||
#client: AdbServerClient;
|
||||
|
||||
constructor(client: AdbServerClient) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* `adb pair <password> <address>`
|
||||
*/
|
||||
async pair(address: string, password: string): Promise<void> {
|
||||
const connection = await this.#client.createConnection(
|
||||
`host:pair:${password}:${address}`,
|
||||
);
|
||||
try {
|
||||
const response = await connection.readExactly(4);
|
||||
// `response` is either `FAIL`, or 4 hex digits for length of the string
|
||||
if (sequenceEqual(response, FAIL)) {
|
||||
throw new Error(await connection.readString());
|
||||
}
|
||||
const length = hexToNumber(response);
|
||||
// Ignore the string as it's always `Successful ...`
|
||||
await connection.readExactly(length);
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `adb connect <address>`
|
||||
*/
|
||||
async connect(address: string): Promise<void> {
|
||||
const connection = await this.#client.createConnection(
|
||||
`host:connect:${address}`,
|
||||
);
|
||||
try {
|
||||
const response = await connection.readString();
|
||||
switch (response) {
|
||||
case `already connected to ${address}`:
|
||||
throw new AlreadyConnectedError(response);
|
||||
case `failed to connect to ${address}`: // `adb pair` mode not authorized
|
||||
case `failed to authenticate to ${address}`: // `adb tcpip` mode not authorized
|
||||
throw new UnauthorizedError(response);
|
||||
case `connected to ${address}`:
|
||||
return;
|
||||
default:
|
||||
throw new NetworkError(response);
|
||||
}
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `adb disconnect <address>`
|
||||
*/
|
||||
async disconnect(address: string): Promise<void> {
|
||||
const connection = await this.#client.createConnection(
|
||||
`host:disconnect:${address}`,
|
||||
);
|
||||
try {
|
||||
await connection.readString();
|
||||
} finally {
|
||||
await connection.dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,26 +10,27 @@ import { AdbFeature } from "../features.js";
|
|||
|
||||
import type { AdbServerClient } from "./client.js";
|
||||
|
||||
export const ADB_SERVER_DEFAULT_FEATURES = [
|
||||
AdbFeature.ShellV2,
|
||||
AdbFeature.Cmd,
|
||||
AdbFeature.StatV2,
|
||||
AdbFeature.ListV2,
|
||||
AdbFeature.FixedPushMkdir,
|
||||
"apex",
|
||||
AdbFeature.Abb,
|
||||
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
||||
// No special handling required.
|
||||
"fixed_push_symlink_timestamp",
|
||||
AdbFeature.AbbExec,
|
||||
"remount_shell",
|
||||
"track_app",
|
||||
AdbFeature.SendReceiveV2,
|
||||
"sendrecv_v2_brotli",
|
||||
"sendrecv_v2_lz4",
|
||||
"sendrecv_v2_zstd",
|
||||
"sendrecv_v2_dry_run_send",
|
||||
] as AdbFeature[];
|
||||
export const ADB_SERVER_DEFAULT_FEATURES = /* #__PURE__ */ (() =>
|
||||
[
|
||||
AdbFeature.ShellV2,
|
||||
AdbFeature.Cmd,
|
||||
AdbFeature.StatV2,
|
||||
AdbFeature.ListV2,
|
||||
AdbFeature.FixedPushMkdir,
|
||||
"apex",
|
||||
AdbFeature.Abb,
|
||||
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
||||
// No special handling required.
|
||||
"fixed_push_symlink_timestamp",
|
||||
AdbFeature.AbbExec,
|
||||
"remount_shell",
|
||||
"track_app",
|
||||
AdbFeature.SendReceiveV2,
|
||||
"sendrecv_v2_brotli",
|
||||
"sendrecv_v2_lz4",
|
||||
"sendrecv_v2_zstd",
|
||||
"sendrecv_v2_dry_run_send",
|
||||
] as AdbFeature[])();
|
||||
|
||||
export class AdbServerTransport implements AdbTransport {
|
||||
#client: AdbServerClient;
|
||||
|
|
|
@ -6,11 +6,14 @@ import type { SingleUser } from "./utils.js";
|
|||
|
||||
export type SettingsNamespace = "system" | "secure" | "global";
|
||||
|
||||
export enum SettingsResetMode {
|
||||
UntrustedDefaults = "untrusted_defaults",
|
||||
UntrustedClear = "untrusted_clear",
|
||||
TrustedDefaults = "trusted_defaults",
|
||||
}
|
||||
export const SettingsResetMode = {
|
||||
UntrustedDefaults: "untrusted_defaults",
|
||||
UntrustedClear: "untrusted_clear",
|
||||
TrustedDefaults: "trusted_defaults",
|
||||
} as const;
|
||||
|
||||
export type SettingsResetMode =
|
||||
(typeof SettingsResetMode)[keyof typeof SettingsResetMode];
|
||||
|
||||
export interface SettingsOptions {
|
||||
user?: SingleUser;
|
||||
|
|
|
@ -42,14 +42,16 @@ export interface BufferLike {
|
|||
|
||||
export const EmptyUint8Array = new Uint8Array(0);
|
||||
|
||||
export const buffer: BufferLike = function (
|
||||
// Prettier will move the annotation and make it invalid
|
||||
// prettier-ignore
|
||||
export const buffer: BufferLike = (/* #__NO_SIDE_EFFECTS__ */ (
|
||||
lengthOrField:
|
||||
| string
|
||||
| number
|
||||
| Field<number, never, unknown>
|
||||
| BufferLengthConverter<string, unknown>,
|
||||
converter?: Converter<Uint8Array, unknown>,
|
||||
): Field<unknown, string, Record<string, unknown>> {
|
||||
): Field<unknown, string, Record<string, unknown>> => {
|
||||
if (typeof lengthOrField === "number") {
|
||||
if (converter) {
|
||||
if (lengthOrField === 0) {
|
||||
|
@ -257,4 +259,4 @@ export const buffer: BufferLike = function (
|
|||
return reader.readExactly(length);
|
||||
},
|
||||
};
|
||||
} as never;
|
||||
}) as never;
|
||||
|
|
|
@ -23,18 +23,19 @@ export interface NumberField<T> extends Field<T, never, never> {
|
|||
|
||||
/* #__NO_SIDE_EFFECTS__ */
|
||||
function factory<T>(
|
||||
fn: NumberField<T>,
|
||||
size: number,
|
||||
serialize: Field<T, never, never>["serialize"],
|
||||
deserialize: Field<T, never, never>["deserialize"],
|
||||
): NumberField<T> {
|
||||
const result = () => result;
|
||||
result.size = size;
|
||||
result.serialize = serialize;
|
||||
result.deserialize = deserialize;
|
||||
return result as never;
|
||||
) {
|
||||
fn.size = size;
|
||||
fn.serialize = serialize;
|
||||
fn.deserialize = deserialize;
|
||||
}
|
||||
|
||||
export const u8 = factory<number>(
|
||||
export const u8: NumberField<number> = (() => u8) as never;
|
||||
factory(
|
||||
u8,
|
||||
1,
|
||||
(value, { buffer, index }) => {
|
||||
buffer[index] = value;
|
||||
|
@ -45,7 +46,9 @@ export const u8 = factory<number>(
|
|||
}),
|
||||
);
|
||||
|
||||
export const s8 = factory<number>(
|
||||
export const s8: NumberField<number> = (() => s8) as never;
|
||||
factory(
|
||||
s8,
|
||||
1,
|
||||
(value, { buffer, index }) => {
|
||||
buffer[index] = value;
|
||||
|
@ -56,7 +59,9 @@ export const s8 = factory<number>(
|
|||
}),
|
||||
);
|
||||
|
||||
export const u16 = factory<number>(
|
||||
export const u16: NumberField<number> = (() => u16) as never;
|
||||
factory(
|
||||
u16,
|
||||
2,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setUint16(buffer, index, value, littleEndian);
|
||||
|
@ -67,7 +72,9 @@ export const u16 = factory<number>(
|
|||
}),
|
||||
);
|
||||
|
||||
export const s16 = factory<number>(
|
||||
export const s16: NumberField<number> = (() => u16) as never;
|
||||
factory(
|
||||
s16,
|
||||
2,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setInt16(buffer, index, value, littleEndian);
|
||||
|
@ -78,7 +85,9 @@ export const s16 = factory<number>(
|
|||
}),
|
||||
);
|
||||
|
||||
export const u32 = factory<number>(
|
||||
export const u32: NumberField<number> = (() => u32) as never;
|
||||
factory(
|
||||
u32,
|
||||
4,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setUint32(buffer, index, value, littleEndian);
|
||||
|
@ -89,7 +98,9 @@ export const u32 = factory<number>(
|
|||
}),
|
||||
);
|
||||
|
||||
export const s32 = factory<number>(
|
||||
export const s32: NumberField<number> = (() => s32) as never;
|
||||
factory(
|
||||
s32,
|
||||
4,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setInt32(buffer, index, value, littleEndian);
|
||||
|
@ -100,7 +111,9 @@ export const s32 = factory<number>(
|
|||
}),
|
||||
);
|
||||
|
||||
export const u64 = factory<bigint>(
|
||||
export const u64: NumberField<bigint> = (() => u64) as never;
|
||||
factory(
|
||||
u64,
|
||||
8,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setUint64(buffer, index, value, littleEndian);
|
||||
|
@ -111,7 +124,9 @@ export const u64 = factory<bigint>(
|
|||
}),
|
||||
);
|
||||
|
||||
export const s64 = factory<bigint>(
|
||||
export const s64: NumberField<bigint> = (() => u64) as never;
|
||||
factory(
|
||||
s64,
|
||||
8,
|
||||
(value, { buffer, index, littleEndian }) => {
|
||||
setInt64(buffer, index, value, littleEndian);
|
||||
|
|
|
@ -25,19 +25,17 @@ export interface String {
|
|||
): Field<string, KOmitInit, KS>;
|
||||
}
|
||||
|
||||
// Rollup doesn't support `/* #__NO_SIDE_EFFECTS__ */ export const a = () => {}
|
||||
/* #__NO_SIDE_EFFECTS__ */
|
||||
function _string(
|
||||
// Prettier will move the annotation and make it invalid
|
||||
// prettier-ignore
|
||||
export const string: String = (/* #__NO_SIDE_EFFECTS__ */ (
|
||||
lengthOrField: string | number | BufferLengthConverter<string, unknown>,
|
||||
): Field<string, string, Record<string, unknown>> & {
|
||||
as: <T>(infer: T) => Field<T, string, Record<string, unknown>>;
|
||||
} {
|
||||
} => {
|
||||
const field = buffer(lengthOrField as never, {
|
||||
convert: decodeUtf8,
|
||||
back: encodeUtf8,
|
||||
});
|
||||
(field as never as { as: unknown }).as = () => field;
|
||||
return field as never;
|
||||
}
|
||||
|
||||
export const string: String = _string as never;
|
||||
}) as never;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue