mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-04 18:29:23 +02:00
parent
4cec3ec55c
commit
bb5e292a80
69 changed files with 1640 additions and 618 deletions
|
@ -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}</>;
|
|
||||||
}
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
@ -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,
|
|
@ -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';
|
16
apps/demo/src/components/log-view.tsx
Normal file
16
apps/demo/src/components/log-view.tsx
Normal 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}</>;
|
||||||
|
}
|
|
@ -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);
|
|
@ -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 >
|
|
@ -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>
|
|
@ -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} />
|
|
@ -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>
|
283
apps/demo/src/pages/packet-log.tsx
Normal file
283
apps/demo/src/pages/packet-log.tsx
Normal 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);
|
|
@ -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} />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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() {
|
|
@ -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' }} />
|
|
@ -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';
|
|
@ -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;
|
||||||
|
}
|
854
common/config/rush/pnpm-lock.yaml
generated
854
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
libraries/adb/src/commands/subprocess/protocols/index.ts
Normal file
3
libraries/adb/src/commands/subprocess/protocols/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './none.js';
|
||||||
|
export * from './shell.js';
|
||||||
|
export * from './types.js';
|
|
@ -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>;
|
|
@ -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;
|
|
@ -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` */
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
22
libraries/android-bin/src/bu.ts
Normal file
22
libraries/android-bin/src/bu.ts
Normal 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> {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"references": [
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../adb/tsconfig.build.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../event/tsconfig.json"
|
"path": "../event/tsconfig.json"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue