refactor(adb): decouple auth from dispatcher

This commit is contained in:
Simon Chan 2022-03-03 16:15:56 +08:00
parent a92d80951b
commit 8650537c66
No known key found for this signature in database
GPG key ID: 8F75717685A974FB
28 changed files with 416 additions and 325 deletions

View file

@ -143,9 +143,10 @@ function _Connect(): JSX.Element | null {
let device: Adb | undefined; let device: Adb | undefined;
try { try {
setConnecting(true); setConnecting(true);
device = await Adb.connect(selectedBackend, logger.logger); const connection = await selectedBackend.connect();
await device.authenticate(CredentialStore); const adbConnection = Adb.createConnection(connection);
globalState.setDevice(device); device = await Adb.authenticate(adbConnection, CredentialStore, undefined, logger.logger);
globalState.setDevice(selectedBackend, device);
} catch (e) { } catch (e) {
device?.dispose(); device?.dispose();
throw e; throw e;
@ -160,7 +161,7 @@ function _Connect(): JSX.Element | null {
const disconnect = useCallback(async () => { const disconnect = useCallback(async () => {
try { try {
await globalState.device!.dispose(); await globalState.device!.dispose();
globalState.setDevice(undefined); globalState.setDevice(undefined, undefined);
} catch (e: any) { } catch (e: any) {
globalState.showErrorDialog(e.message); globalState.showErrorDialog(e.message);
} }

View file

@ -1,5 +1,5 @@
import { IconButton, IListProps, List, mergeStyles, mergeStyleSets, Stack } from '@fluentui/react'; import { IconButton, IListProps, List, mergeStyles, mergeStyleSets, Stack } from '@fluentui/react';
import { AdbPacketInit, decodeUtf8 } from '@yume-chan/adb'; import { AdbPacketCore, decodeUtf8 } from '@yume-chan/adb';
import { DisposableList } from '@yume-chan/event'; import { DisposableList } from '@yume-chan/event';
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { PropsWithChildren, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { PropsWithChildren, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
@ -23,7 +23,7 @@ const classNames = mergeStyleSets({
}, },
}); });
function serializePacket(packet: AdbPacketInit) { function serializePacket(packet: AdbPacketCore) {
const command = decodeUtf8(new Uint32Array([packet.command])); const command = decodeUtf8(new Uint32Array([packet.command]));
const parts = [ const parts = [
@ -44,7 +44,7 @@ function serializePacket(packet: AdbPacketInit) {
return parts.join(' '); return parts.join(' ');
} }
const LogLine = withDisplayName('LoggerLine')(({ packet }: { packet: [string, AdbPacketInit]; }) => { const LogLine = withDisplayName('LoggerLine')(({ packet }: { packet: [string, AdbPacketCore]; }) => {
const string = useMemo(() => serializePacket(packet[1]), [packet]); const string = useMemo(() => serializePacket(packet[1]), [packet]);
return ( return (
@ -69,11 +69,11 @@ export interface LoggerProps {
className?: string; className?: string;
} }
function shouldVirtualize(props: IListProps<[string, AdbPacketInit]>) { function shouldVirtualize(props: IListProps<[string, AdbPacketCore]>) {
return !!props.items && props.items.length > 100; return !!props.items && props.items.length > 100;
} }
function renderCell(item?: [string, AdbPacketInit]) { function renderCell(item?: [string, AdbPacketCore]) {
if (!item) { if (!item) {
return null; return null;
} }
@ -86,7 +86,7 @@ function renderCell(item?: [string, AdbPacketInit]) {
export const LogView = observer(({ export const LogView = observer(({
className, className,
}: LoggerProps) => { }: LoggerProps) => {
const [packets, setPackets] = useState<[string, AdbPacketInit][]>([]); const [packets, setPackets] = useState<[string, AdbPacketCore][]>([]);
const scrollerRef = useRef<HTMLDivElement | null>(null); const scrollerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {

View file

@ -42,6 +42,6 @@
"@types/react": "17.0.27", "@types/react": "17.0.27",
"eslint": "8.8.0", "eslint": "8.8.0",
"eslint-config-next": "12.1.0", "eslint-config-next": "12.1.0",
"typescript": "^4.5.5" "typescript": "^4.6.2"
} }
} }

View file

@ -84,7 +84,7 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
const url = canvas.toDataURL(); const url = canvas.toDataURL();
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `Screenshot of ${globalState.device!.name}.png`; a.download = `Screenshot of ${globalState.backend!.name}.png`;
a.click(); a.click();
}, },
}, },

View file

@ -1,8 +1,11 @@
import { Adb } from "@yume-chan/adb"; import { Adb, AdbBackend } from "@yume-chan/adb";
import { action, makeAutoObservable } from 'mobx'; import { action, makeAutoObservable } from 'mobx';
export class GlobalState { export class GlobalState {
backend: AdbBackend | undefined = undefined;
device: Adb | undefined = undefined; device: Adb | undefined = undefined;
errorDialogVisible = false; errorDialogVisible = false;
errorDialogMessage = ''; errorDialogMessage = '';
@ -15,7 +18,8 @@ export class GlobalState {
}); });
} }
setDevice(device: Adb | undefined) { setDevice(backend: AdbBackend | undefined, device: Adb | undefined) {
this.backend = backend;
this.device = device; this.device = device;
} }

View file

@ -1,4 +1,4 @@
import { AdbLogger, AdbPacket, AdbPacketInit } from "@yume-chan/adb"; import { AdbLogger, AdbPacket, AdbPacketCore } from "@yume-chan/adb";
import { EventEmitter } from "@yume-chan/event"; import { EventEmitter } from "@yume-chan/event";
export class AdbEventLogger { export class AdbEventLogger {
@ -8,7 +8,7 @@ export class AdbEventLogger {
private readonly _incomingPacketEvent = new EventEmitter<AdbPacket>(); private readonly _incomingPacketEvent = new EventEmitter<AdbPacket>();
public get onIncomingPacket() { return this._incomingPacketEvent.event; } public get onIncomingPacket() { return this._incomingPacketEvent.event; }
private readonly _outgoingPacketEvent = new EventEmitter<AdbPacketInit>(); private readonly _outgoingPacketEvent = new EventEmitter<AdbPacketCore>();
public get onOutgoingPacket() { return this._outgoingPacketEvent.event; } public get onOutgoingPacket() { return this._outgoingPacketEvent.event; }
public constructor() { public constructor() {

View file

@ -3764,7 +3764,7 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@typescript-eslint/parser/5.12.0_eslint@8.8.0+typescript@4.5.5: /@typescript-eslint/parser/5.12.0_eslint@8.8.0+typescript@4.6.2:
resolution: {integrity: sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog==} resolution: {integrity: sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
@ -3776,10 +3776,10 @@ packages:
dependencies: dependencies:
'@typescript-eslint/scope-manager': 5.12.0 '@typescript-eslint/scope-manager': 5.12.0
'@typescript-eslint/types': 5.12.0 '@typescript-eslint/types': 5.12.0
'@typescript-eslint/typescript-estree': 5.12.0_typescript@4.5.5 '@typescript-eslint/typescript-estree': 5.12.0_typescript@4.6.2
debug: 4.3.3 debug: 4.3.3
eslint: 8.8.0 eslint: 8.8.0
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: false dev: false
@ -3817,7 +3817,7 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@typescript-eslint/typescript-estree/5.12.0_typescript@4.5.5: /@typescript-eslint/typescript-estree/5.12.0_typescript@4.6.2:
resolution: {integrity: sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ==} resolution: {integrity: sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
@ -3832,8 +3832,8 @@ packages:
globby: 11.1.0 globby: 11.1.0
is-glob: 4.0.3 is-glob: 4.0.3
semver: 7.3.5 semver: 7.3.5
tsutils: 3.21.0_typescript@4.5.5 tsutils: 3.21.0_typescript@4.6.2
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: false dev: false
@ -6048,7 +6048,7 @@ packages:
source-map: 0.6.1 source-map: 0.6.1
dev: false dev: false
/eslint-config-next/12.1.0_a833109067da92148152ff2f5707ab26: /eslint-config-next/12.1.0_84a9f7b24d99c162ec5512424f921235:
resolution: {integrity: sha512-tBhuUgoDITcdcM7xFvensi9I5WTI4dnvH4ETGRg1U8ZKpXrZsWQFdOKIDzR3RLP5HR3xXrLviaMM4c3zVoE/pA==} resolution: {integrity: sha512-tBhuUgoDITcdcM7xFvensi9I5WTI4dnvH4ETGRg1U8ZKpXrZsWQFdOKIDzR3RLP5HR3xXrLviaMM4c3zVoE/pA==}
peerDependencies: peerDependencies:
eslint: ^7.23.0 || ^8.0.0 eslint: ^7.23.0 || ^8.0.0
@ -6060,7 +6060,7 @@ packages:
dependencies: dependencies:
'@next/eslint-plugin-next': 12.1.0 '@next/eslint-plugin-next': 12.1.0
'@rushstack/eslint-patch': 1.1.0 '@rushstack/eslint-patch': 1.1.0
'@typescript-eslint/parser': 5.12.0_eslint@8.8.0+typescript@4.5.5 '@typescript-eslint/parser': 5.12.0_eslint@8.8.0+typescript@4.6.2
eslint: 8.8.0 eslint: 8.8.0
eslint-import-resolver-node: 0.3.6 eslint-import-resolver-node: 0.3.6
eslint-import-resolver-typescript: 2.5.0_392f898cec7735a5f7a99430cbc0b4f4 eslint-import-resolver-typescript: 2.5.0_392f898cec7735a5f7a99430cbc0b4f4
@ -6069,7 +6069,7 @@ packages:
eslint-plugin-react: 7.28.0_eslint@8.8.0 eslint-plugin-react: 7.28.0_eslint@8.8.0
eslint-plugin-react-hooks: 4.3.0_eslint@8.8.0 eslint-plugin-react-hooks: 4.3.0_eslint@8.8.0
next: 12.1.0_react-dom@17.0.2+react@17.0.2 next: 12.1.0_react-dom@17.0.2+react@17.0.2
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: false dev: false
@ -12269,14 +12269,14 @@ packages:
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/tsutils/3.21.0_typescript@4.5.5: /tsutils/3.21.0_typescript@4.6.2:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
peerDependencies: peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies: dependencies:
tslib: 1.14.1 tslib: 1.14.1
typescript: 4.5.5 typescript: 4.6.2
dev: false dev: false
/type-check/0.3.2: /type-check/0.3.2:
@ -12332,8 +12332,8 @@ packages:
is-typedarray: 1.0.0 is-typedarray: 1.0.0
dev: false dev: false
/typescript/4.5.5: /typescript/4.6.2:
resolution: {integrity: sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==} resolution: {integrity: sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==}
engines: {node: '>=4.2.0'} engines: {node: '>=4.2.0'}
hasBin: true hasBin: true
dev: false dev: false
@ -13199,24 +13199,24 @@ packages:
dev: false dev: false
file:projects/adb-backend-direct-sockets.tgz: file:projects/adb-backend-direct-sockets.tgz:
resolution: {integrity: sha512-clMC9xKIUpsXhBxa22pfc+xZAuPgzTLDPc+9gDGeQfpVVJDMq+Sug51Q3hWSHzjLkqFOB0MF6Ekz7Ggl05FH7A==, tarball: file:projects/adb-backend-direct-sockets.tgz} resolution: {integrity: sha512-7lYtoXDESytDSbVwRclsL5Zei0SyHDrla2MDnOylREFhq9k+jD5y4QlGlp5BA7i8jxCcFO4X+ycdjeo03LgRNg==, tarball: file:projects/adb-backend-direct-sockets.tgz}
name: '@rush-temp/adb-backend-direct-sockets' name: '@rush-temp/adb-backend-direct-sockets'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@types/jest': 27.4.0 '@types/jest': 27.4.0
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
dev: false dev: false
file:projects/adb-backend-webusb.tgz: file:projects/adb-backend-webusb.tgz:
resolution: {integrity: sha512-uxXSrxrQ1mZ4GRMuZ6hlcZjv4L40zy9rYQ3d+XR47rVgLk3vAbyheenAGxSN6o5GcJnH4FFw0T1DXJEnraNm9g==, tarball: file:projects/adb-backend-webusb.tgz} resolution: {integrity: sha512-hLTGiBjXJCtaLLQGVVCSSQF27mpAXhWVPqkZv/pXm+6Gnvl+AeTtwZzI3gBcWLRKZt8kNqG7ZPPoWk+TV+ZLow==, tarball: file:projects/adb-backend-webusb.tgz}
name: '@rush-temp/adb-backend-webusb' name: '@rush-temp/adb-backend-webusb'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@types/w3c-web-usb': 1.0.5 '@types/w3c-web-usb': 1.0.5
jest: 26.6.3 jest: 26.6.3
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- canvas - canvas
@ -13226,13 +13226,13 @@ packages:
dev: false dev: false
file:projects/adb-backend-ws.tgz: file:projects/adb-backend-ws.tgz:
resolution: {integrity: sha512-lHi6rENCAPuUaKIwKPsrZRhif+znoMleIoQu/5u5SZsVal6jPmk9boeB4gbPRACNIpNqYZJ1ptRAtJKU9kUpdQ==, tarball: file:projects/adb-backend-ws.tgz} resolution: {integrity: sha512-gYyPR7NoztGZMkMMApsw/n4hjU2mXpW0Q5ZytAendhOjxFhlgVuzUNVIseEeCZ5WcKZJq8UDcbm3t1XzjNnspg==, tarball: file:projects/adb-backend-ws.tgz}
name: '@rush-temp/adb-backend-ws' name: '@rush-temp/adb-backend-ws'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
jest: 26.6.3 jest: 26.6.3
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- canvas - canvas
@ -13242,23 +13242,23 @@ packages:
dev: false dev: false
file:projects/adb-credential-web.tgz: file:projects/adb-credential-web.tgz:
resolution: {integrity: sha512-ovbVsEARMBPtPv/85e3cUh5VtHdqw4DQaZ/IwGEOmUmx5O6kN/uMRB5oG++g4/Bam+A+/HhY9WPjFsX5W3cKlg==, tarball: file:projects/adb-credential-web.tgz} resolution: {integrity: sha512-ScBUE3flU6eg6FvrtT9WtXbzebLkWF3gktNBTRXXzccRNzkHeERHGEWV6+9ikmUUV76q+Hyn36dY1p81mWlf1w==, tarball: file:projects/adb-credential-web.tgz}
name: '@rush-temp/adb-credential-web' name: '@rush-temp/adb-credential-web'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
dev: false dev: false
file:projects/adb.tgz: file:projects/adb.tgz:
resolution: {integrity: sha512-ERJiC9hMtOFRZhGBqQ76HXFVsjsGHajyWrPoHqOrAW4LqTdHf8jJueqhzPt6e9asmTuxcOynqz4MzbYnahcTtA==, tarball: file:projects/adb.tgz} resolution: {integrity: sha512-5M3XhdG6BqRVn0LyI3z6Ig7qOFQwVJyH17fkNq+1cVYVALZHg2Q8MquZcY76GUfo7XR2Fl7Eiw5zNRBOkgUgbA==, tarball: file:projects/adb.tgz}
name: '@rush-temp/adb' name: '@rush-temp/adb'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@yume-chan/async': 2.1.4 '@yume-chan/async': 2.1.4
jest: 26.6.3 jest: 26.6.3
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- canvas - canvas
@ -13268,22 +13268,22 @@ packages:
dev: false dev: false
file:projects/android-bin.tgz: file:projects/android-bin.tgz:
resolution: {integrity: sha512-+POm1mf4P1s+NhgwA6IHN+GBBmc00lDi7k+geCc04ts836duRlnJfkIEeIIJmjQHVwDz36BONp8rCXAaX4U/fA==, tarball: file:projects/android-bin.tgz} resolution: {integrity: sha512-FF55Xz9NTN3Og0oFF2ya/NBJb0Uz1x/kwJEE2cI93UqtHdJQ/eMwMm1mUcOxUcdIOkriy3eVCvVd1WWpv8Eqnw==, tarball: file:projects/android-bin.tgz}
name: '@rush-temp/android-bin' name: '@rush-temp/android-bin'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
dev: false dev: false
file:projects/dataview-bigint-polyfill.tgz: file:projects/dataview-bigint-polyfill.tgz:
resolution: {integrity: sha512-tlwAp44MyiGGyikI+6kFaCkVQjkpKmlMPm0hm6ts97uydxfdNgMnoGp0JW4J4WDvFOjforF3Z1+kfvBTVDHv1w==, tarball: file:projects/dataview-bigint-polyfill.tgz} resolution: {integrity: sha512-lbBWjYYtVRQA5JWUloB96WIAXPAIsUQKUqqSoETJdsw+jbea3glhdHvLUxQAc0TjxF/rQk0g7dhF1jOtcmkS/A==, tarball: file:projects/dataview-bigint-polyfill.tgz}
name: '@rush-temp/dataview-bigint-polyfill' name: '@rush-temp/dataview-bigint-polyfill'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
jest: 26.6.3 jest: 26.6.3
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- canvas - canvas
@ -13293,7 +13293,7 @@ packages:
dev: false dev: false
file:projects/demo.tgz_@mdx-js+react@1.6.22: file:projects/demo.tgz_@mdx-js+react@1.6.22:
resolution: {integrity: sha512-Rjd2nSA/9Gu41TKCWSKZM9bMVXqUFoWr30etKv07GGFP1NVkM8sWcH9bPezqiQxXRcZ5eMXet47PSS6Db8Qmqg==, tarball: file:projects/demo.tgz} resolution: {integrity: sha512-fb7TRHuIGkcJHZRpdp76shGm+70hVRjUcr2WWD7d+PbK0aQUPvDi7loy9XCdyI3deTF0APfR44agm1irnymRAw==, tarball: file:projects/demo.tgz}
id: file:projects/demo.tgz id: file:projects/demo.tgz
name: '@rush-temp/demo' name: '@rush-temp/demo'
version: 0.0.0 version: 0.0.0
@ -13308,14 +13308,14 @@ packages:
'@types/react': 17.0.27 '@types/react': 17.0.27
'@yume-chan/async': 2.1.4 '@yume-chan/async': 2.1.4
eslint: 8.8.0 eslint: 8.8.0
eslint-config-next: 12.1.0_a833109067da92148152ff2f5707ab26 eslint-config-next: 12.1.0_84a9f7b24d99c162ec5512424f921235
mobx: 6.3.13 mobx: 6.3.13
mobx-react-lite: 3.2.3_96b0034b8b6bfac1b4cc60715db17f02 mobx-react-lite: 3.2.3_96b0034b8b6bfac1b4cc60715db17f02
next: 12.1.0_react-dom@17.0.2+react@17.0.2 next: 12.1.0_react-dom@17.0.2+react@17.0.2
react: 17.0.2 react: 17.0.2
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
streamsaver: 2.0.6 streamsaver: 2.0.6
typescript: 4.5.5 typescript: 4.6.2
xterm: 4.17.0 xterm: 4.17.0
xterm-addon-fit: 0.5.0_xterm@4.17.0 xterm-addon-fit: 0.5.0_xterm@4.17.0
xterm-addon-search: 0.8.2_xterm@4.17.0 xterm-addon-search: 0.8.2_xterm@4.17.0
@ -13333,14 +13333,14 @@ packages:
dev: false dev: false
file:projects/event.tgz: file:projects/event.tgz:
resolution: {integrity: sha512-eEBPgCD8YhbmkfWMY1Nrb7gijGq8+v0zayohQ4um92OzdQbm04nRS+TAD6JSP0YpH3pvKxUoe9QBVOAr98Ltwg==, tarball: file:projects/event.tgz} resolution: {integrity: sha512-/nNG2wtotQQEddux5DQGXRTMa3h9X0y/IEn3jj07hsd/lb1DZz4EhXDaiuFHgxgYC9Vlt78yqcmI6vq3BynbMQ==, tarball: file:projects/event.tgz}
name: '@rush-temp/event' name: '@rush-temp/event'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@yume-chan/async': 2.1.4 '@yume-chan/async': 2.1.4
jest: 26.6.3 jest: 26.6.3
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- canvas - canvas
@ -13350,7 +13350,7 @@ packages:
dev: false dev: false
file:projects/scrcpy.tgz: file:projects/scrcpy.tgz:
resolution: {integrity: sha512-yOKNFXcflPuF8A0tA0dLEe4a6FS1ofKExq9AP/BruIYp9CG15JjNA8FI0UQ7zuJaSjCnGDjFzFnMF4tmSWfhrA==, tarball: file:projects/scrcpy.tgz} resolution: {integrity: sha512-t5PfOgkF6otyHa83xANbVLVXoWPqh+5V2BS0NW4LDY9wvOww9BEr4RW/LRwuP9WszHZZ8pQKApBomeu+fon6Yw==, tarball: file:projects/scrcpy.tgz}
name: '@rush-temp/scrcpy' name: '@rush-temp/scrcpy'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
@ -13361,7 +13361,7 @@ packages:
jest: 26.6.3 jest: 26.6.3
tinyh264: 0.0.7 tinyh264: 0.0.7
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
yuv-buffer: 1.0.0 yuv-buffer: 1.0.0
yuv-canvas: 1.2.9 yuv-canvas: 1.2.9
transitivePeerDependencies: transitivePeerDependencies:
@ -13374,7 +13374,7 @@ packages:
dev: false dev: false
file:projects/struct.tgz: file:projects/struct.tgz:
resolution: {integrity: sha512-UuEJ78bVrGthdhMJa0kKI/dB7tprMDcMEaZ33H+A7ISjPz5U1bfYIXE8IpVUiNBmoKeT/Ce46+isU9cSf2Mzdg==, tarball: file:projects/struct.tgz} resolution: {integrity: sha512-0FfQNcq/N527LPz/UOQ6get3JHRTsWkqg25yvF5iaCAYXpjdgIREQSGGUxru0+oSMaTl76cGEtSrbrOCa+FrWA==, tarball: file:projects/struct.tgz}
name: '@rush-temp/struct' name: '@rush-temp/struct'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
@ -13383,7 +13383,7 @@ packages:
bluebird: 3.7.2 bluebird: 3.7.2
jest: 26.6.3 jest: 26.6.3
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- canvas - canvas
@ -13400,7 +13400,7 @@ packages:
'@types/jest': 27.4.0 '@types/jest': 27.4.0
'@types/node': 17.0.18 '@types/node': 17.0.18
json5: 2.2.0 json5: 2.2.0
typescript: 4.5.5 typescript: 4.6.2
dev: false dev: false
file:projects/unofficial-adb-book.tgz_814045a8852c08eeea7e09b05a06dede: file:projects/unofficial-adb-book.tgz_814045a8852c08eeea7e09b05a06dede:

View file

@ -31,7 +31,7 @@
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@types/jest": "^27.4.0", "@types/jest": "^27.4.0",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
}, },

View file

@ -37,7 +37,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^26.6.3", "jest": "^26.6.3",
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
} }
} }

View file

@ -31,7 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^26.6.3", "jest": "^26.6.3",
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
}, },
"dependencies": { "dependencies": {

View file

@ -29,7 +29,7 @@
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
}, },
"dependencies": { "dependencies": {

View file

@ -39,7 +39,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^26.6.3", "jest": "^26.6.3",
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
} }
} }

View file

@ -1,13 +1,11 @@
import { PromiseResolver } from '@yume-chan/async'; import { PromiseResolver } from '@yume-chan/async';
import { DisposableList } from '@yume-chan/event';
import { AdbAuthenticationHandler, AdbCredentialStore, AdbDefaultAuthenticators } from './auth'; import { AdbAuthenticationHandler, AdbCredentialStore, AdbDefaultAuthenticators } from './auth';
import { AdbBackend } from './backend';
import { AdbFrameBuffer, AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install } from './commands'; import { AdbFrameBuffer, AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install } from './commands';
import { AdbFeatures } from './features'; import { AdbFeatures } from './features';
import { AdbCommand } from './packet'; import { AdbCommand, AdbPacket, AdbPacketCore, AdbPacketInit, AdbPacketSerializeStream, calculateChecksum } from './packet';
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket'; import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
import { DecodeUtf8Stream, GatherStringStream, ReadableStream, WritableStream } from "./stream"; import { AbortController, DecodeUtf8Stream, GatherStringStream, pipeFrom, ReadableWritablePair, StructDeserializeStream, WritableStream } from "./stream";
import { decodeUtf8 } from "./utils"; import { decodeUtf8, encodeUtf8 } from "./utils";
export enum AdbPropKey { export enum AdbPropKey {
Product = 'ro.product.name', Product = 'ro.product.name',
@ -16,20 +14,132 @@ export enum AdbPropKey {
Features = 'features', Features = 'features',
} }
export const VERSION_OMIT_CHECKSUM = 0x01000001;
export class Adb { export class Adb {
public static async connect(backend: AdbBackend, logger?: AdbLogger) { public static createConnection(
const { readable, writable } = await backend.connect(); connection: ReadableWritablePair<Uint8Array, Uint8Array>
return new Adb(backend, readable, writable, logger); ): ReadableWritablePair<AdbPacket, AdbPacketCore> {
return {
readable: connection.readable.pipeThrough(
new StructDeserializeStream(AdbPacket)
),
writable: pipeFrom(
connection.writable,
new AdbPacketSerializeStream()
),
};
} }
private readonly _backend: AdbBackend; /**
* It's possible to call `authenticate` multiple times on a single connection,
* every time the device receives a `CNXN` packet it will reset its internal state,
* and begin authentication again.
*/
public static async authenticate(
connection: ReadableWritablePair<AdbPacket, AdbPacketCore>,
credentialStore: AdbCredentialStore,
authenticators = AdbDefaultAuthenticators,
logger?: AdbLogger
) {
let version = 0x01000001;
let maxPayloadSize = 0x100000;
public get backend(): AdbBackend { return this._backend; } const features = [
'shell_v2',
'cmd',
AdbFeatures.StatV2,
'ls_v2',
'fixed_push_mkdir',
'apex',
'abb',
'fixed_push_symlink_timestamp',
'abb_exec',
'remount_shell',
'track_app',
'sendrecv_v2',
'sendrecv_v2_brotli',
'sendrecv_v2_lz4',
'sendrecv_v2_zstd',
'sendrecv_v2_dry_run_send',
].join(',');
const resolver = new PromiseResolver<string>();
const authHandler = new AdbAuthenticationHandler(authenticators, credentialStore);
const abortController = new AbortController();
const pipe = connection.readable
.pipeTo(new WritableStream({
async write(packet: AdbPacket) {
logger?.onIncomingPacket?.(packet);
switch (packet.command) {
case AdbCommand.Connect:
version = Math.min(version, packet.arg0);
maxPayloadSize = Math.min(maxPayloadSize, packet.arg1);
resolver.resolve(decodeUtf8(packet.payload));
break;
case AdbCommand.Auth:
const response = await authHandler.handle(packet);
await sendPacket(response);
break;
case AdbCommand.Close:
// Last connection was interrupted
// Ignore this packet, device will recover
break;
default:
throw new Error('Device not in correct state. Reconnect your device and try again');
}
}
}), {
preventCancel: true,
signal: abortController.signal,
})
.catch((e) => { resolver.reject(e); });
const writer = connection.writable.getWriter();
async function sendPacket(init: AdbPacketCore) {
logger?.onOutgoingPacket?.(init);
// Always send checksum in auth steps
// Because we don't know if the device will ignore it yet.
await writer.write(calculateChecksum(init));
}
await sendPacket({
command: AdbCommand.Connect,
arg0: version,
arg1: maxPayloadSize,
// The terminating `;` is required in formal definition
// But ADB daemon (all versions) can still work without it
payload: encodeUtf8(`host::features=${features};`),
});
try {
const banner = await resolver.promise;
// Stop piping before creating Adb object
// Because AdbPacketDispatcher will try to lock the streams when initializing
abortController.abort();
await pipe;
writer.releaseLock();
return new Adb(
connection,
version,
maxPayloadSize,
banner,
logger
);
} finally {
abortController.abort();
writer.releaseLock();
}
}
private readonly packetDispatcher: AdbPacketDispatcher; private readonly packetDispatcher: AdbPacketDispatcher;
public get name() { return this.backend.name; }
private _protocolVersion: number | undefined; private _protocolVersion: number | undefined;
public get protocolVersion() { return this._protocolVersion; } public get protocolVersion() { return this._protocolVersion; }
@ -51,13 +161,23 @@ export class Adb {
public readonly tcpip: AdbTcpIpCommand; public readonly tcpip: AdbTcpIpCommand;
public constructor( public constructor(
backend: AdbBackend, connection: ReadableWritablePair<AdbPacket, AdbPacketInit>,
readable: ReadableStream<Uint8Array>, version: number,
writable: WritableStream<Uint8Array>, maxPayloadSize: number,
banner: string,
logger?: AdbLogger logger?: AdbLogger
) { ) {
this._backend = backend; this.parseBanner(banner);
this.packetDispatcher = new AdbPacketDispatcher(readable, writable, logger); this.packetDispatcher = new AdbPacketDispatcher(connection, logger);
this._protocolVersion = version;
if (version >= VERSION_OMIT_CHECKSUM) {
this.packetDispatcher.calculateChecksum = false;
// Android prior to 9.0.0 uses char* to parse service string
// thus requires an extra null character
this.packetDispatcher.appendNullToServiceString = false;
}
this.packetDispatcher.maxPayloadSize = maxPayloadSize;
this.subprocess = new AdbSubprocess(this); this.subprocess = new AdbSubprocess(this);
this.power = new AdbPower(this); this.power = new AdbPower(this);
@ -65,98 +185,6 @@ export class Adb {
this.tcpip = new AdbTcpIpCommand(this); this.tcpip = new AdbTcpIpCommand(this);
} }
public async authenticate(
credentialStore: AdbCredentialStore,
authenticators = AdbDefaultAuthenticators
): Promise<void> {
this.packetDispatcher.maxPayloadSize = 0x1000;
this.packetDispatcher.calculateChecksum = true;
this.packetDispatcher.appendNullToServiceString = true;
const version = 0x01000001;
const versionNoChecksum = 0x01000001;
const maxPayloadSize = 0x100000;
const features = [
'shell_v2',
'cmd',
AdbFeatures.StatV2,
'ls_v2',
'fixed_push_mkdir',
'apex',
'abb',
'fixed_push_symlink_timestamp',
'abb_exec',
'remount_shell',
'track_app',
'sendrecv_v2',
'sendrecv_v2_brotli',
'sendrecv_v2_lz4',
'sendrecv_v2_zstd',
'sendrecv_v2_dry_run_send',
].join(',');
const resolver = new PromiseResolver<void>();
const authHandler = new AdbAuthenticationHandler(authenticators, credentialStore);
const disposableList = new DisposableList();
disposableList.add(this.packetDispatcher.onPacket(async (e) => {
e.handled = true;
const { packet } = e;
try {
switch (packet.command) {
case AdbCommand.Connect:
this.packetDispatcher.maxPayloadSize = Math.min(maxPayloadSize, packet.arg1);
const finalVersion = Math.min(version, packet.arg0);
this._protocolVersion = finalVersion;
if (finalVersion >= versionNoChecksum) {
this.packetDispatcher.calculateChecksum = false;
// Android prior to 9.0.0 uses char* to parse service string
// thus requires an extra null character
this.packetDispatcher.appendNullToServiceString = false;
}
this.parseBanner(decodeUtf8(packet.payload!));
resolver.resolve();
break;
case AdbCommand.Auth:
const authPacket = await authHandler.handle(e.packet);
await this.packetDispatcher.sendPacket(authPacket);
break;
case AdbCommand.Close:
// Last connection was interrupted
// Ignore this packet, device will recover
break;
default:
throw new Error('Device not in correct state. Reconnect your device and try again');
}
} catch (e) {
resolver.reject(e);
}
}));
disposableList.add(this.packetDispatcher.onError(e => {
resolver.reject(e);
}));
await this.packetDispatcher.sendPacket(
AdbCommand.Connect,
version,
maxPayloadSize,
// The terminating `;` is required in formal definition
// But ADB daemon can also work without it
`host::features=${features};`
);
try {
await resolver.promise;
} finally {
disposableList.dispose();
}
}
private parseBanner(banner: string): void { private parseBanner(banner: string): void {
this._features = []; this._features = [];

View file

@ -2,7 +2,7 @@ import { PromiseResolver } from '@yume-chan/async';
import { Disposable } from '@yume-chan/event'; import { Disposable } from '@yume-chan/event';
import { ValueOrPromise } from '@yume-chan/struct'; import { ValueOrPromise } from '@yume-chan/struct';
import { calculatePublicKey, calculatePublicKeyLength, sign } from './crypto'; import { calculatePublicKey, calculatePublicKeyLength, sign } from './crypto';
import { AdbCommand, AdbPacket, AdbPacketInit } from './packet'; import { AdbCommand, AdbPacket, AdbPacketCore } from './packet';
import { calculateBase64EncodedLength, encodeBase64 } from './utils'; import { calculateBase64EncodedLength, encodeBase64 } from './utils';
export type AdbKeyIterable = Iterable<Uint8Array> | AsyncIterable<Uint8Array>; export type AdbKeyIterable = Iterable<Uint8Array> | AsyncIterable<Uint8Array>;
@ -44,13 +44,13 @@ export interface AdbAuthenticator {
( (
credentialStore: AdbCredentialStore, credentialStore: AdbCredentialStore,
getNextRequest: () => Promise<AdbPacket> getNextRequest: () => Promise<AdbPacket>
): AsyncIterable<AdbPacketInit>; ): AsyncIterable<AdbPacketCore>;
} }
export const AdbSignatureAuthenticator: AdbAuthenticator = async function* ( export const AdbSignatureAuthenticator: AdbAuthenticator = async function* (
credentialStore: AdbCredentialStore, credentialStore: AdbCredentialStore,
getNextRequest: () => Promise<AdbPacket>, getNextRequest: () => Promise<AdbPacket>,
): AsyncIterable<AdbPacketInit> { ): AsyncIterable<AdbPacketCore> {
for await (const key of credentialStore.iterateKeys()) { for await (const key of credentialStore.iterateKeys()) {
const packet = await getNextRequest(); const packet = await getNextRequest();
@ -58,7 +58,7 @@ export const AdbSignatureAuthenticator: AdbAuthenticator = async function* (
return; return;
} }
const signature = sign(key, packet.payload!); const signature = sign(key, packet.payload);
yield { yield {
command: AdbCommand.Auth, command: AdbCommand.Auth,
arg0: AdbAuthType.Signature, arg0: AdbAuthType.Signature,
@ -71,7 +71,7 @@ export const AdbSignatureAuthenticator: AdbAuthenticator = async function* (
export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* ( export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
credentialStore: AdbCredentialStore, credentialStore: AdbCredentialStore,
getNextRequest: () => Promise<AdbPacket>, getNextRequest: () => Promise<AdbPacket>,
): AsyncIterable<AdbPacketInit> { ): AsyncIterable<AdbPacketCore> {
const packet = await getNextRequest(); const packet = await getNextRequest();
if (packet.arg0 !== AdbAuthType.Token) { if (packet.arg0 !== AdbAuthType.Token) {
@ -119,7 +119,7 @@ export class AdbAuthenticationHandler implements Disposable {
private pendingRequest = new PromiseResolver<AdbPacket>(); private pendingRequest = new PromiseResolver<AdbPacket>();
private iterator: AsyncIterator<AdbPacketInit> | undefined; private iterator: AsyncIterator<AdbPacketCore> | undefined;
public constructor( public constructor(
authenticators: readonly AdbAuthenticator[], authenticators: readonly AdbAuthenticator[],
@ -133,7 +133,7 @@ export class AdbAuthenticationHandler implements Disposable {
return this.pendingRequest.promise; return this.pendingRequest.promise;
}; };
private async* runAuthenticator(): AsyncGenerator<AdbPacketInit> { private async* runAuthenticator(): AsyncGenerator<AdbPacketCore> {
for (const authenticator of this.authenticators) { for (const authenticator of this.authenticators) {
for await (const packet of authenticator(this.credentialStore, this.getNextRequest)) { for await (const packet of authenticator(this.credentialStore, this.getNextRequest)) {
// If the authenticator yielded a response // If the authenticator yielded a response
@ -151,7 +151,7 @@ export class AdbAuthenticationHandler implements Disposable {
throw new Error('Cannot authenticate with device'); throw new Error('Cannot authenticate with device');
} }
public async handle(packet: AdbPacket): Promise<AdbPacketInit> { public async handle(packet: AdbPacket): Promise<AdbPacketCore> {
if (!this.iterator) { if (!this.iterator) {
this.iterator = this.runAuthenticator(); this.iterator = this.runAuthenticator();
} }

View file

@ -52,7 +52,7 @@ export class AdbReverseCommand extends AutoDisposable {
return; return;
} }
const address = decodeUtf8(e.packet.payload!); const address = decodeUtf8(e.packet.payload);
// Address format: `tcp:12345\0` // Address format: `tcp:12345\0`
const port = Number.parseInt(address.substring(4)); const port = Number.parseInt(address.substring(4));
if (this.localPortToHandler.has(port)) { if (this.localPortToHandler.has(port)) {

View file

@ -19,6 +19,8 @@ const AdbPacketHeader =
.uint32('checksum') .uint32('checksum')
.int32('magic'); .int32('magic');
type AdbPacketHeaderInit = typeof AdbPacketHeader['TInit'];
export const AdbPacket = export const AdbPacket =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.fields(AdbPacketHeader) .fields(AdbPacketHeader)
@ -26,34 +28,40 @@ export const AdbPacket =
export type AdbPacket = typeof AdbPacket['TDeserializeResult']; export type AdbPacket = typeof AdbPacket['TDeserializeResult'];
export type AdbPacketInit = Omit<typeof AdbPacket['TInit'], 'checksum' | 'magic'>; // All the useful fields
export type AdbPacketCore = Omit<typeof AdbPacket['TInit'], 'checksum' | 'magic'>;
// All fields except `magic`, which can be calculated in `AdbPacketSerializeStream`
export type AdbPacketInit = Omit<typeof AdbPacket['TInit'], 'magic'>;
export function calculateChecksum(payload: Uint8Array): number;
export function calculateChecksum(init: AdbPacketCore): AdbPacketInit;
export function calculateChecksum(payload: Uint8Array | AdbPacketCore): number | AdbPacketInit {
if (payload instanceof Uint8Array) {
return payload.reduce((result, item) => result + item, 0);
} else {
(payload as AdbPacketInit).checksum = calculateChecksum(payload.payload);
return payload as AdbPacketInit;
}
}
export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, Uint8Array>{ export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, Uint8Array>{
public calculateChecksum = true;
public constructor() { public constructor() {
super({ super({
transform: async (init, controller) => { transform: async (init, controller) => {
let checksum: number; // This syntax is ugly, but I don't want to create an new object.
if (this.calculateChecksum && init.payload) { (init as unknown as AdbPacketHeaderInit).magic = init.command ^ 0xFFFFFFFF;
const array = init.payload; (init as unknown as AdbPacketHeaderInit).payloadLength = init.payload.byteLength;
checksum = array.reduce((result, item) => result + item, 0);
} else {
checksum = 0;
}
const packet = { controller.enqueue(
...init, AdbPacketHeader.serialize(
checksum, init as unknown as AdbPacketHeaderInit
magic: init.command ^ 0xFFFFFFFF, )
payloadLength: init.payload.byteLength, );
};
controller.enqueue(AdbPacketHeader.serialize(packet)); if (init.payload.byteLength) {
if (packet.payloadLength) {
// Enqueue payload separately to avoid copying // Enqueue payload separately to avoid copying
controller.enqueue(packet.payload); controller.enqueue(init.payload);
} }
}, },
}); });

View file

@ -1,7 +1,7 @@
import { AsyncOperationManager } from '@yume-chan/async'; import { AsyncOperationManager } from '@yume-chan/async';
import { AutoDisposable, EventEmitter } from '@yume-chan/event'; import { AutoDisposable, EventEmitter } from '@yume-chan/event';
import { AdbCommand, AdbPacket, AdbPacketInit, AdbPacketSerializeStream } from '../packet'; import { AdbCommand, AdbPacket, AdbPacketCore, AdbPacketInit, calculateChecksum } from '../packet';
import { AbortController, ReadableStream, StructDeserializeStream, WritableStream, WritableStreamDefaultWriter } from '../stream'; import { AbortController, ReadableWritablePair, WritableStream, WritableStreamDefaultWriter } from '../stream';
import { decodeUtf8, encodeUtf8 } from '../utils'; import { decodeUtf8, encodeUtf8 } from '../utils';
import { AdbSocketController } from './controller'; import { AdbSocketController } from './controller';
import { AdbLogger } from './logger'; import { AdbLogger } from './logger';
@ -32,12 +32,10 @@ export class AdbPacketDispatcher extends AutoDisposable {
private readonly sockets = new Map<number, AdbSocketController>(); private readonly sockets = new Map<number, AdbSocketController>();
private readonly logger: AdbLogger | undefined; private readonly logger: AdbLogger | undefined;
private _packetSerializeStream!: AdbPacketSerializeStream;
private _packetSerializeStreamWriter!: WritableStreamDefaultWriter<AdbPacketInit>; private _packetSerializeStreamWriter!: WritableStreamDefaultWriter<AdbPacketInit>;
public maxPayloadSize = 0; public maxPayloadSize = 0;
public get calculateChecksum() { return this._packetSerializeStream.calculateChecksum; } public calculateChecksum = true;
public set calculateChecksum(value: boolean) { this._packetSerializeStream.calculateChecksum = value; }
public appendNullToServiceString = true; public appendNullToServiceString = true;
private readonly packetEvent = this.addDisposable(new EventEmitter<AdbPacketReceivedEventArgs>()); private readonly packetEvent = this.addDisposable(new EventEmitter<AdbPacketReceivedEventArgs>());
@ -51,17 +49,16 @@ export class AdbPacketDispatcher extends AutoDisposable {
private _abortController = new AbortController(); private _abortController = new AbortController();
public constructor(readable: ReadableStream<Uint8Array>, writable: WritableStream<Uint8Array>, logger?: AdbLogger) { public constructor(
connection: ReadableWritablePair<AdbPacket, AdbPacketInit>,
logger?: AdbLogger
) {
super(); super();
this.logger = logger; this.logger = logger;
readable connection.readable
.pipeThrough( .pipeTo(new WritableStream({
new StructDeserializeStream(AdbPacket),
{ signal: this._abortController.signal, preventCancel: true }
)
.pipeTo(new WritableStream<AdbPacket>({
write: async (packet) => { write: async (packet) => {
try { try {
this.logger?.onIncomingPacket?.(packet); this.logger?.onIncomingPacket?.(packet);
@ -75,7 +72,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
return; return;
case AdbCommand.Write: case AdbCommand.Write:
if (this.sockets.has(packet.arg1)) { if (this.sockets.has(packet.arg1)) {
await this.sockets.get(packet.arg1)!.enqueue(packet.payload!); await this.sockets.get(packet.arg1)!.enqueue(packet.payload);
await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0); await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0);
} }
@ -106,17 +103,13 @@ export class AdbPacketDispatcher extends AutoDisposable {
throw e; throw e;
} }
} }
})) }), {
preventCancel: false,
signal: this._abortController.signal,
})
.catch(() => { }); .catch(() => { });
this._packetSerializeStream = new AdbPacketSerializeStream(); this._packetSerializeStreamWriter = connection.writable.getWriter();
this._packetSerializeStream.readable
.pipeTo(
writable,
{ signal: this._abortController.signal, preventClose: true }
)
.catch(() => { });;
this._packetSerializeStreamWriter = this._packetSerializeStream.writable.getWriter();
} }
private handleOk(packet: AdbPacket) { private handleOk(packet: AdbPacket) {
@ -174,7 +167,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
this.initializers.resolve(localId, undefined); this.initializers.resolve(localId, undefined);
const remoteId = packet.arg0; const remoteId = packet.arg0;
const serviceString = decodeUtf8(packet.payload!); const serviceString = decodeUtf8(packet.payload);
const controller = new AdbSocketController({ const controller = new AdbSocketController({
dispatcher: this, dispatcher: this,
@ -235,7 +228,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
arg1?: number, arg1?: number,
payload: string | Uint8Array = EmptyUint8Array, payload: string | Uint8Array = EmptyUint8Array,
): Promise<void> { ): Promise<void> {
let init: AdbPacketInit; let init: AdbPacketCore;
if (arg0 === undefined) { if (arg0 === undefined) {
init = packetOrCommand as AdbPacketInit; init = packetOrCommand as AdbPacketInit;
} else { } else {
@ -256,8 +249,14 @@ export class AdbPacketDispatcher extends AutoDisposable {
throw new Error('payload too large'); throw new Error('payload too large');
} }
if (this.calculateChecksum) {
calculateChecksum(init);
} else {
(init as AdbPacketInit).checksum = 0;
}
this.logger?.onOutgoingPacket?.(init); this.logger?.onOutgoingPacket?.(init);
await this._packetSerializeStreamWriter.write(init); await this._packetSerializeStreamWriter.write(init as AdbPacketInit);
} }
public override dispose() { public override dispose() {
@ -271,6 +270,8 @@ export class AdbPacketDispatcher extends AutoDisposable {
this._abortController.abort(); this._abortController.abort();
} catch { } } catch { }
this._packetSerializeStreamWriter.releaseLock();
super.dispose(); super.dispose();
} }
} }

View file

@ -1,10 +1,10 @@
import { AdbPacket, AdbPacketInit } from '../packet'; import { AdbPacket, AdbPacketCore } from '../packet';
import { AdbSocket } from './socket'; import { AdbSocket } from './socket';
export interface AdbLogger { export interface AdbLogger {
onIncomingPacket?(packet: AdbPacket): void; onIncomingPacket?(packet: AdbPacket): void;
onOutgoingPacket?(packet: AdbPacketInit): void; onOutgoingPacket?(packet: AdbPacketCore): void;
onSocketOpened?(socket: AdbSocket): void; onSocketOpened?(socket: AdbSocket): void;

View file

@ -1,6 +1,7 @@
import { StructAsyncDeserializeStream } from '@yume-chan/struct'; import { StructAsyncDeserializeStream } from '@yume-chan/struct';
import { AdbSocket, AdbSocketInfo } from '../socket'; import { AdbSocket, AdbSocketInfo } from '../socket';
import { ReadableStream, ReadableStreamDefaultReader } from './detect'; import { ReadableStream, ReadableStreamDefaultReader } from './detect';
import { PushReadableStream } from "./transform";
export class BufferedStreamEndedError extends Error { export class BufferedStreamEndedError extends Error {
public constructor() { public constructor() {
@ -26,10 +27,9 @@ export class BufferedStream {
/** /**
* *
* @param length * @param length
* @param readToEnd When `true`, allow less data to be returned if the stream has reached its end.
* @returns * @returns
*/ */
public async read(length: number, readToEnd: boolean = false): Promise<Uint8Array> { public async read(length: number): Promise<Uint8Array> {
let array: Uint8Array; let array: Uint8Array;
let index: number; let index: number;
if (this.buffer) { if (this.buffer) {
@ -44,16 +44,11 @@ export class BufferedStream {
index = buffer.byteLength; index = buffer.byteLength;
this.buffer = undefined; this.buffer = undefined;
} else { } else {
const result = await this.reader.read(); const { done, value } = await this.reader.read();
if (result.done) { if (done) {
if (readToEnd) { throw new Error('Unexpected end of stream');
return new Uint8Array(0);
} else {
throw new Error('Unexpected end of stream');
}
} }
const { value } = result;
if (value.byteLength === length) { if (value.byteLength === length) {
return value; return value;
} }
@ -71,16 +66,11 @@ export class BufferedStream {
while (index < length) { while (index < length) {
const left = length - index; const left = length - index;
const result = await this.reader.read(); const { done, value } = await this.reader.read();
if (result.done) { if (done) {
if (readToEnd) { throw new Error('Unexpected end of stream');
return new Uint8Array(0);
} else {
throw new Error('Unexpected end of stream');
}
} }
const { value } = result;
if (value.byteLength === left) { if (value.byteLength === left) {
array.set(value, index); array.set(value, index);
return array; return array;
@ -99,6 +89,40 @@ export class BufferedStream {
return array; return array;
} }
/**
* Return a readable stream with unconsumed data (if any) and
* all data from the wrapped stream.
* @returns A `ReadableStream`
*/
public release(): ReadableStream<Uint8Array> {
if (this.buffer) {
return new PushReadableStream<Uint8Array>(async controller => {
// Put the remaining data back to the stream
await controller.enqueue(this.buffer!);
// Manually pipe the stream
while (true) {
try {
const { done, value } = await this.reader.read();
if (done) {
controller.close();
break;
} else {
await controller.enqueue(value);
}
} catch (e) {
controller.error(e);
break;
}
}
});
} else {
// Simply release the reader and return the stream
this.reader.releaseLock();
return this.stream;
}
}
public close() { public close() {
this.reader.cancel(); this.reader.cancel();
} }

View file

@ -335,6 +335,16 @@ export class SplitLineStream extends TransformStream<string, string> {
} }
} }
/**
* Create a new `WritableStream` that, when written to, will write that chunk to
* `pair.writable`, when pipe `pair.readable` to `writable`.
*
* It's the opposite of `ReadableStream.pipeThrough`.
*
* @param writable The `WritableStream` to write to.
* @param pair A `TransformStream` that converts chunks.
* @returns A new `WritableStream`.
*/
export function pipeFrom<W, T>(writable: WritableStream<W>, pair: ReadableWritablePair<W, T>) { export function pipeFrom<W, T>(writable: WritableStream<W>, pair: ReadableWritablePair<W, T>) {
const writer = pair.writable.getWriter(); const writer = pair.writable.getWriter();
const pipe = pair.readable const pipe = pair.readable

View file

@ -30,7 +30,7 @@
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
}, },
"dependencies": { "dependencies": {

View file

@ -39,7 +39,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^26.6.3", "jest": "^26.6.3",
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
} }
} }

View file

@ -37,7 +37,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^26.6.3", "jest": "^26.6.3",
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
} }
} }

View file

@ -48,7 +48,7 @@
"gh-release-fetch": "^2.0.4", "gh-release-fetch": "^2.0.4",
"jest": "^26.6.3", "jest": "^26.6.3",
"tinyh264": "^0.0.7", "tinyh264": "^0.0.7",
"typescript": "^4.5.5", "typescript": "^4.6.2",
"yuv-buffer": "^1.0.0", "yuv-buffer": "^1.0.0",
"yuv-canvas": "^1.2.7" "yuv-canvas": "^1.2.7"
}, },

View file

@ -1,4 +1,4 @@
import { Adb, AdbBufferedStream, AdbNoneSubprocessProtocol, AdbSocket, AdbSubprocessProtocol, DecodeUtf8Stream, PushReadableStream, ReadableStream, TransformStream, WritableStreamDefaultWriter } from '@yume-chan/adb'; import { Adb, AdbBufferedStream, AdbNoneSubprocessProtocol, AdbSocket, AdbSubprocessProtocol, DecodeUtf8Stream, InspectStream, ReadableStream, TransformStream, WritableStreamDefaultWriter } from '@yume-chan/adb';
import { EventEmitter } from '@yume-chan/event'; import { EventEmitter } from '@yume-chan/event';
import Struct from '@yume-chan/struct'; import Struct from '@yume-chan/struct';
import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message'; import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message';
@ -155,20 +155,14 @@ export class ScrcpyClient {
}, },
})); }));
this._videoStream = new PushReadableStream(async controller => { this._videoStream = options
try { .parseVideoStream(videoStream)
while (true) { .pipeThrough(new InspectStream(packet => {
const packet = await options.parseVideoStream(videoStream); if (packet.type === 'configuration') {
if (packet.type === 'configuration') { this._screenWidth = packet.data.croppedWidth;
this._screenWidth = packet.data.croppedWidth; this._screenHeight = packet.data.croppedHeight;
this._screenHeight = packet.data.croppedHeight;
}
await controller.enqueue(packet);
} }
} catch { }));
controller.close();
}
});
if (controlStream) { if (controlStream) {
const buffered = new AdbBufferedStream(controlStream); const buffered = new AdbBufferedStream(controlStream);

View file

@ -1,4 +1,4 @@
import type { Adb, AdbBufferedStream } from "@yume-chan/adb"; import { BufferedStreamEndedError, ReadableStream, TransformStream, type Adb, type AdbBufferedStream } from "@yume-chan/adb";
import Struct, { placeholder } from "@yume-chan/struct"; import Struct, { placeholder } from "@yume-chan/struct";
import type { AndroidCodecLevel, AndroidCodecProfile } from "../../codec"; import type { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../../connection"; import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../../connection";
@ -135,8 +135,6 @@ export const ScrcpyInjectScrollControlMessage1_16 =
export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsInit1_16> implements ScrcpyOptions<T> { export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsInit1_16> implements ScrcpyOptions<T> {
public value: Partial<T>; public value: Partial<T>;
private _streamHeader: Uint8Array | undefined;
public constructor(value: Partial<ScrcpyOptionsInit1_16>) { public constructor(value: Partial<ScrcpyOptionsInit1_16>) {
if (new.target === ScrcpyOptions1_16 && if (new.target === ScrcpyOptions1_16 &&
value.logLevel === ScrcpyLogLevel.Verbose) { value.logLevel === ScrcpyLogLevel.Verbose) {
@ -213,74 +211,97 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsIn
return /\s+scrcpy --encoder-name '(.*?)'/; return /\s+scrcpy --encoder-name '(.*?)'/;
} }
public async parseVideoStream(stream: AdbBufferedStream): Promise<VideoStreamPacket> { public parseVideoStream(stream: AdbBufferedStream): ReadableStream<VideoStreamPacket> {
// Optimized path for video frames only
if (this.value.sendFrameMeta === false) { if (this.value.sendFrameMeta === false) {
return { return stream
type: 'frame', .release()
data: await stream.read(1 * 1024 * 1024, true), .pipeThrough(new TransformStream<Uint8Array, VideoStreamPacket>({
}; transform(chunk, controller) {
controller.enqueue({
type: 'frame',
data: chunk,
});
},
}));
} }
const { pts, data } = await VideoPacket.deserialize(stream); let header: Uint8Array | undefined;
if (pts === NoPts) {
const sequenceParameterSet = parse_sequence_parameter_set(data.slice().buffer);
const { return new ReadableStream<VideoStreamPacket>({
profile_idc: profileIndex, async pull(controller) {
constraint_set: constraintSet, try {
level_idc: levelIndex, const { pts, data } = await VideoPacket.deserialize(stream);
pic_width_in_mbs_minus1, if (pts === NoPts) {
pic_height_in_map_units_minus1, const sequenceParameterSet = parse_sequence_parameter_set(data.slice().buffer);
frame_mbs_only_flag,
frame_crop_left_offset,
frame_crop_right_offset,
frame_crop_top_offset,
frame_crop_bottom_offset,
} = sequenceParameterSet;
const encodedWidth = (pic_width_in_mbs_minus1 + 1) * 16; const {
const encodedHeight = (pic_height_in_map_units_minus1 + 1) * (2 - frame_mbs_only_flag) * 16; profile_idc: profileIndex,
const cropLeft = frame_crop_left_offset * 2; constraint_set: constraintSet,
const cropRight = frame_crop_right_offset * 2; level_idc: levelIndex,
const cropTop = frame_crop_top_offset * 2; pic_width_in_mbs_minus1,
const cropBottom = frame_crop_bottom_offset * 2; pic_height_in_map_units_minus1,
frame_mbs_only_flag,
frame_crop_left_offset,
frame_crop_right_offset,
frame_crop_top_offset,
frame_crop_bottom_offset,
} = sequenceParameterSet;
const croppedWidth = encodedWidth - cropLeft - cropRight; const encodedWidth = (pic_width_in_mbs_minus1 + 1) * 16;
const croppedHeight = encodedHeight - cropTop - cropBottom; const encodedHeight = (pic_height_in_map_units_minus1 + 1) * (2 - frame_mbs_only_flag) * 16;
const cropLeft = frame_crop_left_offset * 2;
const cropRight = frame_crop_right_offset * 2;
const cropTop = frame_crop_top_offset * 2;
const cropBottom = frame_crop_bottom_offset * 2;
this._streamHeader = data; const croppedWidth = encodedWidth - cropLeft - cropRight;
return { const croppedHeight = encodedHeight - cropTop - cropBottom;
type: 'configuration',
data: { header = data;
profileIndex, controller.enqueue({
constraintSet, type: 'configuration',
levelIndex, data: {
encodedWidth, profileIndex,
encodedHeight, constraintSet,
cropLeft, levelIndex,
cropRight, encodedWidth,
cropTop, encodedHeight,
cropBottom, cropLeft,
croppedWidth, cropRight,
croppedHeight, cropTop,
cropBottom,
croppedWidth,
croppedHeight,
}
});
return;
}
let frameData: Uint8Array;
if (header) {
frameData = new Uint8Array(header.byteLength + data.byteLength);
frameData.set(header);
frameData.set(data!, header.byteLength);
header = undefined;
} else {
frameData = data;
}
controller.enqueue({
type: 'frame',
data: frameData,
});
} catch (e) {
if (e instanceof BufferedStreamEndedError) {
controller.close();
return;
}
throw e;
} }
}; }
} });
let frameData: Uint8Array;
if (this._streamHeader) {
frameData = new Uint8Array(this._streamHeader.byteLength + data.byteLength);
frameData.set(this._streamHeader);
frameData.set(data!, this._streamHeader.byteLength);
this._streamHeader = undefined;
} else {
frameData = data;
}
return {
type: 'frame',
data: frameData,
};
} }
public serializeBackOrScreenOnControlMessage( public serializeBackOrScreenOnControlMessage(

View file

@ -1,4 +1,4 @@
import type { Adb, AdbBufferedStream } from "@yume-chan/adb"; import type { Adb, AdbBufferedStream, ReadableStream } from "@yume-chan/adb";
import type { ScrcpyClientConnection } from "../connection"; import type { ScrcpyClientConnection } from "../connection";
import type { H264Configuration } from "../decoder"; import type { H264Configuration } from "../decoder";
import type { ScrcpyBackOrScreenOnEvent1_18 } from "./1_18"; import type { ScrcpyBackOrScreenOnEvent1_18 } from "./1_18";
@ -49,7 +49,7 @@ export interface ScrcpyOptions<T> {
createConnection(adb: Adb): ScrcpyClientConnection; createConnection(adb: Adb): ScrcpyClientConnection;
parseVideoStream(stream: AdbBufferedStream): Promise<VideoStreamPacket>; parseVideoStream(stream: AdbBufferedStream): ReadableStream<VideoStreamPacket>;
serializeBackOrScreenOnControlMessage( serializeBackOrScreenOnControlMessage(
message: ScrcpyBackOrScreenOnEvent1_18, message: ScrcpyBackOrScreenOnEvent1_18,

View file

@ -39,7 +39,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^26.6.3", "jest": "^26.6.3",
"typescript": "^4.5.5", "typescript": "^4.6.2",
"@yume-chan/ts-package-builder": "^1.0.0", "@yume-chan/ts-package-builder": "^1.0.0",
"@types/jest": "^27.4.0", "@types/jest": "^27.4.0",
"@types/bluebird": "^3.5.36" "@types/bluebird": "^3.5.36"