feat(demo): wip: add new packet log

ref #397
This commit is contained in:
Simon Chan 2022-04-11 01:50:43 +08:00
parent 4cec3ec55c
commit bb5e292a80
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
69 changed files with 1640 additions and 618 deletions

View file

@ -1,171 +0,0 @@
import { IconButton, IListProps, List, mergeStyles, mergeStyleSets, Stack } from '@fluentui/react';
import { AdbCommand, AdbPacketCore, decodeUtf8 } from '@yume-chan/adb';
import { observer } from "mobx-react-lite";
import { PropsWithChildren, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { globalState } from "../state";
import { Icons, withDisplayName } from '../utils';
import { CommandBar } from './command-bar';
const classNames = mergeStyleSets({
'logger-container': {
width: 300,
},
grow: {
flexGrow: 1,
height: 0,
padding: '0 8px',
overflowX: 'hidden',
overflowY: 'auto',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
},
});
const ADB_COMMAND_NAME = {
[AdbCommand.Auth]: 'AUTH',
[AdbCommand.Close]: 'CLSE',
[AdbCommand.Connect]: 'CNXN',
[AdbCommand.OK]: 'OKAY',
[AdbCommand.Open]: 'OPEN',
[AdbCommand.Write]: 'WRTE',
};
function serializePacket(packet: AdbPacketCore) {
const command =
ADB_COMMAND_NAME[packet.command as AdbCommand] ??
decodeUtf8(new Uint32Array([packet.command]));
const parts = [
command,
packet.arg0.toString(16).padStart(8, '0'),
packet.arg1.toString(16).padStart(8, '0'),
];
if (packet.payload) {
parts.push(
Array.from(
packet.payload,
byte => byte.toString(16).padStart(2, '0')
).join(' ')
);
}
return parts.join(' ');
}
const LogLine = withDisplayName('LoggerLine')(({ packet }: { packet: [string, AdbPacketCore]; }) => {
const string = useMemo(() => serializePacket(packet[1]), [packet]);
return (
<>
{packet[0]}{' '}{string}
</>
);
});
export const ToggleLogView = observer(() => {
return (
<IconButton
checked={globalState.logVisible}
iconProps={{ iconName: Icons.TextGrammarError }}
title="Toggle Log"
onClick={globalState.toggleLog}
/>
);
});
export interface LoggerProps {
className?: string;
}
function shouldVirtualize(props: IListProps<[string, AdbPacketCore]>) {
return !!props.items && props.items.length > 100;
}
function renderCell(item?: [string, AdbPacketCore]) {
if (!item) {
return null;
}
return (
<LogLine packet={item} />
);
}
export const LogView = observer(({
className,
}: LoggerProps) => {
const scrollerRef = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
const scroller = scrollerRef.current;
if (scroller) {
scroller.scrollTop = scroller.scrollHeight;
}
});
const commandBarItems = useMemo(() => [
{
key: 'Copy',
text: 'Copy',
iconProps: { iconName: Icons.Copy },
onClick: () => {
window.navigator.clipboard.writeText(
globalState.logs
.map(
([direction, packet]) => `${direction}${serializePacket((packet))}`
)
.join('\n'));
},
},
{
key: 'Clear',
text: 'Clear',
iconProps: { iconName: Icons.Delete },
onClick: () => {
globalState.clearLog();
},
},
], []);
const mergedClassName = useMemo(() => mergeStyles(
className,
classNames['logger-container'],
), [className]);
if (!globalState.logVisible) {
return null;
}
return (
<Stack
className={mergedClassName}
verticalFill
>
<CommandBar items={commandBarItems} />
<div ref={scrollerRef} className={classNames.grow}>
<List
items={globalState.logs}
onShouldVirtualize={shouldVirtualize}
onRenderCell={renderCell}
/>
</div>
</Stack>
);
});
export function NoSsr({ children }: PropsWithChildren<{}>) {
const [showChild, setShowChild] = useState(false);
// Wait until after client-side hydration to show
useEffect(() => {
setShowChild(true);
}, []);
if (!showChild) {
return null;
}
return <>{children}</>;
}

View file

@ -31,6 +31,7 @@
"next": "12.1.3", "next": "12.1.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-window": "1.8.6",
"streamsaver": "^2.0.5", "streamsaver": "^2.0.5",
"xterm": "^4.17.0", "xterm": "^4.17.0",
"xterm-addon-fit": "^0.5.0", "xterm-addon-fit": "^0.5.0",
@ -41,8 +42,9 @@
"@mdx-js/loader": "^1.6.22", "@mdx-js/loader": "^1.6.22",
"@next/mdx": "^11.1.2", "@next/mdx": "^11.1.2",
"@types/react": "17.0.27", "@types/react": "17.0.27",
"@types/react-window": "^1.8.5",
"eslint": "8.8.0", "eslint": "8.8.0",
"eslint-config-next": "12.1.3", "eslint-config-next": "12.1.3",
"typescript": "next" "typescript": "4.7.0-beta"
} }
} }

View file

@ -150,13 +150,13 @@ function _Connect(): JSX.Element | null {
const readable = streams.readable const readable = streams.readable
.pipeThrough( .pipeThrough(
new InspectStream(packet => { new InspectStream(packet => {
globalState.appendLog('Incoming', packet); globalState.appendLog('in', packet);
}) })
); );
const writable = pipeFrom( const writable = pipeFrom(
streams.writable, streams.writable,
new InspectStream(packet => { new InspectStream(packet => {
globalState.appendLog('Outgoing', packet); globalState.appendLog('out', packet);
}) })
); );
device = await Adb.authenticate({ readable, writable }, CredentialStore, undefined); device = await Adb.authenticate({ readable, writable }, CredentialStore, undefined);

View file

@ -1,6 +1,6 @@
import { StackItem } from '@fluentui/react'; import { mergeStyleSets, StackItem } from '@fluentui/react';
import { ReactNode, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { ReactNode, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { ResizeObserver, Size } from '../utils/resize-observer'; import { ResizeObserver, Size } from './resize-observer';
import { forwardRef } from '../utils/with-display-name'; import { forwardRef } from '../utils/with-display-name';
export interface DeviceViewProps { export interface DeviceViewProps {
@ -23,6 +23,21 @@ export const DeviceView = forwardRef<DeviceViewRef>('DeviceView')(({
bottomElement, bottomElement,
children, children,
}: DeviceViewProps, ref) => { }: DeviceViewProps, ref) => {
const styles = mergeStyleSets({
outer: {
width: '100%',
height: '100%',
backgroundColor: 'black',
},
inner: {
position: 'absolute',
transformOrigin: 'top left',
},
bottom: {
position: 'absolute',
},
});
const [containerSize, setContainerSize] = useState<Size>({ width: 0, height: 0 }); const [containerSize, setContainerSize] = useState<Size>({ width: 0, height: 0 });
const [bottomSize, setBottomSize] = useState<Size>({ width: 0, height: 0 }); const [bottomSize, setBottomSize] = useState<Size>({ width: 0, height: 0 });
@ -82,6 +97,7 @@ export const DeviceView = forwardRef<DeviceViewRef>('DeviceView')(({
return ( return (
<StackItem grow> <StackItem grow>
<ResizeObserver <ResizeObserver
className={styles.outer}
ref={containerRef} ref={containerRef}
style={{ style={{
position: 'relative', position: 'relative',
@ -92,22 +108,21 @@ export const DeviceView = forwardRef<DeviceViewRef>('DeviceView')(({
onResize={setContainerSize} onResize={setContainerSize}
> >
<div <div
className={styles.inner}
style={{ style={{
position: 'absolute',
top: childrenStyle.top, top: childrenStyle.top,
left: childrenStyle.left, left: childrenStyle.left,
width, width,
height, height,
transform: `scale(${childrenStyle.scale})`, transform: `scale(${childrenStyle.scale})`,
transformOrigin: 'top left',
}} }}
> >
{children} {children}
</div> </div>
<ResizeObserver <ResizeObserver
className={styles.bottom}
style={{ style={{
position: 'absolute',
top: childrenStyle.top + childrenStyle.height, top: childrenStyle.top + childrenStyle.height,
left: childrenStyle.left, left: childrenStyle.left,
width: childrenStyle.width, width: childrenStyle.width,

View file

@ -5,3 +5,4 @@ export * from './device-view';
export * from './error-dialog'; export * from './error-dialog';
export * from './external-link'; export * from './external-link';
export * from './log-view'; export * from './log-view';
export * from './resize-observer';

View file

@ -0,0 +1,16 @@
import { PropsWithChildren, useEffect, useState } from 'react';
export function NoSsr({ children }: PropsWithChildren<{}>) {
const [showChild, setShowChild] = useState(false);
// Wait until after client-side hydration to show
useEffect(() => {
setShowChild(true);
}, []);
if (!showChild) {
return null;
}
return <>{children}</>;
}

View file

@ -1,6 +1,6 @@
import { createMergedRef } from '@fluentui/react'; import { createMergedRef } from '@fluentui/react';
import { CSSProperties, HTMLAttributes, PropsWithChildren, useCallback, useMemo, useRef } from 'react'; import { CSSProperties, HTMLAttributes, PropsWithChildren, useCallback, useLayoutEffect, useMemo, useRef } from 'react';
import { forwardRef } from './with-display-name'; import { forwardRef, useCallbackRef } from '../utils';
export interface Size { export interface Size {
width: number; width: number;
@ -12,17 +12,6 @@ export interface ResizeObserverProps extends HTMLAttributes<HTMLDivElement>, Pro
onResize: (size: Size) => void; onResize: (size: Size) => void;
} }
export function useCallbackRef<TArgs extends any[], R>(callback: (...args: TArgs) => R): (...args: TArgs) => R {
const ref = useRef<(...args: TArgs) => R>(callback);
ref.current = callback;
const wrapper = useCallback((...args: TArgs) => {
return ref.current.apply(undefined, args);
}, []);
return wrapper;
}
const iframeStyle: CSSProperties = { const iframeStyle: CSSProperties = {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@ -46,6 +35,10 @@ export const ResizeObserver = forwardRef<HTMLDivElement>('ResizeObserver')(({
onResize({ width, height }); onResize({ width, height });
}); });
useLayoutEffect(() => {
handleResize();
}, []);
const handleIframeRef = useCallback((element: HTMLIFrameElement | null) => { const handleIframeRef = useCallback((element: HTMLIFrameElement | null) => {
if (element) { if (element) {
element.contentWindow!.addEventListener('resize', handleResize); element.contentWindow!.addEventListener('resize', handleResize);

View file

@ -3,8 +3,8 @@ import type { AppProps } from 'next/app';
import Head from "next/head"; import Head from "next/head";
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Connect, ErrorDialogProvider, LogView, NoSsr, ToggleLogView } from "../components"; import { Connect, ErrorDialogProvider } from "../components";
import '../styles/globals.css'; import '../styles/globals.css';
import { Icons } from "../utils"; import { Icons } from "../utils";
import { register as registerIcons } from '../utils/icons'; import { register as registerIcons } from '../utils/icons';
@ -62,6 +62,11 @@ const ROUTES = [
icon: Icons.Bug, icon: Icons.Bug,
name: 'Bug Report', name: 'Bug Report',
}, },
{
url: '/packet-log',
icon: Icons.TextGrammarError,
name: 'Packet Log',
},
]; ];
function NavLink({ link, defaultRender: DefaultRender, ...props }: IComponentAsProps<INavButtonProps>) { function NavLink({ link, defaultRender: DefaultRender, ...props }: IComponentAsProps<INavButtonProps>) {
@ -123,7 +128,7 @@ function App({ Component, pageProps }: AppProps) {
/> />
<StackItem grow> <StackItem grow>
<div className={classNames.title}>WebADB Demo</div> <div className={classNames.title}>Android Web Toolbox</div>
</StackItem> </StackItem>
<IconButton <IconButton
@ -133,8 +138,6 @@ function App({ Component, pageProps }: AppProps) {
href="https://github.com/yume-chan/ya-webadb/issues/new" href="https://github.com/yume-chan/ya-webadb/issues/new"
target="_blank" target="_blank"
/> />
<ToggleLogView />
</Stack> </Stack>
<Stack grow horizontal verticalFill disableShrink styles={{ root: { minHeight: 0, overflow: 'hidden', lineHeight: '1.5' } }}> <Stack grow horizontal verticalFill disableShrink styles={{ root: { minHeight: 0, overflow: 'hidden', lineHeight: '1.5' } }}>
@ -156,10 +159,6 @@ function App({ Component, pageProps }: AppProps) {
<StackItem grow styles={{ root: { width: 0 } }}> <StackItem grow styles={{ root: { width: 0 } }}>
<Component {...pageProps} /> <Component {...pageProps} />
</StackItem> </StackItem>
<NoSsr>
<LogView className={classNames['right-column']} />
</NoSsr>
</Stack> </Stack>
</Stack> </Stack>
</ErrorDialogProvider > </ErrorDialogProvider >

View file

@ -95,7 +95,7 @@ const BugReportPage: NextPage = () => {
return ( return (
<Stack {...RouteStackProps}> <Stack {...RouteStackProps}>
<Head> <Head>
<title>BugReport - WebADB</title> <title>BugReport - Android Web Toolbox</title>
</Head> </Head>
<MessageBar messageBarType={MessageBarType.info}>This is the `bugreport`/`bugreportz` tool in Android</MessageBar> <MessageBar messageBarType={MessageBarType.info}>This is the `bugreport`/`bugreportz` tool in Android</MessageBar>

View file

@ -587,7 +587,7 @@ const FileManager: NextPage = (): JSX.Element | null => {
return ( return (
<Stack {...RouteStackProps}> <Stack {...RouteStackProps}>
<Head> <Head>
<title>File Manager - WebADB</title> <title>File Manager - Android Web Toolbox</title>
</Head> </Head>
<CommandBar items={state.menuItems} /> <CommandBar items={state.menuItems} />

View file

@ -98,7 +98,7 @@ const Install: NextPage = () => {
return ( return (
<Stack {...RouteStackProps}> <Stack {...RouteStackProps}>
<Head> <Head>
<title>Install APK - WebADB</title> <title>Install APK - Android Web Toolbox</title>
</Head> </Head>
<Stack horizontal> <Stack horizontal>

View file

@ -0,0 +1,283 @@
import { mergeStyleSets, Stack, StackItem } from "@fluentui/react";
import { AdbCommand, decodeUtf8 } from "@yume-chan/adb";
import { observer } from "mobx-react-lite";
import { NextPage } from "next";
import Head from "next/head";
import { Children, CSSProperties, forwardRef, isValidElement, ReactChildren, ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { VariableSizeGrid } from 'react-window';
import { ResizeObserver, Size } from "../components";
import { globalState, PacketLogItem } from "../state";
import { RouteStackProps, useCallbackRef } from "../utils";
const ADB_COMMAND_NAME = {
[AdbCommand.Auth]: 'AUTH',
[AdbCommand.Close]: 'CLSE',
[AdbCommand.Connect]: 'CNXN',
[AdbCommand.OK]: 'OKAY',
[AdbCommand.Open]: 'OPEN',
[AdbCommand.Write]: 'WRTE',
};
interface Column {
key: string;
title: string;
width?: number;
flexGrow?: number;
render: (value: PacketLogItem, style: CSSProperties) => JSX.Element;
}
const LINE_HEIGHT = 32;
const PacketLog: NextPage = () => {
const styles = mergeStyleSets({
tableContainer: {
width: '100%',
height: '100%',
},
table: {
position: 'absolute !important',
top: 0,
left: 0,
},
header: {
position: 'sticky',
textAlign: 'center',
fontWeight: 'bold',
lineHeight: LINE_HEIGHT + 'px',
},
code: {
fontFamily: 'monospace',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
lineHeight: LINE_HEIGHT + 'px',
}
});
const columns: Column[] = [
{
key: 'direction',
title: 'Direction',
width: 100,
render(item, style) {
return (
<div
className={styles.code}
style={style}
>
{item.direction}
</div>
);
},
},
{
key: 'command',
title: 'Command',
width: 100,
render(item, style) {
if (!item.commandString) {
item.commandString =
ADB_COMMAND_NAME[item.command as AdbCommand] ??
decodeUtf8(new Uint32Array([item.command]));
}
return (
<div
className={styles.code}
style={style}
>
{item.commandString}
</div>
);
}
},
{
key: 'arg0',
title: 'Arg0',
width: 100,
render(item, style) {
if (!item.arg0String) {
item.arg0String = item.arg0.toString(16).padStart(8, '0');
}
return (
<div
className={styles.code}
style={style}
>
{item.arg0String}
</div>
);
}
},
{
key: 'arg1',
title: 'Arg1',
width: 100,
render(item, style) {
if (!item.arg1String) {
item.arg1String = item.arg1.toString(16).padStart(8, '0');
}
return (
<div
className={styles.code}
style={style}
>
{item.arg1String}
</div>
);
}
},
{
key: 'payload',
title: 'Payload',
render(item, style) {
if (!item.payloadString) {
item.payloadString = decodeUtf8(item.payload);
}
return (
<div
className={styles.code}
style={style}
>
{item.payloadString}
</div>
);
}
}
];
const [tableSize, setTableSize] = useState<Size>({ width: 0, height: 0 });
const columnWidths = useMemo(() => {
let distributableWidth = tableSize.width;
let distributedSlices = 0;
for (const column of columns) {
if (column.width) {
distributableWidth -= column.width;
} else {
distributedSlices += column.flexGrow ?? 1;
}
}
const widthPerSlice = distributableWidth / distributedSlices;
return columns.map(column => {
if (column.width) {
return column.width;
} else {
return widthPerSlice * (column.flexGrow ?? 1);
}
});
}, [tableSize.width]);
const columnWidth = useCallbackRef((index: number) => columnWidths[index]);
const tableRef = useRef<VariableSizeGrid | null>(null);
useEffect(() => {
tableRef.current?.resetAfterColumnIndex(
columns.findIndex(column => !column.width),
true
);
}, [columnWidths]);
const innerElementType = useMemo(() =>
forwardRef<HTMLDivElement, any>(({ children, ...rest }: { children: ReactNode; }, ref) => {
let left = 0;
const { minColumn, maxColumn } = Children.toArray(children).reduce<{ minColumn: number; maxColumn: number; }>(
({ minColumn, maxColumn }, child) => {
if (!isValidElement(child)) {
return { minColumn, maxColumn };
}
const columnIndex = child.props.columnIndex as number;
return {
minColumn: Math.min(minColumn, columnIndex),
maxColumn: Math.max(maxColumn, columnIndex),
};
},
{
minColumn: Infinity,
maxColumn: -Infinity
}
);
const headers = [];
if (minColumn !== Infinity && maxColumn !== -Infinity) {
for (let i = 0; i < minColumn; i += 1) {
left += columnWidth(i);
}
const height = LINE_HEIGHT;
for (let i = minColumn; i <= maxColumn; i++) {
const width = columnWidth(i);
headers.push(
<div
key={columns[i].key}
style={{
position: 'absolute',
top: 0,
left,
width,
height,
}}
>
{columns[i].title}
</div>
);
left += width;
}
}
return (
<div ref={ref} {...rest}>
<div className={styles.header}>
{headers}
</div>
{children}
</div>
);
}),
[]
);
return (
<Stack {...RouteStackProps} tokens={{}}>
<Head>
<title>Packet Log - Android Web Toolbox</title>
</Head>
<StackItem grow shrink>
<ResizeObserver
className={styles.tableContainer}
onResize={setTableSize}
>
<VariableSizeGrid
ref={tableRef}
className={styles.table}
width={tableSize.width}
height={tableSize.height}
columnCount={columns.length}
columnWidth={columnWidth}
rowCount={globalState.logs.length + 1}
rowHeight={() => LINE_HEIGHT}
estimatedRowHeight={LINE_HEIGHT}
innerElementType={innerElementType}
>
{({ columnIndex, rowIndex, style }) => {
if (rowIndex === 0) {
return null;
}
const item = globalState.logs[rowIndex - 1];
return columns[columnIndex].render(item, style);
}}
</VariableSizeGrid>
</ResizeObserver>
</StackItem>
</Stack>
);
};
export default observer(PacketLog);

View file

@ -824,7 +824,7 @@ const Scrcpy: NextPage = () => {
return ( return (
<Stack {...RouteStackProps}> <Stack {...RouteStackProps}>
<Head> <Head>
<title>Scrcpy - WebADB</title> <title>Scrcpy - Android Web Toolbox</title>
</Head> </Head>
<CommandBar items={state.commandBarItems} farItems={state.commandBarFarItems} /> <CommandBar items={state.commandBarItems} farItems={state.commandBarFarItems} />

View file

@ -5,8 +5,9 @@ import { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import 'xterm/css/xterm.css'; import 'xterm/css/xterm.css';
import { ResizeObserver } from '../components';
import { globalState } from "../state"; import { globalState } from "../state";
import { Icons, ResizeObserver, RouteStackProps } from '../utils'; import { Icons, RouteStackProps } from '../utils';
let terminal: import('../components/terminal').AdbTerminal; let terminal: import('../components/terminal').AdbTerminal;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@ -80,7 +81,7 @@ const Shell: NextPage = (): JSX.Element | null => {
return ( return (
<Stack {...RouteStackProps}> <Stack {...RouteStackProps}>
<Head> <Head>
<title>Interactive Shell - WebADB</title> <title>Interactive Shell - Android Web Toolbox</title>
</Head> </Head>
<StackItem> <StackItem>

View file

@ -141,7 +141,7 @@ const TcpIp: NextPage = () => {
return ( return (
<Stack {...RouteStackProps}> <Stack {...RouteStackProps}>
<Head> <Head>
<title>ADB over WiFi - WebADB</title> <title>ADB over WiFi - Android Web Toolbox</title>
</Head> </Head>
<CommandBar items={state.commandBarItems} /> <CommandBar items={state.commandBarItems} />
@ -149,7 +149,7 @@ const TcpIp: NextPage = () => {
<StackItem> <StackItem>
<MessageBar> <MessageBar>
<Text> <Text>
For WebADB to wirelessly connect to your phone, For Android Web Toolbox to wirelessly connect to your phone,
<ExternalLink href="https://github.com/yume-chan/ya-webadb/discussions/245#discussioncomment-384030" spaceBefore spaceAfter>extra software</ExternalLink> <ExternalLink href="https://github.com/yume-chan/ya-webadb/discussions/245#discussioncomment-384030" spaceBefore spaceAfter>extra software</ExternalLink>
is required. is required.
</Text> </Text>

View file

@ -1,6 +1,17 @@
import { Adb, AdbBackend, AdbPacketCore } from "@yume-chan/adb"; import { Adb, AdbBackend, AdbPacketData } from "@yume-chan/adb";
import { action, makeAutoObservable, observable } from 'mobx'; import { action, makeAutoObservable, observable } from 'mobx';
export type PacketLogItemDirection = 'in' | 'out';
export interface PacketLogItem extends AdbPacketData {
direction: PacketLogItemDirection;
commandString?: string;
arg0String?: string;
arg1String?: string;
payloadString?: string;
}
export class GlobalState { export class GlobalState {
backend: AdbBackend | undefined = undefined; backend: AdbBackend | undefined = undefined;
@ -9,13 +20,11 @@ export class GlobalState {
errorDialogVisible = false; errorDialogVisible = false;
errorDialogMessage = ''; errorDialogMessage = '';
logVisible = false; logs: PacketLogItem[] = [];
logs: [string, AdbPacketCore][] = [];
constructor() { constructor() {
makeAutoObservable(this, { makeAutoObservable(this, {
hideErrorDialog: action.bound, hideErrorDialog: action.bound,
toggleLog: action.bound,
logs: observable.shallow, logs: observable.shallow,
}); });
} }
@ -34,12 +43,9 @@ export class GlobalState {
this.errorDialogVisible = false; this.errorDialogVisible = false;
} }
toggleLog() { appendLog(direction: PacketLogItemDirection, packet: AdbPacketData) {
this.logVisible = !this.logVisible; (packet as PacketLogItem).direction = direction;
} this.logs.push((packet as PacketLogItem));
appendLog(direction: string, packet: AdbPacketCore) {
this.logs.push([direction, packet]);
} }
clearLog() { clearLog() {

View file

@ -1,5 +1,5 @@
import { registerIcons } from "@fluentui/react"; import { registerIcons } from "@fluentui/react";
import { AddCircleRegular, ArrowClockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, BoxRegular, BugRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, MoreHorizontalRegular, NavigationRegular, PanelBottomRegular, PersonFeedbackRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, PowerRegular, SaveRegular, SearchRegular, SettingsRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WarningRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons'; import { FilterRegular, AddCircleRegular, ArrowClockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, BoxRegular, BugRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, MoreHorizontalRegular, NavigationRegular, PanelBottomRegular, PersonFeedbackRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, PowerRegular, SaveRegular, SearchRegular, SettingsRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WarningRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons';
const STYLE = {}; const STYLE = {};
@ -52,6 +52,7 @@ export function register() {
SortUp: <ArrowSortUpRegular style={STYLE} />, SortUp: <ArrowSortUpRegular style={STYLE} />,
SortDown: <ArrowSortDownRegular style={STYLE} />, SortDown: <ArrowSortDownRegular style={STYLE} />,
Search: <SearchRegular style={STYLE} />, Search: <SearchRegular style={STYLE} />,
Filter: <FilterRegular style={STYLE} />,
// Required by file manager page // Required by file manager page
Document20: <DocumentRegular style={{ fontSize: 20, verticalAlign: 'middle' }} /> Document20: <DocumentRegular style={{ fontSize: 20, verticalAlign: 'middle' }} />

View file

@ -2,6 +2,5 @@ export * from './async-effect';
export * from './file'; export * from './file';
export * from './file-size'; export * from './file-size';
export { default as Icons } from './icons'; export { default as Icons } from './icons';
export * from './resize-observer';
export * from './styles'; export * from './styles';
export * from './with-display-name'; export * from './with-display-name';

View file

@ -1,4 +1,4 @@
import React, { memo } from 'react'; import React, { memo, useCallback, useRef } from 'react';
export function withDisplayName(name: string) { export function withDisplayName(name: string) {
return <P extends object>(Component: React.FunctionComponent<P>) => { return <P extends object>(Component: React.FunctionComponent<P>) => {
@ -12,3 +12,14 @@ export function forwardRef<T>(name: string) {
return withDisplayName(name)(React.forwardRef(Component)); return withDisplayName(name)(React.forwardRef(Component));
}; };
} }
export function useCallbackRef<TArgs extends any[], R>(callback: (...args: TArgs) => R): (...args: TArgs) => R {
const ref = useRef<(...args: TArgs) => R>(callback);
ref.current = callback;
const wrapper = useCallback((...args: TArgs) => {
return ref.current.apply(undefined, args);
}, []);
return wrapper;
}

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,7 @@
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"devDependencies": { "devDependencies": {
"typescript": "next", "typescript": "4.7.0-beta",
"@types/jest": "^27.4.1", "@types/jest": "^27.4.1",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
}, },

View file

@ -38,7 +38,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^27.5.1", "jest": "^27.5.1",
"typescript": "next", "typescript": "4.7.0-beta",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
} }
} }

View file

@ -1,4 +1,4 @@
import { AdbPacketHeader, AdbPacketSerializeStream, DuplexStreamFactory, pipeFrom, ReadableStream, type AdbBackend, type AdbPacketCore, type AdbPacketInit, type ReadableWritablePair, type WritableStream } from '@yume-chan/adb'; import { AdbPacketHeader, AdbPacketSerializeStream, DuplexStreamFactory, pipeFrom, ReadableStream, type AdbBackend, type AdbPacketData, type AdbPacketInit, type ReadableWritablePair, type WritableStream } from '@yume-chan/adb';
import type { StructDeserializeStream } from "@yume-chan/struct"; import type { StructDeserializeStream } from "@yume-chan/struct";
export const ADB_DEVICE_FILTER: USBDeviceFilter = { export const ADB_DEVICE_FILTER: USBDeviceFilter = {
@ -24,21 +24,21 @@ class Uint8ArrayStructDeserializeStream implements StructDeserializeStream {
} }
} }
export class AdbWebUsbBackendStream implements ReadableWritablePair<AdbPacketCore, AdbPacketInit>{ export class AdbWebUsbBackendStream implements ReadableWritablePair<AdbPacketData, AdbPacketInit>{
private _readable: ReadableStream<AdbPacketCore>; private _readable: ReadableStream<AdbPacketData>;
public get readable() { return this._readable; } public get readable() { return this._readable; }
private _writable: WritableStream<AdbPacketInit>; private _writable: WritableStream<AdbPacketInit>;
public get writable() { return this._writable; } public get writable() { return this._writable; }
public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) { public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) {
const factory = new DuplexStreamFactory<AdbPacketCore, Uint8Array>({ const factory = new DuplexStreamFactory<AdbPacketData, Uint8Array>({
close: async () => { close: async () => {
navigator.usb.removeEventListener('disconnect', handleUsbDisconnect); navigator.usb.removeEventListener('disconnect', handleUsbDisconnect);
try { try {
await device.close(); await device.close();
} catch { } catch {
// device may already disconnected // device may have already disconnected
} }
}, },
}); });
@ -51,36 +51,43 @@ export class AdbWebUsbBackendStream implements ReadableWritablePair<AdbPacketCor
navigator.usb.addEventListener('disconnect', handleUsbDisconnect); navigator.usb.addEventListener('disconnect', handleUsbDisconnect);
this._readable = factory.createWrapReadable(new ReadableStream<AdbPacketCore>({ this._readable = factory.createWrapReadable(new ReadableStream<AdbPacketData>({
async pull(controller) { async pull(controller) {
// The `length` argument in `transferIn` must not be smaller than what the device sent, // The `length` argument in `transferIn` must not be smaller than what the device sent,
// otherwise it will return `babble` status without any data. // otherwise it will return `babble` status without any data.
// But using `inEndpoint.packetSize` as `length` (ensures it can read packets in any size) // Here we read exactly 24 bytes (packet header) followed by exactly `payloadLength`.
// leads to poor performance due to unnecessarily large allocations and corresponding GCs.
// So we read exactly 24 bytes (packet header) followed by exactly `payloadLength`.
const result = await device.transferIn(inEndpoint.endpointNumber, 24); const result = await device.transferIn(inEndpoint.endpointNumber, 24);
// TODO: webusb-backend: handle `babble` by discarding the data and receive again // TODO: webusb-backend: handle `babble` by discarding the data and receive again
// From spec, the `result.data` always covers the whole `buffer`. // From spec, the `result.data` always covers the whole `buffer`.
const buffer = new Uint8Array(result.data!.buffer); const buffer = new Uint8Array(result.data!.buffer);
const stream = new Uint8ArrayStructDeserializeStream(buffer); const stream = new Uint8ArrayStructDeserializeStream(buffer);
const packet = AdbPacketHeader.deserialize(stream);
// Add `payload` field to its type, because we will assign `payload` in next step.
const packet = AdbPacketHeader.deserialize(stream) as AdbPacketHeader & { payload: Uint8Array; };
if (packet.payloadLength !== 0) { if (packet.payloadLength !== 0) {
const payload = await device.transferIn(inEndpoint.endpointNumber, packet.payloadLength!); const result = await device.transferIn(inEndpoint.endpointNumber, packet.payloadLength);
// Use the cast to avoid allocate another object. packet.payload = new Uint8Array(result.data!.buffer);
(packet as unknown as AdbPacketCore).payload = new Uint8Array(payload.data!.buffer); } else {
packet.payload = new Uint8Array(0);
} }
controller.enqueue(packet as unknown as AdbPacketCore);
controller.enqueue(packet);
}, },
})); }));
this._writable = pipeFrom(factory.createWritable({ this._writable = pipeFrom(
factory.createWritable({
write: async (chunk) => { write: async (chunk) => {
await device.transferOut(outEndpoint.endpointNumber, chunk); await device.transferOut(outEndpoint.endpointNumber, chunk);
}, },
}, { }, {
highWaterMark: 16 * 1024, highWaterMark: 16 * 1024,
size(chunk) { return chunk.byteLength; }, size(chunk) { return chunk.byteLength; },
}), new AdbPacketSerializeStream()); }),
new AdbPacketSerializeStream()
);
} }
} }
@ -130,7 +137,7 @@ export class AdbWebUsbBackend implements AdbBackend {
alternate.interfaceClass === ADB_DEVICE_FILTER.classCode && alternate.interfaceClass === ADB_DEVICE_FILTER.classCode &&
alternate.interfaceSubclass === ADB_DEVICE_FILTER.subclassCode) { alternate.interfaceSubclass === ADB_DEVICE_FILTER.subclassCode) {
if (this._device.configuration?.configurationValue !== configuration.configurationValue) { if (this._device.configuration?.configurationValue !== configuration.configurationValue) {
// Note: It's not possible to switch configuration on Windows, // Note: Switching configuration is not supported on Windows,
// but Android devices should always expose ADB function at the first (default) configuration. // but Android devices should always expose ADB function at the first (default) configuration.
await this._device.selectConfiguration(configuration.configurationValue); await this._device.selectConfiguration(configuration.configurationValue);
} }

View file

@ -31,7 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^27.5.1", "jest": "^27.5.1",
"typescript": "next", "typescript": "4.7.0-beta",
"@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": "next", "typescript": "4.7.0-beta",
"@yume-chan/ts-package-builder": "^1.0.0" "@yume-chan/ts-package-builder": "^1.0.0"
}, },
"dependencies": { "dependencies": {

View file

@ -45,6 +45,6 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jest": "^27.5.1", "jest": "^27.5.1",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"typescript": "next" "typescript": "4.7.0-beta"
} }
} }

View file

@ -1,9 +1,11 @@
// cspell: ignore libusb
import { PromiseResolver } from '@yume-chan/async'; import { PromiseResolver } from '@yume-chan/async';
import { AdbAuthenticationHandler, AdbDefaultAuthenticators, type AdbCredentialStore } from './auth.js'; import { AdbAuthenticationProcessor, ADB_DEFAULT_AUTHENTICATORS, type AdbCredentialStore } from './auth.js';
import { AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install, type AdbFrameBuffer } from './commands/index.js'; import { AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install, type AdbFrameBuffer } from './commands/index.js';
import { AdbFeatures } from './features.js'; import { AdbFeatures } from './features.js';
import { AdbCommand, AdbPacket, calculateChecksum, type AdbPacketCore, type AdbPacketInit } from './packet.js'; import { AdbCommand, calculateChecksum, type AdbPacketData, type AdbPacketInit } from './packet.js';
import { AdbPacketDispatcher, AdbSocket } from './socket/index.js'; import { AdbPacketDispatcher, type AdbSocket } from './socket/index.js';
import { AbortController, DecodeUtf8Stream, GatherStringStream, WritableStream, type ReadableWritablePair } from "./stream/index.js"; import { AbortController, DecodeUtf8Stream, GatherStringStream, WritableStream, type ReadableWritablePair } from "./stream/index.js";
import { decodeUtf8, encodeUtf8 } from "./utils/index.js"; import { decodeUtf8, encodeUtf8 } from "./utils/index.js";
@ -23,13 +25,60 @@ export class Adb {
* and starts a new authentication process. * and starts a new authentication process.
*/ */
public static async authenticate( public static async authenticate(
connection: ReadableWritablePair<AdbPacketCore, AdbPacketCore>, connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
credentialStore: AdbCredentialStore, credentialStore: AdbCredentialStore,
authenticators = AdbDefaultAuthenticators, authenticators = ADB_DEFAULT_AUTHENTICATORS,
): Promise<Adb> { ): Promise<Adb> {
// Initially, set to highest-supported version and payload size.
let version = 0x01000001; let version = 0x01000001;
let maxPayloadSize = 0x100000; let maxPayloadSize = 0x100000;
const resolver = new PromiseResolver<string>();
const authProcessor = new AdbAuthenticationProcessor(authenticators, credentialStore);
// Here is similar to `AdbPacketDispatcher`,
// But the received packet types and send packet processing are different.
const abortController = new AbortController();
const pipe = connection.readable
.pipeTo(new WritableStream({
async write(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 authProcessor.process(packet);
await sendPacket(response);
break;
default:
// Maybe the previous ADB session exited without reading all packets,
// so they are still waiting in OS internal buffer.
// Just ignore them.
// Because a `Connect` packet will reset the device,
// Eventually there will be `Connect` and `Auth` response packets.
break;
}
}
}), {
preventCancel: true,
signal: abortController.signal,
})
.catch((e) => {
resolver.reject(e);
});
const writer = connection.writable.getWriter();
async function sendPacket(init: AdbPacketData) {
// Always send checksum in auth steps
// Because we don't know if the device needs it or not.
await writer.write(calculateChecksum(init));
}
try {
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
// There are more feature constants, but some of them are only used by ADB server, not devices.
const features = [ const features = [
'shell_v2', 'shell_v2',
'cmd', 'cmd',
@ -49,44 +98,6 @@ export class Adb {
'sendrecv_v2_dry_run_send', 'sendrecv_v2_dry_run_send',
].join(','); ].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) {
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) {
// 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({ await sendPacket({
command: AdbCommand.Connect, command: AdbCommand.Connect,
arg0: version, arg0: version,
@ -96,25 +107,26 @@ export class Adb {
payload: encodeUtf8(`host::features=${features};`), payload: encodeUtf8(`host::features=${features};`),
}); });
try {
const banner = await resolver.promise; const banner = await resolver.promise;
// Stop piping before creating Adb object // Stop piping before creating `Adb` object
// Because AdbPacketDispatcher will try to lock the streams when initializing // Because `AdbPacketDispatcher` will lock the streams when initializing
abortController.abort(); abortController.abort();
await pipe;
writer.releaseLock(); writer.releaseLock();
// Wait until pipe stops (`ReadableStream` lock released)
await pipe;
return new Adb( return new Adb(
connection, connection,
version, version,
maxPayloadSize, maxPayloadSize,
banner, banner,
); );
} finally { } catch (e) {
abortController.abort(); abortController.abort();
writer.releaseLock(); writer.releaseLock();
throw e;
} }
} }
@ -143,22 +155,33 @@ export class Adb {
public readonly tcpip: AdbTcpIpCommand; public readonly tcpip: AdbTcpIpCommand;
public constructor( public constructor(
connection: ReadableWritablePair<AdbPacketCore, AdbPacketInit>, connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
version: number, version: number,
maxPayloadSize: number, maxPayloadSize: number,
banner: string, banner: string,
) { ) {
this.parseBanner(banner); this.parseBanner(banner);
this.packetDispatcher = new AdbPacketDispatcher(connection);
let calculateChecksum: boolean;
let appendNullToServiceString: boolean;
if (version >= VERSION_OMIT_CHECKSUM) {
calculateChecksum = false;
appendNullToServiceString = false;
} else {
calculateChecksum = true;
appendNullToServiceString = true;
}
this.packetDispatcher = new AdbPacketDispatcher(
connection,
{
calculateChecksum,
appendNullToServiceString,
maxPayloadSize,
}
);
this._protocolVersion = version; 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);
@ -201,6 +224,19 @@ export class Adb {
} }
} }
public async createSocket(service: string): Promise<AdbSocket> {
return this.packetDispatcher.createSocket(service);
}
public async createSocketAndWait(service: string): Promise<string> {
const socket = await this.createSocket(service);
const gatherStream = new GatherStringStream();
await socket.readable
.pipeThrough(new DecodeUtf8Stream())
.pipeTo(gatherStream);
return gatherStream.result;
}
public async getProp(key: string): Promise<string> { public async getProp(key: string): Promise<string> {
const stdout = await this.subprocess.spawnAndWaitLegacy( const stdout = await this.subprocess.spawnAndWaitLegacy(
['getprop', key] ['getprop', key]
@ -228,19 +264,6 @@ export class Adb {
return framebuffer(this); return framebuffer(this);
} }
public async createSocket(service: string): Promise<AdbSocket> {
return this.packetDispatcher.createSocket(service);
}
public async createSocketAndWait(service: string): Promise<string> {
const socket = await this.createSocket(service);
const gatherStream = new GatherStringStream();
await socket.readable
.pipeThrough(new DecodeUtf8Stream())
.pipeTo(gatherStream);
return gatherStream.result;
}
public async dispose(): Promise<void> { public async dispose(): Promise<void> {
this.packetDispatcher.dispose(); this.packetDispatcher.dispose();
} }

View file

@ -2,7 +2,7 @@ import { PromiseResolver } from '@yume-chan/async';
import type { Disposable } from '@yume-chan/event'; import type { Disposable } from '@yume-chan/event';
import type { ValueOrPromise } from '@yume-chan/struct'; import type { ValueOrPromise } from '@yume-chan/struct';
import { calculatePublicKey, calculatePublicKeyLength, sign } from './crypto.js'; import { calculatePublicKey, calculatePublicKeyLength, sign } from './crypto.js';
import { AdbCommand, AdbPacket, type AdbPacketCore } from './packet.js'; import { AdbCommand, type AdbPacketData } from './packet.js';
import { calculateBase64EncodedLength, encodeBase64 } from './utils/index.js'; import { calculateBase64EncodedLength, encodeBase64 } from './utils/index.js';
export type AdbKeyIterable = Iterable<Uint8Array> | AsyncIterable<Uint8Array>; export type AdbKeyIterable = Iterable<Uint8Array> | AsyncIterable<Uint8Array>;
@ -43,14 +43,14 @@ export interface AdbAuthenticator {
*/ */
( (
credentialStore: AdbCredentialStore, credentialStore: AdbCredentialStore,
getNextRequest: () => Promise<AdbPacket> getNextRequest: () => Promise<AdbPacketData>
): AsyncIterable<AdbPacketCore>; ): AsyncIterable<AdbPacketData>;
} }
export const AdbSignatureAuthenticator: AdbAuthenticator = async function* ( export const AdbSignatureAuthenticator: AdbAuthenticator = async function* (
credentialStore: AdbCredentialStore, credentialStore: AdbCredentialStore,
getNextRequest: () => Promise<AdbPacket>, getNextRequest: () => Promise<AdbPacketData>,
): AsyncIterable<AdbPacketCore> { ): AsyncIterable<AdbPacketData> {
for await (const key of credentialStore.iterateKeys()) { for await (const key of credentialStore.iterateKeys()) {
const packet = await getNextRequest(); const packet = await getNextRequest();
@ -70,8 +70,8 @@ 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<AdbPacketData>,
): AsyncIterable<AdbPacketCore> { ): AsyncIterable<AdbPacketData> {
const packet = await getNextRequest(); const packet = await getNextRequest();
if (packet.arg0 !== AdbAuthType.Token) { if (packet.arg0 !== AdbAuthType.Token) {
@ -110,19 +110,19 @@ export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
}; };
}; };
export const AdbDefaultAuthenticators: AdbAuthenticator[] = [ export const ADB_DEFAULT_AUTHENTICATORS: AdbAuthenticator[] = [
AdbSignatureAuthenticator, AdbSignatureAuthenticator,
AdbPublicKeyAuthenticator, AdbPublicKeyAuthenticator,
]; ];
export class AdbAuthenticationHandler implements Disposable { export class AdbAuthenticationProcessor implements Disposable {
public readonly authenticators: readonly AdbAuthenticator[]; public readonly authenticators: readonly AdbAuthenticator[];
private readonly credentialStore: AdbCredentialStore; private readonly credentialStore: AdbCredentialStore;
private pendingRequest = new PromiseResolver<AdbPacket>(); private pendingRequest = new PromiseResolver<AdbPacketData>();
private iterator: AsyncIterator<AdbPacketCore> | undefined; private iterator: AsyncIterator<AdbPacketData, void, void> | undefined;
public constructor( public constructor(
authenticators: readonly AdbAuthenticator[], authenticators: readonly AdbAuthenticator[],
@ -132,16 +132,16 @@ export class AdbAuthenticationHandler implements Disposable {
this.credentialStore = credentialStore; this.credentialStore = credentialStore;
} }
private getNextRequest = (): Promise<AdbPacket> => { private getNextRequest = (): Promise<AdbPacketData> => {
return this.pendingRequest.promise; return this.pendingRequest.promise;
}; };
private async* runAuthenticator(): AsyncGenerator<AdbPacketCore> { private async* invokeAuthenticator(): AsyncGenerator<AdbPacketData, void, void> {
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
// Prepare `nextRequest` for next authentication request // Prepare `nextRequest` for next authentication request
this.pendingRequest = new PromiseResolver<AdbPacket>(); this.pendingRequest = new PromiseResolver();
// Yield the response to outer layer // Yield the response to outer layer
yield packet; yield packet;
@ -150,17 +150,20 @@ export class AdbAuthenticationHandler implements Disposable {
// If the authenticator returned, // If the authenticator returned,
// Next authenticator will be given the same `pendingRequest` // Next authenticator will be given the same `pendingRequest`
} }
throw new Error('Cannot authenticate with device');
} }
public async handle(packet: AdbPacket): Promise<AdbPacketCore> { public async process(packet: AdbPacketData): Promise<AdbPacketData> {
if (!this.iterator) { if (!this.iterator) {
this.iterator = this.runAuthenticator(); this.iterator = this.invokeAuthenticator();
} }
this.pendingRequest.resolve(packet); this.pendingRequest.resolve(packet);
const result = await this.iterator.next(); const result = await this.iterator.next();
if (result.done) {
throw new Error('Cannot authenticate with device');
}
return result.value; return result.value;
} }

View file

@ -1,5 +1,5 @@
import type { ValueOrPromise } from '@yume-chan/struct'; import type { ValueOrPromise } from '@yume-chan/struct';
import type { AdbPacketCore, AdbPacketInit } from "./packet.js"; import type { AdbPacketData, AdbPacketInit } from "./packet.js";
import type { ReadableWritablePair } from "./stream/index.js"; import type { ReadableWritablePair } from "./stream/index.js";
export interface AdbBackend { export interface AdbBackend {
@ -7,5 +7,5 @@ export interface AdbBackend {
readonly name: string | undefined; readonly name: string | undefined;
connect(): ValueOrPromise<ReadableWritablePair<AdbPacketCore, AdbPacketInit>>; connect(): ValueOrPromise<ReadableWritablePair<AdbPacketData, AdbPacketInit>>;
} }

View file

@ -11,6 +11,8 @@ export function install(
let sync!: AdbSync; let sync!: AdbSync;
return new WrapWritableStream<Uint8Array>({ return new WrapWritableStream<Uint8Array>({
async start() { async start() {
// TODO: install: support other install apk methods (streaming, etc.)
// Upload apk file to tmp folder // Upload apk file to tmp folder
sync = await adb.sync(); sync = await adb.sync();
return sync.write(filename, undefined, undefined); return sync.write(filename, undefined, undefined);

View file

@ -2,13 +2,13 @@
import { AutoDisposable } from '@yume-chan/event'; import { AutoDisposable } from '@yume-chan/event';
import Struct from '@yume-chan/struct'; import Struct from '@yume-chan/struct';
import type { AdbPacketCore } from '../packet.js'; import type { AdbPacketData } from '../packet.js';
import type { AdbIncomingSocketEventArgs, AdbPacketDispatcher, AdbSocket } from '../socket/index.js'; import type { AdbIncomingSocketEventArgs, AdbPacketDispatcher, AdbSocket } from '../socket/index.js';
import { AdbBufferedStream } from '../stream/index.js'; import { AdbBufferedStream } from '../stream/index.js';
import { decodeUtf8 } from "../utils/index.js"; import { decodeUtf8 } from "../utils/index.js";
export interface AdbReverseHandler { export interface AdbReverseHandler {
onSocket(packet: AdbPacketCore, socket: AdbSocket): void; onSocket(packet: AdbPacketData, socket: AdbSocket): void;
} }
export interface AdbForwardListener { export interface AdbForwardListener {

View file

@ -1,34 +1,30 @@
import type { Adb } from '../../adb.js'; import type { Adb } from '../../adb.js';
import { DecodeUtf8Stream, GatherStringStream } from "../../stream/index.js"; import { DecodeUtf8Stream, GatherStringStream } from "../../stream/index.js";
import { AdbNoneSubprocessProtocol } from './legacy.js'; import { AdbSubprocessNoneProtocol, AdbSubprocessShellProtocol, type AdbSubprocessProtocol, type AdbSubprocessProtocolConstructor } from './protocols/index.js';
import { AdbShellSubprocessProtocol } from './protocol.js';
import type { AdbSubprocessProtocol, AdbSubprocessProtocolConstructor } from './types.js';
export * from './legacy.js'; export * from './protocols/index.js';
export * from './protocol.js';
export * from './types.js';
export * from './utils.js'; export * from './utils.js';
export interface AdbSubprocessOptions { export interface AdbSubprocessOptions {
/** /**
* A list of `AdbShellConstructor`s to be used. * A list of `AdbSubprocessProtocolConstructor`s to be used.
* *
* Different `AdbShell` has different capabilities, thus requires specific adaptations. * Different `AdbSubprocessProtocol` has different capabilities, thus requires specific adaptations.
* Check each `AdbShell`'s documentation for details. * Check their documentations for details.
* *
* The first one whose `isSupported` returns `true` will be used. * The first protocol whose `isSupported` returns `true` will be used.
* If no `AdbShell` is supported, an error will be thrown. * If no `AdbSubprocessProtocol` is supported, an error will be thrown.
* *
* The default value is `[AdbShellProtocol, AdbLegacyShell]`. * @default [AdbSubprocessShellProtocol, AdbSubprocessNoneProtocol]
*/ */
protocols: AdbSubprocessProtocolConstructor[]; protocols: AdbSubprocessProtocolConstructor[];
} }
const DefaultOptions: AdbSubprocessOptions = { const DEFAULT_OPTIONS: AdbSubprocessOptions = {
protocols: [AdbShellSubprocessProtocol, AdbNoneSubprocessProtocol], protocols: [AdbSubprocessShellProtocol, AdbSubprocessNoneProtocol],
}; };
export interface SubprocessResult { export interface AdbSubprocessWaitResult {
stdout: string; stdout: string;
stderr: string; stderr: string;
exitCode: number; exitCode: number;
@ -41,11 +37,16 @@ export class AdbSubprocess {
this.adb = adb; this.adb = adb;
} }
private async createProtocol(mode: 'pty' | 'raw', command?: string | string[], options?: Partial<AdbSubprocessOptions>): Promise<AdbSubprocessProtocol> { private async createProtocol(
let { protocols } = { ...DefaultOptions, ...options }; mode: 'pty' | 'raw',
command?: string | string[],
options?: Partial<AdbSubprocessOptions>
): Promise<AdbSubprocessProtocol> {
const { protocols } = { ...DEFAULT_OPTIONS, ...options };
let Constructor: AdbSubprocessProtocolConstructor | undefined; let Constructor: AdbSubprocessProtocolConstructor | undefined;
for (const item of protocols) { for (const item of protocols) {
// It's async so can't use `Array#find`
if (await item.isSupported(this.adb)) { if (await item.isSupported(this.adb)) {
Constructor = item; Constructor = item;
break; break;
@ -59,40 +60,48 @@ export class AdbSubprocess {
if (Array.isArray(command)) { if (Array.isArray(command)) {
command = command.join(' '); command = command.join(' ');
} else if (command === undefined) { } else if (command === undefined) {
// spawn the default shell
command = ''; command = '';
} }
return await Constructor[mode](this.adb, command); return await Constructor[mode](this.adb, command);
} }
/** /**
* Spawns the default shell in interactive mode. * Spawns an executable in PTY (interactive) mode.
* @param options The options for creating the `AdbShell` * @param command The command to run. If omitted, the default shell will be spawned.
* @returns A new `AdbShell` instance connecting to the spawned shell process. * @param options The options for creating the `AdbSubprocessProtocol`
* @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
*/ */
public shell(command?: string | string[], options?: Partial<AdbSubprocessOptions>): Promise<AdbSubprocessProtocol> { public shell(
command?: string | string[],
options?: Partial<AdbSubprocessOptions>
): Promise<AdbSubprocessProtocol> {
return this.createProtocol('pty', command, options); return this.createProtocol('pty', command, options);
} }
/** /**
* Spawns a new process using the given `command`. * Spawns an executable and pipe the output.
* @param command The command to run, or an array of strings containing both command and args. * @param command The command to run, or an array of strings containing both command and args.
* @param options The options for creating the `AdbShell` * @param options The options for creating the `AdbSubprocessProtocol`
* @returns A new `AdbShell` instance connecting to the spawned process. * @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
*/ */
public spawn(command: string | string[], options?: Partial<AdbSubprocessOptions>): Promise<AdbSubprocessProtocol> { public spawn(
command: string | string[],
options?: Partial<AdbSubprocessOptions>
): Promise<AdbSubprocessProtocol> {
return this.createProtocol('raw', command, options); return this.createProtocol('raw', command, options);
} }
/** /**
* Spawns a new process, waits until it exits, and returns the entire output. * Spawns a new process, waits until it exits, and returns the entire output.
* @param command The command to run * @param command The command to run
* @param options The options for creating the `AdbShell` * @param options The options for creating the `AdbSubprocessProtocol`
* @returns The entire output of the command * @returns The entire output of the command
*/ */
public async spawnAndWait( public async spawnAndWait(
command: string | string[], command: string | string[],
options?: Partial<AdbSubprocessOptions> options?: Partial<AdbSubprocessOptions>
): Promise<SubprocessResult> { ): Promise<AdbSubprocessWaitResult> {
const shell = await this.spawn(command, options); const shell = await this.spawn(command, options);
const stdout = new GatherStringStream(); const stdout = new GatherStringStream();
@ -121,7 +130,10 @@ export class AdbSubprocess {
* @returns The entire output of the command * @returns The entire output of the command
*/ */
public async spawnAndWaitLegacy(command: string | string[]): Promise<string> { public async spawnAndWaitLegacy(command: string | string[]): Promise<string> {
const { stdout } = await this.spawnAndWait(command, { protocols: [AdbNoneSubprocessProtocol] }); const { stdout } = await this.spawnAndWait(
command,
{ protocols: [AdbSubprocessNoneProtocol] }
);
return stdout; return stdout;
} }
} }

View file

@ -0,0 +1,3 @@
export * from './none.js';
export * from './shell.js';
export * from './types.js';

View file

@ -1,6 +1,6 @@
import type { Adb } from "../../adb.js"; import type { Adb } from "../../../adb.js";
import type { AdbSocket } from "../../socket/index.js"; import type { AdbSocket } from "../../../socket/index.js";
import { DuplexStreamFactory, type ReadableStream } from "../../stream/index.js"; import { DuplexStreamFactory, type ReadableStream } from "../../../stream/index.js";
import type { AdbSubprocessProtocol } from "./types.js"; import type { AdbSubprocessProtocol } from "./types.js";
/** /**
@ -11,15 +11,17 @@ import type { AdbSubprocessProtocol } from "./types.js";
* * `exit` exit code: No * * `exit` exit code: No
* * `resize`: No * * `resize`: No
*/ */
export class AdbNoneSubprocessProtocol implements AdbSubprocessProtocol { export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
public static isSupported() { return true; } public static isSupported() { return true; }
public static async pty(adb: Adb, command: string) { public static async pty(adb: Adb, command: string) {
return new AdbNoneSubprocessProtocol(await adb.createSocket(`shell:${command}`)); return new AdbSubprocessNoneProtocol(await adb.createSocket(`shell:${command}`));
} }
public static async raw(adb: Adb, command: string) { public static async raw(adb: Adb, command: string) {
return new AdbNoneSubprocessProtocol(await adb.createSocket(`shell,raw:${command}`)); // Native ADB client doesn't allow none protocol + raw mode,
// But ADB daemon supports it.
return new AdbSubprocessNoneProtocol(await adb.createSocket(`shell,raw:${command}`));
} }
private readonly socket: AdbSocket; private readonly socket: AdbSocket;
@ -28,11 +30,15 @@ export class AdbNoneSubprocessProtocol implements AdbSubprocessProtocol {
public get stdin() { return this.socket.writable; } public get stdin() { return this.socket.writable; }
private _stdout: ReadableStream<Uint8Array>; private _stdout: ReadableStream<Uint8Array>;
// Legacy shell doesn't support splitting output streams. /**
* Legacy shell mixes stdout and stderr.
*/
public get stdout() { return this._stdout; } public get stdout() { return this._stdout; }
// `stderr` of Legacy shell is always empty.
private _stderr: ReadableStream<Uint8Array>; private _stderr: ReadableStream<Uint8Array>;
/**
* `stderr` will always be empty.
*/
public get stderr() { return this._stderr; } public get stderr() { return this._stderr; }
private _exit: Promise<number>; private _exit: Promise<number>;

View file

@ -1,10 +1,10 @@
import { PromiseResolver } from "@yume-chan/async"; import { PromiseResolver } from "@yume-chan/async";
import Struct, { placeholder, type StructValueType } from "@yume-chan/struct"; import Struct, { placeholder, type StructValueType } from "@yume-chan/struct";
import type { Adb } from "../../adb.js"; import type { Adb } from "../../../adb.js";
import { AdbFeatures } from "../../features.js"; import { AdbFeatures } from "../../../features.js";
import type { AdbSocket } from "../../socket/index.js"; import type { AdbSocket } from "../../../socket/index.js";
import { PushReadableStream, ReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter, type PushReadableStreamController } from "../../stream/index.js"; import { PushReadableStream, ReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter, type PushReadableStreamController } from "../../../stream/index.js";
import { encodeUtf8 } from "../../utils/index.js"; import { encodeUtf8 } from "../../../utils/index.js";
import type { AdbSubprocessProtocol } from "./types.js"; import type { AdbSubprocessProtocol } from "./types.js";
export enum AdbShellProtocolId { export enum AdbShellProtocolId {
@ -17,7 +17,8 @@ export enum AdbShellProtocolId {
} }
// This packet format is used in both direction. // This packet format is used in both direction.
const AdbShellProtocolPacket = new Struct({ littleEndian: true }) const AdbShellProtocolPacket =
new Struct({ littleEndian: true })
.uint8('id', placeholder<AdbShellProtocolId>()) .uint8('id', placeholder<AdbShellProtocolId>())
.uint32('length') .uint32('length')
.uint8Array('data', { lengthField: 'length' }); .uint8Array('data', { lengthField: 'length' });
@ -99,18 +100,18 @@ class MultiplexStream<T>{
* * `exit` exit code: Yes * * `exit` exit code: Yes
* * `resize`: Yes * * `resize`: Yes
*/ */
export class AdbShellSubprocessProtocol implements AdbSubprocessProtocol { export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
public static isSupported(adb: Adb) { public static isSupported(adb: Adb) {
return adb.features!.includes(AdbFeatures.ShellV2); return adb.features!.includes(AdbFeatures.ShellV2);
} }
public static async pty(adb: Adb, command: string) { public static async pty(adb: Adb, command: string) {
// TODO: AdbShellSubprocessProtocol: Support setting `XTERM` environment variable // TODO: AdbShellSubprocessProtocol: Support setting `XTERM` environment variable
return new AdbShellSubprocessProtocol(await adb.createSocket(`shell,v2,pty:${command}`)); return new AdbSubprocessShellProtocol(await adb.createSocket(`shell,v2,pty:${command}`));
} }
public static async raw(adb: Adb, command: string) { public static async raw(adb: Adb, command: string) {
return new AdbShellSubprocessProtocol(await adb.createSocket(`shell,v2,raw:${command}`)); return new AdbSubprocessShellProtocol(await adb.createSocket(`shell,v2,raw:${command}`));
} }
private readonly _socket: AdbSocket; private readonly _socket: AdbSocket;

View file

@ -1,7 +1,7 @@
import type { ValueOrPromise } from "@yume-chan/struct"; import type { ValueOrPromise } from "@yume-chan/struct";
import type { Adb } from "../../adb.js"; import type { Adb } from "../../../adb.js";
import type { AdbSocket } from "../../socket/index.js"; import type { AdbSocket } from "../../../socket/index.js";
import type { ReadableStream, WritableStream } from "../../stream/index.js"; import type { ReadableStream, WritableStream } from "../../../stream/index.js";
export interface AdbSubprocessProtocol { export interface AdbSubprocessProtocol {
/** /**
@ -17,7 +17,7 @@ export interface AdbSubprocessProtocol {
/** /**
* The `stderr` pipe of the process. * The `stderr` pipe of the process.
* *
* Note: Some `AdbShell` doesn't separate `stdout` and `stderr`, * Note: Some `AdbSubprocessProtocol` doesn't separate `stdout` and `stderr`,
* All output will be sent to `stdout`. * All output will be sent to `stdout`.
*/ */
readonly stderr: ReadableStream<Uint8Array>; readonly stderr: ReadableStream<Uint8Array>;
@ -25,15 +25,16 @@ export interface AdbSubprocessProtocol {
/** /**
* A `Promise` that resolves to the exit code of the process. * A `Promise` that resolves to the exit code of the process.
* *
* Note: Some `AdbShell` doesn't support exit code, * Note: Some `AdbSubprocessProtocol` doesn't support exit code,
* They will always resolve with `0`. * They will always resolve it with `0`.
*/ */
readonly exit: Promise<number>; readonly exit: Promise<number>;
/** /**
* Resizes the current shell. * Resizes the current shell.
* *
* Some `AdbShell`s may not support resizing and will always ignore calls to this method. * Some `AdbSubprocessProtocol`s may not support resizing
* and will ignore calls to this method.
*/ */
resize(rows: number, cols: number): ValueOrPromise<void>; resize(rows: number, cols: number): ValueOrPromise<void>;
@ -47,9 +48,10 @@ export interface AdbSubprocessProtocolConstructor {
/** Returns `true` if the `adb` instance supports this shell */ /** Returns `true` if the `adb` instance supports this shell */
isSupported(adb: Adb): ValueOrPromise<boolean>; isSupported(adb: Adb): ValueOrPromise<boolean>;
/** Creates a new `AdbShell` using the specified `Adb` and `command` */ /** Spawns an executable in PTY (interactive) mode. */
pty(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>; pty(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
/** Spawns an executable and pipe the output. */
raw(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>; raw(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
/** Creates a new `AdbShell` by attaching to an exist `AdbSocket` */ /** Creates a new `AdbShell` by attaching to an exist `AdbSocket` */

View file

@ -25,6 +25,9 @@ export function adbSyncPush(
return pipeFrom( return pipeFrom(
new WritableStream<Uint8Array>({ new WritableStream<Uint8Array>({
async start() { async start() {
// TODO: AdbSyncPush: support create directory using `mkdir`
// if device doesn't support `fixed_push_mkdir` feature.
const pathAndMode = `${filename},${mode.toString()}`; const pathAndMode = `${filename},${mode.toString()}`;
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode); await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode);
}, },

View file

@ -5,48 +5,60 @@ const BigInt1 = BigInt(1);
const BigInt2 = BigInt(2); const BigInt2 = BigInt(2);
const BigInt64 = BigInt(64); const BigInt64 = BigInt(64);
export function getBig( /**
array: Uint8Array, * Gets the `BigInt` value at the specified byte offset and length from the start of the view. There is
offset = 0, * no alignment constraint; multi-byte values may be fetched from any offset.
length = array.byteLength - offset *
): bigint { * Only supports Big-Endian, because that's what ADB uses.
const view = new DataView(array.buffer, array.byteOffset, array.byteLength); * @param byteOffset The place in the buffer at which the value should be retrieved.
*/
export function getBigUint(dataView: DataView, byteOffset: number, length: number): bigint {
let result = BigInt0; let result = BigInt0;
// Currently `length` must be a multiplication of 8 // Currently `length` must be a multiplication of 8
// Support for arbitrary length can be easily added // Support for arbitrary length can be easily added
for (let i = offset; i < offset + length; i += 8) { for (let i = byteOffset; i < byteOffset + length; i += 8) {
result <<= BigInt64; result <<= BigInt64;
const value = getBigUint64(view, i, false); const value = getBigUint64(dataView, i, false);
result += value; result += value;
} }
return result; return result;
} }
export function setBig(buffer: ArrayBuffer, value: bigint, offset: number = 0) { /**
* Stores an arbitrary-precision positive `BigInt` value at the specified byte offset from the start of the view.
* @param byteOffset The place in the buffer at which the value should be set.
* @param value The value to set.
* @param littleEndian If `false` or `undefined`, a big-endian value should be written,
* otherwise a little-endian value should be written.
*/
export function setBigUint(dataView: DataView, byteOffset: number, value: bigint, littleEndian?: boolean) {
const start = byteOffset;
if (littleEndian) {
while (value > BigInt0) {
setBigUint64(dataView, byteOffset, value, true);
byteOffset += 8;
value >>= BigInt64;
}
} else {
// Because we don't know how long (in bits) the `value` is,
// Convert it to an array of `uint64` first.
const uint64Array: bigint[] = []; const uint64Array: bigint[] = [];
while (value > BigInt0) { while (value > BigInt0) {
uint64Array.push(BigInt.asUintN(64, value)); uint64Array.push(BigInt.asUintN(64, value));
value >>= BigInt64; value >>= BigInt64;
} }
const view = new DataView(buffer);
for (let i = uint64Array.length - 1; i >= 0; i -= 1) { for (let i = uint64Array.length - 1; i >= 0; i -= 1) {
setBigUint64(view, offset, uint64Array[i]!, false); setBigUint64(dataView, byteOffset, uint64Array[i]!, false);
offset += 8; byteOffset += 8;
} }
} }
export function setBigLE(array: Uint8Array, value: bigint, offset = 0) { return byteOffset - start;
const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
while (value > BigInt0) {
setBigUint64(view, offset, value, true);
offset += 8;
value >>= BigInt64;
}
} }
// These values are correct only if // These values are correct only if
@ -76,9 +88,9 @@ const RsaPrivateKeyDOffset = 303;
const RsaPrivateKeyDLength = 2048 / 8; const RsaPrivateKeyDLength = 2048 / 8;
export function parsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] { export function parsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] {
let n = getBig(key, RsaPrivateKeyNOffset, RsaPrivateKeyNLength); const view = new DataView(key.buffer, key.byteOffset, key.byteLength);
let d = getBig(key, RsaPrivateKeyDOffset, RsaPrivateKeyDLength); const n = getBigUint(view, RsaPrivateKeyNOffset, RsaPrivateKeyNLength);
const d = getBigUint(view, RsaPrivateKeyDOffset, RsaPrivateKeyDLength);
return [n, d]; return [n, d];
} }
@ -132,7 +144,7 @@ export function calculatePublicKey(
// [ // [
// modulusLengthInWords, // 32-bit integer, a "word" is 32-bit so it must be 2048 / 8 / 4 // modulusLengthInWords, // 32-bit integer, a "word" is 32-bit so it must be 2048 / 8 / 4
// // Actually the comment in Android source code was wrong // // Actually the comment in Android source code was wrong
// n0inv, // 32-bit integer, the modular inverse of (lower 32 bits of) n // n0inv, // 32-bit integer, the modular inverse of (low 32 bits of n)
// modulus, // n // modulus, // n
// rr, // Montgomery parameter R^2 // rr, // Montgomery parameter R^2
// exponent, // 32-bit integer, must be 65537 // exponent, // 32-bit integer, must be 65537
@ -172,13 +184,12 @@ export function calculatePublicKey(
outputOffset += 4; outputOffset += 4;
// Write n // Write n
setBigLE(output, n, outputOffset); setBigUint(outputView, outputOffset, n, true);
outputOffset += 256; outputOffset += 256;
// Calculate rr = (2^(rsa_size)) ^ 2 mod n // Calculate rr = (2^(rsa_size)) ^ 2 mod n
let rr = BigInt(2) ** BigInt(4096) % n; let rr = BigInt(2) ** BigInt(4096) % n;
setBigLE(output, rr, outputOffset); outputOffset += setBigUint(outputView, outputOffset, rr, true);
outputOffset += 256;
// exponent // exponent
outputView.setUint32(outputOffset, 65537, true); outputView.setUint32(outputOffset, 65537, true);
@ -218,21 +229,21 @@ export function powMod(base: bigint, exponent: bigint, modulus: bigint): bigint
return r; return r;
} }
export const Sha1DigestLength = 20; export const SHA1_DIGEST_LENGTH = 20;
export const Asn1Sequence = 0x30; export const ASN1_SEQUENCE = 0x30;
export const Asn1OctetString = 0x04; export const ASN1_OCTET_STRING = 0x04;
export const Asn1Null = 0x05; export const ASN1_NULL = 0x05;
export const Asn1Oid = 0x06; export const ASN1_OID = 0x06;
// PKCS#1 SHA-1 hash digest info // PKCS#1 SHA-1 hash digest info
export const Sha1DigestInfo = new Uint8Array([ export const SHA1_DIGEST_INFO = new Uint8Array([
Asn1Sequence, 0x0d + Sha1DigestLength, ASN1_SEQUENCE, 0x0d + SHA1_DIGEST_LENGTH,
Asn1Sequence, 0x09, ASN1_SEQUENCE, 0x09,
// SHA-1 (1 3 14 3 2 26) // SHA-1 (1 3 14 3 2 26)
Asn1Oid, 0x05, 1 * 40 + 3, 14, 3, 2, 26, ASN1_OID, 0x05, 1 * 40 + 3, 14, 3, 2, 26,
Asn1Null, 0x00, ASN1_NULL, 0x00,
Asn1OctetString, Sha1DigestLength ASN1_OCTET_STRING, SHA1_DIGEST_LENGTH
]); ]);
// SubtleCrypto.sign() will hash the given data and sign the hash // SubtleCrypto.sign() will hash the given data and sign the hash
@ -241,7 +252,7 @@ export const Sha1DigestInfo = new Uint8Array([
// encrypt the given data with its private key) // encrypt the given data with its private key)
// However SubtileCrypto.encrypt() doesn't accept 'RSASSA-PKCS1-v1_5' algorithm // However SubtileCrypto.encrypt() doesn't accept 'RSASSA-PKCS1-v1_5' algorithm
// So we need to implement the encryption by ourself // So we need to implement the encryption by ourself
export function sign(privateKey: Uint8Array, data: Uint8Array): ArrayBuffer { export function sign(privateKey: Uint8Array, data: Uint8Array): Uint8Array {
const [n, d] = parsePrivateKey(privateKey); const [n, d] = parsePrivateKey(privateKey);
// PKCS#1 padding // PKCS#1 padding
@ -254,7 +265,7 @@ export function sign(privateKey: Uint8Array, data: Uint8Array): ArrayBuffer {
padded[index] = 1; padded[index] = 1;
index += 1; index += 1;
const fillLength = padded.length - Sha1DigestInfo.length - data.length - 1; const fillLength = padded.length - SHA1_DIGEST_INFO.length - data.length - 1;
while (index < fillLength) { while (index < fillLength) {
padded[index] = 0xff; padded[index] = 0xff;
index += 1; index += 1;
@ -263,18 +274,23 @@ export function sign(privateKey: Uint8Array, data: Uint8Array): ArrayBuffer {
padded[index] = 0; padded[index] = 0;
index += 1; index += 1;
padded.set(Sha1DigestInfo, index); padded.set(SHA1_DIGEST_INFO, index);
index += Sha1DigestInfo.length; index += SHA1_DIGEST_INFO.length;
padded.set(data, index); padded.set(data, index);
// Encryption // Encryption
// signature = padded ** d % n // signature = padded ** d % n
let signature = powMod(getBig(padded), d, n); const view = new DataView(padded.buffer);
const signature = powMod(
getBigUint(view, 0, view.byteLength),
d,
n
);
// Put into an ArrayBuffer // `padded` is not used anymore,
const result = new ArrayBuffer(256); // re-use the buffer to store the result
setBig(result, signature); setBigUint(view, 0, signature, false);
return result; return padded;
} }

View file

@ -30,15 +30,24 @@ export const AdbPacket =
export type AdbPacket = typeof AdbPacket['TDeserializeResult']; export type AdbPacket = typeof AdbPacket['TDeserializeResult'];
// All the useful fields /**
export type AdbPacketCore = Omit<typeof AdbPacket['TInit'], 'checksum' | 'magic'>; * `AdbPacketData` contains all the useful fields of `AdbPacket`.
*
* `AdbBackend#connect` will return a `ReadableStream<AdbPacketData>`,
* so each backend can encode `AdbPacket` in different ways.
*
* `AdbBackend#connect` will return a `WritableStream<AdbPacketInit>`,
* however, `AdbPacketDispatcher` will transform `AdbPacketData` to `AdbPacketInit` for you,
* so `AdbSocket#writable#write` only needs `AdbPacketData`.
*/
export type AdbPacketData = Omit<typeof AdbPacket['TInit'], 'checksum' | 'magic'>;
// All fields except `magic`, which can be calculated in `AdbPacketSerializeStream` // All fields except `magic`, which can be calculated in `AdbPacketSerializeStream`
export type AdbPacketInit = Omit<typeof AdbPacket['TInit'], 'magic'>; export type AdbPacketInit = Omit<typeof AdbPacket['TInit'], 'magic'>;
export function calculateChecksum(payload: Uint8Array): number; export function calculateChecksum(payload: Uint8Array): number;
export function calculateChecksum(init: AdbPacketCore): AdbPacketInit; export function calculateChecksum(init: AdbPacketData): AdbPacketInit;
export function calculateChecksum(payload: Uint8Array | AdbPacketCore): number | AdbPacketInit { export function calculateChecksum(payload: Uint8Array | AdbPacketData): number | AdbPacketInit {
if (payload instanceof Uint8Array) { if (payload instanceof Uint8Array) {
return payload.reduce((result, item) => result + item, 0); return payload.reduce((result, item) => result + item, 0);
} else { } else {
@ -51,7 +60,7 @@ export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, Uin
public constructor() { public constructor() {
super({ super({
transform: async (init, controller) => { transform: async (init, controller) => {
// This syntax is ugly, but I don't want to create an new object. // This syntax is ugly, but I don't want to create a new object.
(init as unknown as AdbPacketHeaderInit).magic = init.command ^ 0xFFFFFFFF; (init as unknown as AdbPacketHeaderInit).magic = init.command ^ 0xFFFFFFFF;
(init as unknown as AdbPacketHeaderInit).payloadLength = init.payload.byteLength; (init as unknown as AdbPacketHeaderInit).payloadLength = init.payload.byteLength;

View file

@ -1,14 +1,14 @@
import { AsyncOperationManager, PromiseResolver } from '@yume-chan/async'; import { AsyncOperationManager, PromiseResolver } from '@yume-chan/async';
import { AutoDisposable, EventEmitter } from '@yume-chan/event'; import { AutoDisposable, EventEmitter } from '@yume-chan/event';
import { AdbCommand, calculateChecksum, type AdbPacketCore, type AdbPacketInit } from '../packet.js'; import { AdbCommand, calculateChecksum, type AdbPacketData, type AdbPacketInit } from '../packet.js';
import { AbortController, WritableStream, WritableStreamDefaultWriter, type ReadableWritablePair } from '../stream/index.js'; import { AbortController, WritableStream, WritableStreamDefaultWriter, type ReadableWritablePair } from '../stream/index.js';
import { decodeUtf8, encodeUtf8 } from '../utils/index.js'; import { decodeUtf8, encodeUtf8 } from '../utils/index.js';
import { AdbSocket } from './socket.js'; import { AdbSocket, AdbSocketController } from './socket.js';
export interface AdbIncomingSocketEventArgs { export interface AdbIncomingSocketEventArgs {
handled: boolean; handled: boolean;
packet: AdbPacketCore; packet: AdbPacketData;
serviceString: string; serviceString: string;
@ -17,17 +17,27 @@ export interface AdbIncomingSocketEventArgs {
const EmptyUint8Array = new Uint8Array(0); const EmptyUint8Array = new Uint8Array(0);
export interface AdbPacketDispatcherOptions {
calculateChecksum: boolean;
/**
* Before Android 9.0, ADB uses `char*` to parse service string,
* thus requires a null character to terminate.
*
* Usually it should have the same value as `calculateChecksum`.
*/
appendNullToServiceString: boolean;
maxPayloadSize: number;
}
export class AdbPacketDispatcher extends AutoDisposable { export class AdbPacketDispatcher extends AutoDisposable {
// ADB socket id starts from 1 // ADB socket id starts from 1
// (0 means open failed) // (0 means open failed)
private readonly initializers = new AsyncOperationManager(1); private readonly initializers = new AsyncOperationManager(1);
private readonly sockets = new Map<number, AdbSocket>(); private readonly sockets = new Map<number, AdbSocketController>();
private _writer!: WritableStreamDefaultWriter<AdbPacketInit>; private _writer!: WritableStreamDefaultWriter<AdbPacketInit>;
public maxPayloadSize = 0; public readonly options: AdbPacketDispatcherOptions;
public calculateChecksum = true;
public appendNullToServiceString = true;
private _disconnected = new PromiseResolver<void>(); private _disconnected = new PromiseResolver<void>();
public get disconnected() { return this._disconnected.promise; } public get disconnected() { return this._disconnected.promise; }
@ -41,10 +51,13 @@ export class AdbPacketDispatcher extends AutoDisposable {
private _abortController = new AbortController(); private _abortController = new AbortController();
public constructor( public constructor(
connection: ReadableWritablePair<AdbPacketCore, AdbPacketInit>, connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
options: AdbPacketDispatcherOptions
) { ) {
super(); super();
this.options = options;
connection.readable connection.readable
.pipeTo(new WritableStream({ .pipeTo(new WritableStream({
write: async (packet) => { write: async (packet) => {
@ -91,7 +104,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
this._writer = connection.writable.getWriter(); this._writer = connection.writable.getWriter();
} }
private handleOk(packet: AdbPacketCore) { private handleOk(packet: AdbPacketData) {
if (this.initializers.resolve(packet.arg1, packet.arg0)) { if (this.initializers.resolve(packet.arg1, packet.arg0)) {
// Device successfully created the socket // Device successfully created the socket
return; return;
@ -109,7 +122,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0); this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
} }
private async handleClose(packet: AdbPacketCore) { private async handleClose(packet: AdbPacketData) {
// From https://android.googlesource.com/platform/packages/modules/adb/+/65d18e2c1cc48b585811954892311b28a4c3d188/adb.cpp#459 // From https://android.googlesource.com/platform/packages/modules/adb/+/65d18e2c1cc48b585811954892311b28a4c3d188/adb.cpp#459
/* According to protocol.txt, p->msg.arg0 might be 0 to indicate /* According to protocol.txt, p->msg.arg0 might be 0 to indicate
* a failed OPEN only. However, due to a bug in previous ADB * a failed OPEN only. However, due to a bug in previous ADB
@ -139,7 +152,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
// Just ignore it // Just ignore it
} }
private async handleOpen(packet: AdbPacketCore) { private async handleOpen(packet: AdbPacketData) {
// AsyncOperationManager doesn't support get and skip an ID // AsyncOperationManager doesn't support get and skip an ID
// Use `add` + `resolve` to simulate this behavior // Use `add` + `resolve` to simulate this behavior
const [localId] = this.initializers.add<number>(); const [localId] = this.initializers.add<number>();
@ -148,7 +161,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
const remoteId = packet.arg0; const remoteId = packet.arg0;
const serviceString = decodeUtf8(packet.payload); const serviceString = decodeUtf8(packet.payload);
const socket = new AdbSocket({ const controller = new AdbSocketController({
dispatcher: this, dispatcher: this,
localId, localId,
remoteId, remoteId,
@ -160,12 +173,12 @@ export class AdbPacketDispatcher extends AutoDisposable {
handled: false, handled: false,
packet, packet,
serviceString, serviceString,
socket, socket: controller.socket,
}; };
this.incomingSocketEvent.fire(args); this.incomingSocketEvent.fire(args);
if (args.handled) { if (args.handled) {
this.sockets.set(localId, socket); this.sockets.set(localId, controller);
await this.sendPacket(AdbCommand.OK, localId, remoteId); await this.sendPacket(AdbCommand.OK, localId, remoteId);
} else { } else {
await this.sendPacket(AdbCommand.Close, 0, remoteId); await this.sendPacket(AdbCommand.Close, 0, remoteId);
@ -173,25 +186,30 @@ export class AdbPacketDispatcher extends AutoDisposable {
} }
public async createSocket(serviceString: string): Promise<AdbSocket> { public async createSocket(serviceString: string): Promise<AdbSocket> {
if (this.appendNullToServiceString) { if (this.options.appendNullToServiceString) {
serviceString += '\0'; serviceString += '\0';
} }
const [localId, initializer] = this.initializers.add<number>(); const [localId, initializer] = this.initializers.add<number>();
await this.sendPacket(AdbCommand.Open, localId, 0, serviceString); await this.sendPacket(
AdbCommand.Open,
localId,
0,
serviceString
);
// Fulfilled by `handleOk` // Fulfilled by `handleOk`
const remoteId = await initializer; const remoteId = await initializer;
const socket = new AdbSocket({ const controller = new AdbSocketController({
dispatcher: this, dispatcher: this,
localId, localId,
remoteId, remoteId,
localCreated: true, localCreated: true,
serviceString, serviceString,
}); });
this.sockets.set(localId, socket); this.sockets.set(localId, controller);
return socket; return controller.socket;
} }
public sendPacket(packet: AdbPacketInit): Promise<void>; public sendPacket(packet: AdbPacketInit): Promise<void>;
@ -207,7 +225,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
arg1?: number, arg1?: number,
payload: string | Uint8Array = EmptyUint8Array, payload: string | Uint8Array = EmptyUint8Array,
): Promise<void> { ): Promise<void> {
let init: AdbPacketCore; let init: AdbPacketData;
if (arg0 === undefined) { if (arg0 === undefined) {
init = packetOrCommand as AdbPacketInit; init = packetOrCommand as AdbPacketInit;
} else { } else {
@ -224,11 +242,11 @@ export class AdbPacketDispatcher extends AutoDisposable {
} }
if (init.payload && if (init.payload &&
init.payload.byteLength > this.maxPayloadSize) { init.payload.byteLength > this.options.maxPayloadSize) {
throw new Error('payload too large'); throw new Error('payload too large');
} }
if (this.calculateChecksum) { if (this.options.calculateChecksum) {
calculateChecksum(init); calculateChecksum(init);
} else { } else {
(init as AdbPacketInit).checksum = 0; (init as AdbPacketInit).checksum = 0;

View file

@ -1,6 +1,6 @@
import { PromiseResolver } from "@yume-chan/async"; import { PromiseResolver } from "@yume-chan/async";
import { AdbCommand } from '../packet.js'; import { AdbCommand } from '../packet.js';
import { ChunkStream, DuplexStreamFactory, pipeFrom, ReadableStream, WritableStream, type PushReadableStreamController } from '../stream/index.js'; import { ChunkStream, DuplexStreamFactory, pipeFrom, type PushReadableStreamController, type ReadableStream, type ReadableWritablePair, type WritableStream } from '../stream/index.js';
import type { AdbPacketDispatcher } from './dispatcher.js'; import type { AdbPacketDispatcher } from './dispatcher.js';
export interface AdbSocketInfo { export interface AdbSocketInfo {
@ -8,25 +8,16 @@ export interface AdbSocketInfo {
remoteId: number; remoteId: number;
localCreated: boolean; localCreated: boolean;
serviceString: string; serviceString: string;
} }
export interface AdbSocketConstructionOptions { export interface AdbSocketConstructionOptions extends AdbSocketInfo {
dispatcher: AdbPacketDispatcher; dispatcher: AdbPacketDispatcher;
localId: number;
remoteId: number;
localCreated: boolean;
serviceString: string;
highWaterMark?: number | undefined; highWaterMark?: number | undefined;
} }
export class AdbSocket implements AdbSocketInfo { export class AdbSocketController implements AdbSocketInfo, ReadableWritablePair<Uint8Array, Uint8Array> {
private readonly dispatcher!: AdbPacketDispatcher; private readonly dispatcher!: AdbPacketDispatcher;
public readonly localId!: number; public readonly localId!: number;
@ -46,6 +37,9 @@ export class AdbSocket implements AdbSocketInfo {
private _closed = false; private _closed = false;
public get closed() { return this._closed; } public get closed() { return this._closed; }
private _socket: AdbSocket;
public get socket() { return this._socket; }
public constructor(options: AdbSocketConstructionOptions) { public constructor(options: AdbSocketConstructionOptions) {
Object.assign(this, options); Object.assign(this, options);
@ -80,20 +74,16 @@ export class AdbSocket implements AdbSocketInfo {
await this._writePromise.promise; await this._writePromise.promise;
}, },
}), }),
new ChunkStream(this.dispatcher.maxPayloadSize) new ChunkStream(this.dispatcher.options.maxPayloadSize)
); );
this._socket = new AdbSocket(this);
} }
/**
* @internal
*/
public async enqueue(packet: Uint8Array) { public async enqueue(packet: Uint8Array) {
await this._readableController.enqueue(packet); await this._readableController.enqueue(packet);
} }
/**
* @internal
*/
public ack() { public ack() {
this._writePromise?.resolve(); this._writePromise?.resolve();
} }
@ -115,9 +105,6 @@ export class AdbSocket implements AdbSocketInfo {
} }
} }
/**
* @internal
*/
public dispose() { public dispose() {
this._closed = true; this._closed = true;
@ -127,3 +114,23 @@ export class AdbSocket implements AdbSocketInfo {
this.close(); this.close();
} }
} }
export class AdbSocket implements AdbSocketInfo, ReadableWritablePair<Uint8Array, Uint8Array>{
private _controller: AdbSocketController;
public get localId(): number { return this._controller.localId; }
public get remoteId(): number { return this._controller.remoteId; }
public get localCreated(): boolean { return this._controller.localCreated; }
public get serviceString(): string { return this._controller.serviceString; }
public get readable(): ReadableStream<Uint8Array> { return this._controller.readable; }
public get writable(): WritableStream<Uint8Array> { return this._controller.writable; }
public constructor(controller: AdbSocketController) {
this._controller = controller;
}
public close() {
return this._controller.close();
}
}

View file

@ -1 +1,5 @@
// cspell: ignore vercel
// Always use polyfilled version because
// Vercel doesn't support Node.js 16 (`streams/web` module) yet
export * from './detect.polyfill.js'; export * from './detect.polyfill.js';

View file

@ -11,6 +11,12 @@ export interface DuplexStreamFactoryOptions {
close?: (() => void | Promise<void>) | undefined; close?: (() => void | Promise<void>) | undefined;
} }
/**
* A factory for creating a duplex stream.
*
* It can create multiple `ReadableStream`s and `WritableStream`s,
* when any of them is closed, all other streams will be closed as well.
*/
export class DuplexStreamFactory<R, W> { export class DuplexStreamFactory<R, W> {
private readableControllers: ReadableStreamDefaultController<R>[] = []; private readableControllers: ReadableStreamDefaultController<R>[] = [];
private pushReadableControllers: PushReadableStreamController<R>[] = []; private pushReadableControllers: PushReadableStreamController<R>[] = [];
@ -42,6 +48,8 @@ export class DuplexStreamFactory<R, W> {
await controller.enqueue(chunk); await controller.enqueue(chunk);
}, },
close: async () => { close: async () => {
// The source signals stream ended,
// usually means the other end closed the connection first.
controller.close(); controller.close();
this._closeRequestedByReadable = true; this._closeRequestedByReadable = true;
await this.close(); await this.close();
@ -58,15 +66,7 @@ export class DuplexStreamFactory<R, W> {
public createWrapReadable(wrapper: ReadableStream<R> | WrapReadableStreamStart<R> | ReadableStreamWrapper<R>): WrapReadableStream<R> { public createWrapReadable(wrapper: ReadableStream<R> | WrapReadableStreamStart<R> | ReadableStreamWrapper<R>): WrapReadableStream<R> {
return new WrapReadableStream<R>({ return new WrapReadableStream<R>({
async start() { async start() {
if ('start' in wrapper) { return getWrappedReadableStream(wrapper);
return await wrapper.start();
} else if (typeof wrapper === 'function') {
return await wrapper();
} else {
// Can't use `wrapper instanceof ReadableStream`
// Because we want to be compatible with any ReadableStream-like objects
return wrapper;
}
}, },
close: async () => { close: async () => {
if ('close' in wrapper) { if ('close' in wrapper) {
@ -169,7 +169,7 @@ export class GatherStringStream extends WritableStream<string>{
} }
} }
// TODO: Find other ways to implement `StructTransformStream` // TODO: StructTransformStream: Looking for better implementation
export class StructDeserializeStream<T extends Struct<any, any, any, any>> export class StructDeserializeStream<T extends Struct<any, any, any, any>>
implements ReadableWritablePair<Uint8Array, StructValueType<T>>{ implements ReadableWritablePair<Uint8Array, StructValueType<T>>{
private _readable: ReadableStream<StructValueType<T>>; private _readable: ReadableStream<StructValueType<T>>;
@ -234,6 +234,20 @@ export interface WritableStreamWrapper<T> {
close?(): Promise<void>; close?(): Promise<void>;
} }
async function getWrappedWritableStream<T>(
wrapper: WritableStream<T> | WrapWritableStreamStart<T> | WritableStreamWrapper<T>
) {
if ('start' in wrapper) {
return await wrapper.start();
} else if (typeof wrapper === 'function') {
return await wrapper();
} else {
// Can't use `wrapper instanceof WritableStream`
// Because we want to be compatible with any WritableStream-like objects
return wrapper;
}
}
export class WrapWritableStream<T> extends WritableStream<T> { export class WrapWritableStream<T> extends WritableStream<T> {
public writable!: WritableStream<T>; public writable!: WritableStream<T>;
@ -242,21 +256,13 @@ export class WrapWritableStream<T> extends WritableStream<T> {
public constructor(wrapper: WritableStream<T> | WrapWritableStreamStart<T> | WritableStreamWrapper<T>) { public constructor(wrapper: WritableStream<T> | WrapWritableStreamStart<T> | WritableStreamWrapper<T>) {
super({ super({
start: async () => { start: async () => {
// `start` is invoked before `WritableStream`'s constructor finish, // `start` is invoked before `ReadableStream`'s constructor finish,
// so using `this` synchronously causes // so using `this` synchronously causes
// "Must call super constructor in derived class before accessing 'this' or returning from derived constructor". // "Must call super constructor in derived class before accessing 'this' or returning from derived constructor".
// Queue a microtask to avoid this. // Queue a microtask to avoid this.
await Promise.resolve(); await Promise.resolve();
if ('start' in wrapper) { this.writable = await getWrappedWritableStream(wrapper);
this.writable = await wrapper.start();
} else if (typeof wrapper === 'function') {
this.writable = await wrapper();
} else {
// Can't use `wrapper instanceof WritableStream`
// Because we want to be compatible with any WritableStream-like objects
this.writable = wrapper;
}
this.writer = this.writable.getWriter(); this.writer = this.writable.getWriter();
}, },
write: async (chunk) => { write: async (chunk) => {
@ -291,6 +297,20 @@ export interface ReadableStreamWrapper<T> {
close?(): Promise<void>; close?(): Promise<void>;
} }
function getWrappedReadableStream<T>(
wrapper: ReadableStream<T> | WrapReadableStreamStart<T> | ReadableStreamWrapper<T>
) {
if ('start' in wrapper) {
return wrapper.start();
} else if (typeof wrapper === 'function') {
return wrapper();
} else {
// Can't use `wrapper instanceof ReadableStream`
// Because we want to be compatible with any ReadableStream-like objects
return wrapper;
}
}
export class WrapReadableStream<T> extends ReadableStream<T>{ export class WrapReadableStream<T> extends ReadableStream<T>{
public readable!: ReadableStream<T>; public readable!: ReadableStream<T>;
@ -305,15 +325,7 @@ export class WrapReadableStream<T> extends ReadableStream<T>{
// Queue a microtask to avoid this. // Queue a microtask to avoid this.
await Promise.resolve(); await Promise.resolve();
if ('start' in wrapper) { this.readable = await getWrappedReadableStream(wrapper);
this.readable = await wrapper.start();
} else if (typeof wrapper === 'function') {
this.readable = await wrapper();
} else {
// Can't use `wrapper instanceof ReadableStream`
// Because we want to be compatible with any ReadableStream-like objects
this.readable = wrapper;
}
this.reader = this.readable.getReader(); this.reader = this.readable.getReader();
}, },
cancel: async (reason) => { cancel: async (reason) => {

View file

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

View file

@ -0,0 +1,22 @@
// cspell: ignore apks
// cspell: ignore obbs
import { AdbCommandBase } from "@yume-chan/adb";
export interface AdbBackupOptions {
apps: string[] | 'all' | 'all-including-system';
apks: boolean;
obbs: boolean;
shared: boolean;
widgets: boolean;
compress: boolean;
user: number;
}
export class AdbBackup extends AdbCommandBase {
async backup(options: AdbBackupOptions): Promise<void> {
}
async restore(options: AdbBackupOptions): Promise<void> {
}
}

View file

@ -1,7 +1,7 @@
// cspell: ignore bugreport // cspell: ignore bugreport
// cspell: ignore bugreportz // cspell: ignore bugreportz
import { AdbCommandBase, AdbShellSubprocessProtocol, DecodeUtf8Stream, PushReadableStream, ReadableStream, SplitLineStream, WrapReadableStream, WritableStream } from "@yume-chan/adb"; import { AdbCommandBase, AdbSubprocessShellProtocol, DecodeUtf8Stream, PushReadableStream, ReadableStream, SplitLineStream, WrapReadableStream, WritableStream } from "@yume-chan/adb";
export interface BugReportZVersion { export interface BugReportZVersion {
major: number; major: number;
@ -29,7 +29,7 @@ export class BugReportZ extends AdbCommandBase {
*/ */
public async version(): Promise<BugReportZVersion | undefined> { public async version(): Promise<BugReportZVersion | undefined> {
// bugreportz requires shell protocol // bugreportz requires shell protocol
if (!AdbShellSubprocessProtocol.isSupported(this.adb)) { if (!AdbSubprocessShellProtocol.isSupported(this.adb)) {
return undefined; return undefined;
} }
@ -61,9 +61,10 @@ export class BugReportZ extends AdbCommandBase {
/** /**
* Create a zipped bugreport file. * Create a zipped bugreport file.
* *
* Compare to `stream`, this method will write the output on device. You can pull it using sync protocol. * Compare to `stream`, this method will write the output to a file on device.
* You can pull it later using sync protocol.
* *
* @param onProgress Progress callback. Only specify this if `supportsProgress()` returns `true`. * @param onProgress Progress callback. Only specify this if `supportsProgress` is `true`.
* @returns The path of the bugreport file. * @returns The path of the bugreport file.
*/ */
public async generate(onProgress?: (progress: string, total: string) => void): Promise<string> { public async generate(onProgress?: (progress: string, total: string) => void): Promise<string> {

View file

@ -39,7 +39,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^27.5.1", "jest": "^27.5.1",
"typescript": "next", "typescript": "4.7.0-beta",
"@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": "^27.5.1", "jest": "^27.5.1",
"typescript": "next", "typescript": "4.7.0-beta",
"@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": "^27.5.1", "jest": "^27.5.1",
"tinyh264": "^0.0.7", "tinyh264": "^0.0.7",
"typescript": "next", "typescript": "4.7.0-beta",
"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 { AdbBufferedStream, AdbNoneSubprocessProtocol, DecodeUtf8Stream, InspectStream, TransformStream, WritableStream, type Adb, type AdbSocket, type AdbSubprocessProtocol, type ReadableStream, type WritableStreamDefaultWriter } from '@yume-chan/adb'; import { AdbBufferedStream, AdbSubprocessNoneProtocol, DecodeUtf8Stream, InspectStream, TransformStream, WritableStream, type Adb, type AdbSocket, type AdbSubprocessProtocol, type ReadableStream, type 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.js'; import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message.js';
@ -81,7 +81,7 @@ export class ScrcpyClient {
{ {
// Scrcpy server doesn't split stdout and stderr, // Scrcpy server doesn't split stdout and stderr,
// so disable Shell Protocol to simplify processing // so disable Shell Protocol to simplify processing
protocols: [AdbNoneSubprocessProtocol], protocols: [AdbSubprocessNoneProtocol],
} }
); );

View file

@ -11,6 +11,9 @@
] ]
}, },
"references": [ "references": [
{
"path": "../adb/tsconfig.build.json"
},
{ {
"path": "../event/tsconfig.json" "path": "../event/tsconfig.json"
}, },

View file

@ -39,7 +39,7 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^27.5.1", "jest": "^27.5.1",
"typescript": "next", "typescript": "4.7.0-beta",
"@yume-chan/ts-package-builder": "^1.0.0", "@yume-chan/ts-package-builder": "^1.0.0",
"@types/jest": "^27.4.1", "@types/jest": "^27.4.1",
"@types/bluebird": "^3.5.36" "@types/bluebird": "^3.5.36"

View file

@ -118,7 +118,7 @@ export class VariableLengthBufferLikeFieldLengthValue
extends StructFieldValue { extends StructFieldValue {
protected originalField: StructFieldValue; protected originalField: StructFieldValue;
protected arrayBufferField: VariableLengthBufferLikeFieldValueLike; protected bufferField: VariableLengthBufferLikeFieldValueLike;
public constructor( public constructor(
originalField: StructFieldValue, originalField: StructFieldValue,
@ -126,7 +126,7 @@ export class VariableLengthBufferLikeFieldLengthValue
) { ) {
super(originalField.definition, originalField.options, originalField.struct, 0); super(originalField.definition, originalField.options, originalField.struct, 0);
this.originalField = originalField; this.originalField = originalField;
this.arrayBufferField = arrayBufferField; this.bufferField = arrayBufferField;
} }
public override getSize() { public override getSize() {
@ -134,11 +134,11 @@ export class VariableLengthBufferLikeFieldLengthValue
} }
public override get() { public override get() {
let value: string | number = this.arrayBufferField.getSize(); let value: string | number = this.bufferField.getSize();
const originalValue = this.originalField.get(); const originalValue = this.originalField.get();
if (typeof originalValue === 'string') { if (typeof originalValue === 'string') {
value = value.toString(this.arrayBufferField.definition.options.lengthFieldRadix ?? 10); value = value.toString(this.bufferField.definition.options.lengthFieldRadix ?? 10);
} }
return value; return value;