mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 10:49:24 +02:00
parent
f46ff36830
commit
c3a61f9b71
22 changed files with 946 additions and 231 deletions
|
@ -162,6 +162,9 @@ function _Connect(): JSX.Element | null {
|
||||||
device = await Adb.authenticate({ readable, writable }, CredentialStore, undefined);
|
device = await Adb.authenticate({ readable, writable }, CredentialStore, undefined);
|
||||||
device.disconnected.then(() => {
|
device.disconnected.then(() => {
|
||||||
globalState.setDevice(undefined, undefined);
|
globalState.setDevice(undefined, undefined);
|
||||||
|
}, (e) => {
|
||||||
|
globalState.showErrorDialog(e);
|
||||||
|
globalState.setDevice(undefined, undefined);
|
||||||
});
|
});
|
||||||
globalState.setDevice(selectedBackend, device);
|
globalState.setDevice(selectedBackend, device);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -170,7 +173,7 @@ function _Connect(): JSX.Element | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
globalState.showErrorDialog(e.message);
|
globalState.showErrorDialog(e);
|
||||||
} finally {
|
} finally {
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
}
|
}
|
||||||
|
@ -180,7 +183,7 @@ function _Connect(): JSX.Element | null {
|
||||||
await globalState.device!.dispose();
|
await globalState.device!.dispose();
|
||||||
globalState.setDevice(undefined, undefined);
|
globalState.setDevice(undefined, undefined);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
globalState.showErrorDialog(e.message);
|
globalState.showErrorDialog(e);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -140,8 +140,8 @@ class FileManagerState {
|
||||||
const itemPath = path.resolve(this.path, item.name);
|
const itemPath = path.resolve(this.path, item.name);
|
||||||
await sync.read(itemPath)
|
await sync.read(itemPath)
|
||||||
.pipeTo(saveFile(item.name, Number(item.size)));
|
.pipeTo(saveFile(item.name, Number(item.size)));
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
globalState.showErrorDialog(e);
|
||||||
} finally {
|
} finally {
|
||||||
sync.dispose();
|
sync.dispose();
|
||||||
}
|
}
|
||||||
|
@ -169,8 +169,8 @@ class FileManagerState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
globalState.showErrorDialog(e);
|
||||||
} finally {
|
} finally {
|
||||||
this.loadFiles();
|
this.loadFiles();
|
||||||
}
|
}
|
||||||
|
@ -481,8 +481,8 @@ class FileManagerState {
|
||||||
} finally {
|
} finally {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
globalState.showErrorDialog(e);
|
||||||
} finally {
|
} finally {
|
||||||
sync.dispose();
|
sync.dispose();
|
||||||
this.loadFiles();
|
this.loadFiles();
|
||||||
|
|
|
@ -45,8 +45,8 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
|
||||||
try {
|
try {
|
||||||
const framebuffer = await globalState.device.framebuffer();
|
const framebuffer = await globalState.device.framebuffer();
|
||||||
state.setImage(framebuffer);
|
state.setImage(framebuffer);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
globalState.showErrorDialog(e);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
301
apps/demo/src/pages/logcat.tsx
Normal file
301
apps/demo/src/pages/logcat.tsx
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
// cspell: ignore logcat
|
||||||
|
|
||||||
|
import { ICommandBarItemProps, Stack, StackItem } from "@fluentui/react";
|
||||||
|
import { makeStyles, mergeClasses, shorthands } from "@griffel/react";
|
||||||
|
import { AbortController, decodeUtf8, ReadableStream, WritableStream } from '@yume-chan/adb';
|
||||||
|
import { Logcat, LogMessage, LogPriority } from '@yume-chan/android-bin';
|
||||||
|
import { autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { CommandBar, Grid, GridColumn, GridHeaderProps, GridRowProps } from "../components";
|
||||||
|
import { globalState } from "../state";
|
||||||
|
import { Icons, RouteStackProps, useCallbackRef } from "../utils";
|
||||||
|
|
||||||
|
const LINE_HEIGHT = 32;
|
||||||
|
|
||||||
|
const useClasses = makeStyles({
|
||||||
|
grid: {
|
||||||
|
height: '100%',
|
||||||
|
marginLeft: '-16px',
|
||||||
|
marginRight: '-16px',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: `${LINE_HEIGHT}px`,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f3f2f1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
backgroundColor: '#edebe9',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
lineHeight: LINE_HEIGHT + 'px',
|
||||||
|
cursor: 'default',
|
||||||
|
...shorthands.overflow('hidden'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface Column extends GridColumn {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogRow extends LogMessage {
|
||||||
|
timeString?: string;
|
||||||
|
payloadString?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = makeAutoObservable({
|
||||||
|
logcat: undefined as Logcat | undefined,
|
||||||
|
running: false,
|
||||||
|
list: [] as LogRow[],
|
||||||
|
stream: undefined as ReadableStream<LogMessage> | undefined,
|
||||||
|
stopSignal: undefined as AbortController | undefined,
|
||||||
|
selectedCount: 0,
|
||||||
|
start() {
|
||||||
|
if (this.running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.running = true;
|
||||||
|
this.stream = this.logcat!.binary();
|
||||||
|
this.stopSignal = new AbortController();
|
||||||
|
this.stream
|
||||||
|
.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write: (chunk) => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.list.push(chunk);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ signal: this.stopSignal.signal }
|
||||||
|
)
|
||||||
|
.catch(() => { });
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
this.running = false;
|
||||||
|
this.stopSignal!.abort();
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
this.list = [];
|
||||||
|
this.selectedCount = 0;
|
||||||
|
},
|
||||||
|
get empty() {
|
||||||
|
return this.list.length === 0;
|
||||||
|
},
|
||||||
|
get commandBar(): ICommandBarItemProps[] {
|
||||||
|
return [
|
||||||
|
this.running ? {
|
||||||
|
key: "stop",
|
||||||
|
text: "Stop",
|
||||||
|
iconProps: { iconName: Icons.Stop },
|
||||||
|
onClick: () => this.stop(),
|
||||||
|
} : {
|
||||||
|
key: "start",
|
||||||
|
text: "Start",
|
||||||
|
disabled: this.logcat === undefined,
|
||||||
|
iconProps: { iconName: Icons.Play },
|
||||||
|
onClick: () => this.start(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clear',
|
||||||
|
text: 'Clear',
|
||||||
|
disabled: this.empty,
|
||||||
|
iconProps: { iconName: Icons.Delete },
|
||||||
|
onClick: () => this.clear(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'copyAll',
|
||||||
|
text: 'Copy Rows',
|
||||||
|
disabled: this.selectedCount === 0,
|
||||||
|
iconProps: { iconName: Icons.Copy },
|
||||||
|
onClick: () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'copyText',
|
||||||
|
text: 'Copy Messages',
|
||||||
|
disabled: this.selectedCount === 0,
|
||||||
|
iconProps: { iconName: Icons.Copy },
|
||||||
|
onClick: () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
get columns(): Column[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
width: 200,
|
||||||
|
title: 'Time',
|
||||||
|
CellComponent: ({ rowIndex, columnIndex, className, ...rest }) => {
|
||||||
|
const item = this.list[rowIndex];
|
||||||
|
if (!item.timeString) {
|
||||||
|
item.timeString = new Date(item.second * 1000).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mergeClasses(classes.code, className)} {...rest}>
|
||||||
|
{item.timeString}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 80,
|
||||||
|
title: 'PID',
|
||||||
|
CellComponent: ({ rowIndex, columnIndex, className, ...rest }) => {
|
||||||
|
const item = this.list[rowIndex];
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mergeClasses(classes.code, className)} {...rest}>
|
||||||
|
{item.pid}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 80,
|
||||||
|
title: 'TID',
|
||||||
|
CellComponent: ({ rowIndex, columnIndex, className, ...rest }) => {
|
||||||
|
const item = this.list[rowIndex];
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mergeClasses(classes.code, className)} {...rest}>
|
||||||
|
{item.tid}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 100,
|
||||||
|
title: 'Priority',
|
||||||
|
CellComponent: ({ rowIndex, columnIndex, className, ...rest }) => {
|
||||||
|
const item = this.list[rowIndex];
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mergeClasses(classes.code, className)} {...rest}>
|
||||||
|
{LogPriority[item.priority]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 300,
|
||||||
|
flexGrow: 1,
|
||||||
|
title: 'Payload',
|
||||||
|
CellComponent: ({ rowIndex, columnIndex, className, ...rest }) => {
|
||||||
|
const item = this.list[rowIndex];
|
||||||
|
if (!item.payloadString) {
|
||||||
|
item.payloadString = decodeUtf8(item.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mergeClasses(classes.code, className)} {...rest}>
|
||||||
|
{item.payloadString}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
list: observable.shallow,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(state);
|
||||||
|
|
||||||
|
autorun(() => {
|
||||||
|
if (globalState.device) {
|
||||||
|
state.logcat = new Logcat(globalState.device);
|
||||||
|
} else {
|
||||||
|
state.logcat = undefined;
|
||||||
|
if (state.running) {
|
||||||
|
state.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const Header = observer(function Header({
|
||||||
|
className,
|
||||||
|
columnIndex,
|
||||||
|
...rest
|
||||||
|
}: GridHeaderProps) {
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mergeClasses(className, classes.header)} {...rest}>
|
||||||
|
{state.columns[columnIndex].title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Row = observer(function Row({
|
||||||
|
className,
|
||||||
|
rowIndex,
|
||||||
|
...rest
|
||||||
|
}: GridRowProps) {
|
||||||
|
const item = state.list[rowIndex];
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
const handleClick = useCallbackRef(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={mergeClasses(
|
||||||
|
className,
|
||||||
|
classes.row,
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const LogcatPage: NextPage = () => {
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack {...RouteStackProps}>
|
||||||
|
<Head>
|
||||||
|
<title>Logcat - Android Web Toolbox</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<CommandBar items={state.commandBar} />
|
||||||
|
|
||||||
|
<StackItem grow>
|
||||||
|
<Grid
|
||||||
|
className={classes.grid}
|
||||||
|
rowCount={state.list.length}
|
||||||
|
rowHeight={LINE_HEIGHT}
|
||||||
|
columns={state.columns}
|
||||||
|
HeaderComponent={Header}
|
||||||
|
RowComponent={Row}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(LogcatPage);
|
|
@ -5,7 +5,6 @@ import { autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useMemo } from "react";
|
|
||||||
import { CommandBar, Grid, GridCellProps, GridColumn, GridHeaderProps, GridRowProps, HexViewer, toText } from "../components";
|
import { CommandBar, Grid, GridCellProps, GridColumn, GridHeaderProps, GridRowProps, HexViewer, toText } from "../components";
|
||||||
import { globalState, PacketLogItem } from "../state";
|
import { globalState, PacketLogItem } from "../state";
|
||||||
import { Icons, RouteStackProps, useCallbackRef, withDisplayName } from "../utils";
|
import { Icons, RouteStackProps, useCallbackRef, withDisplayName } from "../utils";
|
||||||
|
@ -89,158 +88,160 @@ const useClasses = makeStyles({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const PacketLog: NextPage = () => {
|
const columns: Column[] = [
|
||||||
const classes = useClasses();
|
{
|
||||||
|
title: 'Direction',
|
||||||
|
width: 100,
|
||||||
|
CellComponent: withDisplayName('Direction')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||||
|
const item = globalState.logs[rowIndex];
|
||||||
|
|
||||||
const columns: Column[] = useMemo(() => [
|
const classes = useClasses();
|
||||||
{
|
|
||||||
key: 'direction',
|
|
||||||
title: 'Direction',
|
|
||||||
width: 100,
|
|
||||||
CellComponent: withDisplayName('Direction')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
|
||||||
const item = globalState.logs[rowIndex];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={mergeClasses(className, classes.code)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{item.direction}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'command',
|
|
||||||
title: 'Command',
|
|
||||||
width: 100,
|
|
||||||
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
|
||||||
const item = globalState.logs[rowIndex];
|
|
||||||
|
|
||||||
if (!item.commandString) {
|
|
||||||
item.commandString =
|
|
||||||
ADB_COMMAND_NAME[item.command as AdbCommand] ??
|
|
||||||
decodeUtf8(new Uint32Array([item.command]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={mergeClasses(className, classes.code)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{item.commandString}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'arg0',
|
|
||||||
title: 'Arg0',
|
|
||||||
width: 100,
|
|
||||||
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
|
||||||
const item = globalState.logs[rowIndex];
|
|
||||||
|
|
||||||
if (!item.arg0String) {
|
|
||||||
item.arg0String = item.arg0.toString(16).padStart(8, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={mergeClasses(className, classes.code)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{item.arg0String}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'arg1',
|
|
||||||
title: 'Arg1',
|
|
||||||
width: 100,
|
|
||||||
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
|
||||||
const item = globalState.logs[rowIndex];
|
|
||||||
|
|
||||||
if (!item.arg1String) {
|
|
||||||
item.arg1String = item.arg0.toString(16).padStart(8, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={mergeClasses(className, classes.code)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{item.arg1String}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'payload',
|
|
||||||
title: 'Payload',
|
|
||||||
width: 200,
|
|
||||||
flexGrow: 1,
|
|
||||||
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
|
||||||
const item = globalState.logs[rowIndex];
|
|
||||||
|
|
||||||
if (!item.payloadString) {
|
|
||||||
item.payloadString = toText(item.payload.subarray(0, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={mergeClasses(className, classes.code)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{item.payloadString}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
], [classes.code]);
|
|
||||||
|
|
||||||
const Header = useMemo(
|
|
||||||
() => withDisplayName('Header')(({
|
|
||||||
className,
|
|
||||||
columnIndex,
|
|
||||||
...rest
|
|
||||||
}: GridHeaderProps) => {
|
|
||||||
return (
|
|
||||||
<div className={mergeClasses(className, classes.header)} {...rest}>
|
|
||||||
{columns[columnIndex].title}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[classes.header, columns]
|
|
||||||
);
|
|
||||||
|
|
||||||
const Row = useMemo(
|
|
||||||
() => observer(function Row({
|
|
||||||
className,
|
|
||||||
rowIndex,
|
|
||||||
...rest
|
|
||||||
}: GridRowProps) {
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
const handleClick = useCallbackRef(() => {
|
|
||||||
runInAction(() => {
|
|
||||||
state.selectedPacket = globalState.logs[rowIndex];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={mergeClasses(
|
className={mergeClasses(className, classes.code)}
|
||||||
className,
|
|
||||||
classes.row,
|
|
||||||
state.selectedPacket === globalState.logs[rowIndex] && classes.selected
|
|
||||||
)}
|
|
||||||
onClick={handleClick}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
>
|
||||||
|
{item.direction}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
[classes]
|
},
|
||||||
|
{
|
||||||
|
title: 'Command',
|
||||||
|
width: 100,
|
||||||
|
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||||
|
const item = globalState.logs[rowIndex];
|
||||||
|
|
||||||
|
if (!item.commandString) {
|
||||||
|
item.commandString =
|
||||||
|
ADB_COMMAND_NAME[item.command as AdbCommand] ??
|
||||||
|
decodeUtf8(new Uint32Array([item.command]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={mergeClasses(className, classes.code)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{item.commandString}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Arg0',
|
||||||
|
width: 100,
|
||||||
|
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||||
|
const item = globalState.logs[rowIndex];
|
||||||
|
|
||||||
|
if (!item.arg0String) {
|
||||||
|
item.arg0String = item.arg0.toString(16).padStart(8, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={mergeClasses(className, classes.code)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{item.arg0String}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Arg1',
|
||||||
|
width: 100,
|
||||||
|
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||||
|
const item = globalState.logs[rowIndex];
|
||||||
|
|
||||||
|
if (!item.arg1String) {
|
||||||
|
item.arg1String = item.arg0.toString(16).padStart(8, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={mergeClasses(className, classes.code)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{item.arg1String}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Payload',
|
||||||
|
width: 200,
|
||||||
|
flexGrow: 1,
|
||||||
|
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||||
|
const item = globalState.logs[rowIndex];
|
||||||
|
|
||||||
|
if (!item.payloadString) {
|
||||||
|
item.payloadString = toText(item.payload.subarray(0, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={mergeClasses(className, classes.code)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{item.payloadString}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Header = withDisplayName('Header')(({
|
||||||
|
className,
|
||||||
|
columnIndex,
|
||||||
|
...rest
|
||||||
|
}: GridHeaderProps) => {
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mergeClasses(className, classes.header)} {...rest}>
|
||||||
|
{columns[columnIndex].title}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Row = observer(function Row({
|
||||||
|
className,
|
||||||
|
rowIndex,
|
||||||
|
...rest
|
||||||
|
}: GridRowProps) {
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
|
const handleClick = useCallbackRef(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
state.selectedPacket = globalState.logs[rowIndex];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={mergeClasses(
|
||||||
|
className,
|
||||||
|
classes.row,
|
||||||
|
state.selectedPacket === globalState.logs[rowIndex] && classes.selected
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PacketLog: NextPage = () => {
|
||||||
|
const classes = useClasses();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack {...RouteStackProps} tokens={{}}>
|
<Stack {...RouteStackProps} tokens={{}}>
|
||||||
|
|
|
@ -519,7 +519,7 @@ class ScrcpyPageState {
|
||||||
this.running = true;
|
this.running = true;
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
globalState.showErrorDialog(e.message);
|
globalState.showErrorDialog(e);
|
||||||
} finally {
|
} finally {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.connecting = false;
|
this.connecting = false;
|
||||||
|
|
|
@ -51,8 +51,8 @@ const Shell: NextPage = (): JSX.Element | null => {
|
||||||
connectingRef.current = true;
|
connectingRef.current = true;
|
||||||
const socket = await globalState.device.subprocess.shell();
|
const socket = await globalState.device.subprocess.shell();
|
||||||
terminal.socket = socket;
|
terminal.socket = socket;
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
globalState.showErrorDialog(e);
|
||||||
} finally {
|
} finally {
|
||||||
connectingRef.current = false;
|
connectingRef.current = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,13 @@ export class GlobalState {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
}
|
}
|
||||||
|
|
||||||
showErrorDialog(message: string) {
|
showErrorDialog(message: Error | string) {
|
||||||
this.errorDialogVisible = true;
|
this.errorDialogVisible = true;
|
||||||
this.errorDialogMessage = message;
|
if (message instanceof Error) {
|
||||||
|
this.errorDialogMessage = message.stack!;
|
||||||
|
} else {
|
||||||
|
this.errorDialogMessage = message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideErrorDialog() {
|
hideErrorDialog() {
|
||||||
|
|
247
apps/demo/src/utils/b-tree.ts
Normal file
247
apps/demo/src/utils/b-tree.ts
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
export function binarySearch(
|
||||||
|
array: Uint8Array,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
value: number,
|
||||||
|
) {
|
||||||
|
while (start <= end) {
|
||||||
|
const mid = (start + end) >> 1;
|
||||||
|
if (array[mid] === value) {
|
||||||
|
return mid;
|
||||||
|
} else if (array[mid] < value) {
|
||||||
|
start = mid + 1;
|
||||||
|
} else {
|
||||||
|
end = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -(start + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BTreeInsertionResult {
|
||||||
|
keys: Uint8Array;
|
||||||
|
keyCount: number;
|
||||||
|
children: BTreeNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BTreeNode {
|
||||||
|
order: number;
|
||||||
|
|
||||||
|
keys: Uint8Array;
|
||||||
|
keyCount: number;
|
||||||
|
|
||||||
|
children: BTreeNode[];
|
||||||
|
|
||||||
|
isLeaf = true;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
order: number,
|
||||||
|
keys: Uint8Array,
|
||||||
|
keyCount: number,
|
||||||
|
children: BTreeNode[]
|
||||||
|
) {
|
||||||
|
this.order = order;
|
||||||
|
this.keys = keys;
|
||||||
|
this.keyCount = keyCount;
|
||||||
|
this.children = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected split(value: number, index: number, split?: BTreeInsertionResult): BTreeInsertionResult {
|
||||||
|
const keys = new Uint8Array(this.order);
|
||||||
|
let children: BTreeNode[];
|
||||||
|
const mid = this.keyCount >> 1;
|
||||||
|
|
||||||
|
if (index < mid) {
|
||||||
|
keys.set(this.keys.subarray(mid - 1), 0);
|
||||||
|
|
||||||
|
this.keys.set(this.keys.subarray(index, mid - 1), index + 1);
|
||||||
|
this.keys[index] = value;
|
||||||
|
|
||||||
|
if (split) {
|
||||||
|
children = this.children.splice(mid - 1, this.order - mid + 1);
|
||||||
|
// TODO: this may cause the underlying array to grow (re-alloc and copy)
|
||||||
|
// investigate if this is a problem.
|
||||||
|
this.children.splice(
|
||||||
|
index,
|
||||||
|
0,
|
||||||
|
new BTreeNode(
|
||||||
|
this.order,
|
||||||
|
split.keys,
|
||||||
|
split.keyCount,
|
||||||
|
split.children
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children = new Array(this.order);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index !== mid) {
|
||||||
|
keys.set(this.keys.subarray(mid, index), 0);
|
||||||
|
}
|
||||||
|
keys[index - mid] = value;
|
||||||
|
keys.set(this.keys.subarray(index), index - mid + 1);
|
||||||
|
|
||||||
|
if (split) {
|
||||||
|
children = this.children.splice(mid, this.order - mid);
|
||||||
|
children.splice(
|
||||||
|
index - mid,
|
||||||
|
0,
|
||||||
|
new BTreeNode(
|
||||||
|
this.order,
|
||||||
|
split.keys.subarray(1),
|
||||||
|
split.keyCount,
|
||||||
|
split.children
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children = new Array(this.order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keyCount = mid;
|
||||||
|
return {
|
||||||
|
keys,
|
||||||
|
keyCount: this.order - 1 - mid,
|
||||||
|
children
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public insert(value: number): BTreeInsertionResult | undefined {
|
||||||
|
let index = binarySearch(this.keys, 0, this.keyCount - 1, value);
|
||||||
|
if (index >= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
index = -index - 1;
|
||||||
|
|
||||||
|
if (!this.isLeaf) {
|
||||||
|
let child = this.children[index];
|
||||||
|
|
||||||
|
if (!child) {
|
||||||
|
child = new BTreeNode(
|
||||||
|
this.order,
|
||||||
|
new Uint8Array(this.order - 1),
|
||||||
|
0,
|
||||||
|
new Array(this.order)
|
||||||
|
);
|
||||||
|
child.keys[0] = value;
|
||||||
|
child.keyCount = 1;
|
||||||
|
this.children[index] = child;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = child.insert(value);
|
||||||
|
if (split) {
|
||||||
|
if (this.keyCount === this.order - 1) {
|
||||||
|
value = split.keys[0];
|
||||||
|
split.keys = split.keys.subarray(1);
|
||||||
|
return this.split(value, index, split);
|
||||||
|
} else {
|
||||||
|
this.keys.set(this.keys.subarray(index, this.keyCount), index + 1);
|
||||||
|
this.keys[index] = split.keys[0];
|
||||||
|
this.keyCount += 1;
|
||||||
|
|
||||||
|
this.children.splice(
|
||||||
|
index,
|
||||||
|
0,
|
||||||
|
new BTreeNode(
|
||||||
|
this.order,
|
||||||
|
split.keys.subarray(1),
|
||||||
|
split.keyCount,
|
||||||
|
split.children
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.keyCount === this.order - 1) {
|
||||||
|
return this.split(value, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index !== this.keyCount) {
|
||||||
|
this.keys.set(this.keys.subarray(index, this.keyCount), index + 1);
|
||||||
|
}
|
||||||
|
this.keys[index] = value;
|
||||||
|
this.keyCount += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(): any[] {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < this.keyCount; i += 1) {
|
||||||
|
result.push(this.children[i]?.log());
|
||||||
|
result.push(this.keys[i]);
|
||||||
|
}
|
||||||
|
result.push(this.children[this.keyCount]?.log());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BTreeRoot extends BTreeNode {
|
||||||
|
public constructor(order: number) {
|
||||||
|
super(
|
||||||
|
order,
|
||||||
|
new Uint8Array(order - 1),
|
||||||
|
0,
|
||||||
|
new Array(order)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override split(value: number, index: number, split?: BTreeInsertionResult | undefined): BTreeInsertionResult {
|
||||||
|
split = super.split(value, index, split);
|
||||||
|
|
||||||
|
this.children[0] = new BTreeNode(
|
||||||
|
this.order,
|
||||||
|
this.keys,
|
||||||
|
this.keyCount,
|
||||||
|
this.children.slice(),
|
||||||
|
);
|
||||||
|
this.children[0].isLeaf = this.isLeaf;
|
||||||
|
|
||||||
|
this.children[1] = new BTreeNode(
|
||||||
|
this.order,
|
||||||
|
split.keys.subarray(1),
|
||||||
|
split.keyCount,
|
||||||
|
split.children,
|
||||||
|
);
|
||||||
|
this.children[1].isLeaf = this.isLeaf;
|
||||||
|
|
||||||
|
this.keys = new Uint8Array(this.order);
|
||||||
|
this.keys[0] = split.keys[0];
|
||||||
|
this.keyCount = 1;
|
||||||
|
this.isLeaf = false;
|
||||||
|
|
||||||
|
return split;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BTree {
|
||||||
|
order: number;
|
||||||
|
root: BTreeNode;
|
||||||
|
|
||||||
|
public constructor(order: number) {
|
||||||
|
this.order = order;
|
||||||
|
this.root = new BTreeRoot(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
public has(value: number) {
|
||||||
|
let node = this.root;
|
||||||
|
while (true) {
|
||||||
|
const index = binarySearch(node.keys, 0, node.keyCount - 1, value);
|
||||||
|
if (index >= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = node.children[-index - 1];
|
||||||
|
if (!node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public insert(value: number) {
|
||||||
|
this.root.insert(value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,6 @@
|
||||||
// cspell: ignore RSASSA
|
// cspell: ignore RSASSA
|
||||||
|
|
||||||
import { calculateBase64EncodedLength, calculatePublicKey, calculatePublicKeyLength, decodeBase64, encodeBase64, type AdbCredentialStore } from "@yume-chan/adb";
|
import { calculateBase64EncodedLength, calculatePublicKey, calculatePublicKeyLength, decodeBase64, decodeUtf8, encodeBase64, type AdbCredentialStore } from "@yume-chan/adb";
|
||||||
|
|
||||||
const Utf8Encoder = new TextEncoder();
|
|
||||||
const Utf8Decoder = new TextDecoder();
|
|
||||||
|
|
||||||
export function encodeUtf8(input: string): Uint8Array {
|
|
||||||
return Utf8Encoder.encode(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeUtf8(array: Uint8Array): string {
|
|
||||||
return Utf8Decoder.decode(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AdbWebCredentialStore implements AdbCredentialStore {
|
export default class AdbWebCredentialStore implements AdbCredentialStore {
|
||||||
public readonly localStorageKey: string;
|
public readonly localStorageKey: string;
|
||||||
|
|
|
@ -18,9 +18,10 @@ TypeScript implementation of Android Debug Bridge (ADB) protocol.
|
||||||
- [AdbAuthenticator](#adbauthenticator)
|
- [AdbAuthenticator](#adbauthenticator)
|
||||||
- [`authenticate`](#authenticate)
|
- [`authenticate`](#authenticate)
|
||||||
- [Stream multiplex](#stream-multiplex)
|
- [Stream multiplex](#stream-multiplex)
|
||||||
- [Backend](#backend-1)
|
|
||||||
- [Commands](#commands)
|
- [Commands](#commands)
|
||||||
- [childProcess](#childprocess)
|
- [subprocess](#subprocess)
|
||||||
|
- [raw mode](#raw-mode)
|
||||||
|
- [pty mode](#pty-mode)
|
||||||
- [usb](#usb)
|
- [usb](#usb)
|
||||||
- [tcpip](#tcpip)
|
- [tcpip](#tcpip)
|
||||||
- [sync](#sync)
|
- [sync](#sync)
|
||||||
|
@ -48,10 +49,9 @@ Each backend may have different requirements.
|
||||||
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||||
| ------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
|
| ------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
|
||||||
| `@yume-chan/struct`<sup>1</sup> | 67 | 79 | 68 | No | 14 | 8.3<sup>2</sup>, 11 |
|
| `@yume-chan/struct`<sup>1</sup> | 67 | 79 | 68 | No | 14 | 8.3<sup>2</sup>, 11 |
|
||||||
| [Streams][MDN_Streams] | 67 | 79 | No | No | 14.1 | 16.5 |
|
|
||||||
| *Overall* | 67 | 79 | No | No | 14.1 | 16.5 |
|
| *Overall* | 67 | 79 | No | No | 14.1 | 16.5 |
|
||||||
|
|
||||||
<sup>1</sup> `uint64` and `string` used.
|
<sup>1</sup> `uint64` and `string` are used.
|
||||||
|
|
||||||
<sup>2</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Need to be assigned to `globalThis`.
|
<sup>2</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Need to be assigned to `globalThis`.
|
||||||
|
|
||||||
|
@ -61,8 +61,6 @@ Each backend may have different requirements.
|
||||||
| --------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
|
| --------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
|
||||||
| Top-level await | 89 | 89 | 89 | No | 15 | 14.8 |
|
| Top-level await | 89 | 89 | 89 | No | 15 | 14.8 |
|
||||||
|
|
||||||
[MDN_Streams]: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
|
|
||||||
|
|
||||||
## Connection
|
## Connection
|
||||||
|
|
||||||
This library doesn't tie to a specific transportation method.
|
This library doesn't tie to a specific transportation method.
|
||||||
|
@ -79,7 +77,7 @@ connect(): ValueOrPromise<ReadableWritablePair<AdbPacketCore, AdbPacketInit>>
|
||||||
|
|
||||||
Connect to a device and create a pair of `AdbPacket` streams.
|
Connect to a device and create a pair of `AdbPacket` streams.
|
||||||
|
|
||||||
The backend is responsible for serializing and deserializing the packets, because it's extreme slow for WebUSB backend (`@yume-chan/adb-backend-webusb`) to read packets with unknown size.
|
The backend, instead of the core library, is responsible for serializing and deserializing the packets. Because it's extreme slow for WebUSB backend (`@yume-chan/adb-backend-webusb`) to read packets with unknown size.
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
|
@ -133,7 +131,7 @@ static async authenticate(
|
||||||
|
|
||||||
Call this method to authenticate the connection and create an `Adb` instance.
|
Call this method to authenticate the connection and create an `Adb` instance.
|
||||||
|
|
||||||
It's possible to call `authenticate` multiple times on a single connection, every time the device receives a `CNXN` packet, it resets its internal state, and starts a new authentication process.
|
If an authentication process failed, it's possible to call `authenticate` again on the same connection (`AdbPacket` stream pair). Every time the device receives a `CNXN` packet, it resets all internal state, and starts a new authentication process.
|
||||||
|
|
||||||
## Stream multiplex
|
## Stream multiplex
|
||||||
|
|
||||||
|
@ -144,27 +142,36 @@ ADB commands are all based on streams. Multiple streams can send and receive at
|
||||||
3. Client and server read/write on the stream.
|
3. Client and server read/write on the stream.
|
||||||
4. Client/server sends a `CLSE` to close the stream.
|
4. Client/server sends a `CLSE` to close the stream.
|
||||||
|
|
||||||
The `Backend` is responsible for reading and writing data from underlying source.
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### childProcess
|
### subprocess
|
||||||
|
|
||||||
Spawns child process on server. ADB has two shell modes:
|
ADB has two subprocess invocation modes and two data protocols (4 combinations).
|
||||||
|
|
||||||
| | Legacy mode | Shell Protocol |
|
#### raw mode
|
||||||
|
|
||||||
|
In raw mode, Shell protocol transfers `stdout` and `stderr` separately. It also supports returning exit code.
|
||||||
|
|
||||||
|
| | Legacy protocol | Shell Protocol |
|
||||||
| --------------------------- | --------------------------- | ---------------------------- |
|
| --------------------------- | --------------------------- | ---------------------------- |
|
||||||
| Feature flag | - | `shell_v2` |
|
| Feature flag | - | `shell_v2` |
|
||||||
| Implementation | `AdbNoneSubprocessProtocol` | `AdbShellSubprocessProtocol` |
|
| Implementation | `AdbNoneSubprocessProtocol` | `AdbShellSubprocessProtocol` |
|
||||||
| Splitting stdout and stderr | No | Yes |
|
| Splitting stdout and stderr | No | Yes |
|
||||||
| Returning exit code | No | Yes |
|
| Returning exit code | No | Yes |
|
||||||
|
|
||||||
|
Use `spawn` method to create a subprocess in raw mode.
|
||||||
|
|
||||||
|
#### pty mode
|
||||||
|
|
||||||
|
In PTY mode, the subprocess has a pseudo-terminal, so it can send special control sequences like clear screen and set cursor position. The two protocols both send data in `stdout`, but Shell Protocol also supports resizing the terminal from client.
|
||||||
|
|
||||||
|
| | Legacy protocol | Shell Protocol |
|
||||||
|
| --------------------------- | --------------------------- | ---------------------------- |
|
||||||
|
| Feature flag | - | `shell_v2` |
|
||||||
|
| Implementation | `AdbNoneSubprocessProtocol` | `AdbShellSubprocessProtocol` |
|
||||||
| Resizing window | No | Yes |
|
| Resizing window | No | Yes |
|
||||||
|
|
||||||
The `Adb#childProcess#shell` and `Adb#childProcess#spawn` methods accepts a list of implementations, and will use the first supported one.
|
Use `shell` method to create a subprocess in PTY mode.
|
||||||
|
|
||||||
For simple command invocation, usually the `AdbNoneSubprocessProtocol` is enough.
|
|
||||||
|
|
||||||
### usb
|
### usb
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ 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 { pipeFrom, 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";
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
||||||
transform: (chunk, controller) => {
|
transform: (chunk, controller) => {
|
||||||
if (chunk.id === AdbShellProtocolId.Exit) {
|
if (chunk.id === AdbShellProtocolId.Exit) {
|
||||||
this._exit.resolve(new Uint8Array(chunk.data)[0]!);
|
this._exit.resolve(new Uint8Array(chunk.data)[0]!);
|
||||||
// We can let `StdoutTransformStream` to process `AdbShellProtocolId.Exit`,
|
// We can let `StdoutDeserializeStream` to process `AdbShellProtocolId.Exit`,
|
||||||
// but since we need this `TransformStream` to capture the exit code anyway,
|
// but since we need this `TransformStream` to capture the exit code anyway,
|
||||||
// terminating child streams here is killing two birds with one stone.
|
// terminating child streams here is killing two birds with one stone.
|
||||||
controller.terminate();
|
controller.terminate();
|
||||||
|
@ -164,9 +164,10 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
||||||
.pipeThrough(new StructSerializeStream(AdbShellProtocolPacket))
|
.pipeThrough(new StructSerializeStream(AdbShellProtocolPacket))
|
||||||
.pipeTo(socket.writable);
|
.pipeTo(socket.writable);
|
||||||
|
|
||||||
const { readable, writable } = new StdinSerializeStream();
|
this._stdin = pipeFrom(
|
||||||
this._stdin = writable;
|
multiplexer.createWriteable(),
|
||||||
readable.pipeTo(multiplexer.createWriteable());
|
new StdinSerializeStream()
|
||||||
|
);
|
||||||
|
|
||||||
this._socketWriter = multiplexer.createWriteable().getWriter();
|
this._socketWriter = multiplexer.createWriteable().getWriter();
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,7 +151,7 @@ export class AdbSync extends AutoDisposable {
|
||||||
if (this.needPushMkdirWorkaround) {
|
if (this.needPushMkdirWorkaround) {
|
||||||
// It may fail if the path is already existed.
|
// It may fail if the path is already existed.
|
||||||
// Ignore the result.
|
// Ignore the result.
|
||||||
// TODO: test this
|
// TODO: sync: test this
|
||||||
await this.adb.subprocess.spawnAndWait([
|
await this.adb.subprocess.spawnAndWait([
|
||||||
'mkdir',
|
'mkdir',
|
||||||
'-p',
|
'-p',
|
||||||
|
|
|
@ -45,9 +45,6 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
private readonly incomingSocketEvent = this.addDisposable(new EventEmitter<AdbIncomingSocketEventArgs>());
|
private readonly incomingSocketEvent = this.addDisposable(new EventEmitter<AdbIncomingSocketEventArgs>());
|
||||||
public get onIncomingSocket() { return this.incomingSocketEvent.event; }
|
public get onIncomingSocket() { return this.incomingSocketEvent.event; }
|
||||||
|
|
||||||
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
|
|
||||||
public get onError() { return this.errorEvent.event; }
|
|
||||||
|
|
||||||
private _abortController = new AbortController();
|
private _abortController = new AbortController();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
@ -83,8 +80,6 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorEvent.fire(e as Error);
|
|
||||||
|
|
||||||
// Throw error here will stop the pipe
|
// Throw error here will stop the pipe
|
||||||
// But won't close `readable` because of `preventCancel: true`
|
// But won't close `readable` because of `preventCancel: true`
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -96,8 +91,8 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.dispose();
|
this.dispose();
|
||||||
}, () => {
|
}, (e) => {
|
||||||
// TODO: AdbPacketDispatcher: reject `_disconnected` when pipe errored?
|
this._disconnected.reject(e);
|
||||||
this.dispose();
|
this.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -475,8 +475,8 @@ export class PushReadableStream<T> extends ReadableStream<T> {
|
||||||
waterMarkLow?.resolve();
|
waterMarkLow?.resolve();
|
||||||
},
|
},
|
||||||
cancel: async (reason) => {
|
cancel: async (reason) => {
|
||||||
waterMarkLow?.reject(reason);
|
|
||||||
canceled.abort();
|
canceled.abort();
|
||||||
|
waterMarkLow?.reject(reason);
|
||||||
},
|
},
|
||||||
}, strategy);
|
}, strategy);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
|
|
||||||
// @ts-expect-error @types/node missing `TextEncoder`
|
|
||||||
const Utf8Encoder = new TextEncoder();
|
|
||||||
// @ts-expect-error @types/node missing `TextDecoder`
|
|
||||||
const Utf8Decoder = new TextDecoder();
|
|
||||||
|
|
||||||
export function encodeUtf8(input: string): Uint8Array {
|
|
||||||
return Utf8Encoder.encode(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string {
|
|
||||||
return Utf8Decoder.decode(buffer);
|
|
||||||
}
|
|
|
@ -1,3 +1,3 @@
|
||||||
|
export { decodeUtf8, encodeUtf8 } from '@yume-chan/struct';
|
||||||
export * from './auto-reset-event.js';
|
export * from './auto-reset-event.js';
|
||||||
export * from './base64.js';
|
export * from './base64.js';
|
||||||
export * from './encoding.js';
|
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/adb": "^0.0.12",
|
"@yume-chan/adb": "^0.0.12",
|
||||||
|
"@yume-chan/struct": "^0.0.12",
|
||||||
"tslib": "^2.3.1"
|
"tslib": "^2.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// cspell: ignore logcat
|
||||||
|
|
||||||
export * from './bug-report.js';
|
export * from './bug-report.js';
|
||||||
export * from './demo-mode.js';
|
export * from './demo-mode.js';
|
||||||
|
export * from './logcat.js';
|
||||||
export * from './settings.js';
|
export * from './settings.js';
|
||||||
|
|
187
libraries/android-bin/src/logcat.ts
Normal file
187
libraries/android-bin/src/logcat.ts
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
// cspell: ignore logcat
|
||||||
|
|
||||||
|
import { AdbCommandBase, BufferedStream, BufferedStreamEndedError, DecodeUtf8Stream, ReadableStream, SplitLineStream, WritableStream } from "@yume-chan/adb";
|
||||||
|
import Struct, { StructAsyncDeserializeStream } from "@yume-chan/struct";
|
||||||
|
|
||||||
|
// `adb logcat` is an alias to `adb shell logcat`
|
||||||
|
// so instead of adding to core library, it's implemented here
|
||||||
|
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/android/log.h;l=141;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
|
||||||
|
export enum LogId {
|
||||||
|
All = -1,
|
||||||
|
Main,
|
||||||
|
Radio,
|
||||||
|
Events,
|
||||||
|
System,
|
||||||
|
Crash,
|
||||||
|
Stats,
|
||||||
|
Security,
|
||||||
|
Kernel,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LogPriority {
|
||||||
|
Unknown,
|
||||||
|
Default,
|
||||||
|
Verbose,
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
Fatal,
|
||||||
|
Silent,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogcatOptions {
|
||||||
|
pid?: number;
|
||||||
|
ids?: LogId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const NANOSECONDS_PER_SECOND = BigInt(1e9);
|
||||||
|
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/log/log_read.h;l=39;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
|
||||||
|
export const LoggerEntry =
|
||||||
|
new Struct({ littleEndian: true })
|
||||||
|
.uint16('payloadSize')
|
||||||
|
.uint16('headerSize')
|
||||||
|
.int32('pid')
|
||||||
|
.uint32('tid')
|
||||||
|
.uint32('second')
|
||||||
|
.uint32('nanoseconds')
|
||||||
|
.uint32('logId')
|
||||||
|
.uint32('uid')
|
||||||
|
.extra({
|
||||||
|
get timestamp() {
|
||||||
|
return BigInt(this.second) * NANOSECONDS_PER_SECOND + BigInt(this.nanoseconds);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoggerEntry = typeof LoggerEntry['TDeserializeResult'];
|
||||||
|
|
||||||
|
export interface LogMessage extends LoggerEntry {
|
||||||
|
priority: LogPriority;
|
||||||
|
payload: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deserializeLogMessage(stream: StructAsyncDeserializeStream): Promise<LogMessage> {
|
||||||
|
const entry = await LoggerEntry.deserialize(stream);
|
||||||
|
await stream.read(entry.headerSize - LoggerEntry.size);
|
||||||
|
const priority = (await stream.read(1))[0] as LogPriority;
|
||||||
|
const payload = await stream.read(entry.payloadSize - 1);
|
||||||
|
return { ...entry, priority, payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogSize {
|
||||||
|
id: LogId;
|
||||||
|
size: number;
|
||||||
|
readable?: number;
|
||||||
|
consumed: number;
|
||||||
|
maxEntrySize: number;
|
||||||
|
maxPayloadSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Logcat extends AdbCommandBase {
|
||||||
|
public static logIdToName(id: LogId): string {
|
||||||
|
return LogId[id]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static logNameToId(name: string): LogId {
|
||||||
|
const key = name[0]!.toUpperCase() + name.substring(1);
|
||||||
|
return (LogId as any)[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static joinLogId(ids: LogId[]): string {
|
||||||
|
return ids.map(id => Logcat.logIdToName(id)).join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static parseSize(value: number, multiplier: string): number {
|
||||||
|
const MULTIPLIERS = ['', 'Ki', 'Mi', 'Gi'];
|
||||||
|
return value * 1024 ** (MULTIPLIERS.indexOf(multiplier) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: logcat: Support output format before Android 10
|
||||||
|
// ref https://android-review.googlesource.com/c/platform/system/core/+/748128
|
||||||
|
public static readonly LOG_SIZE_REGEX_10 = /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed\), max entry is (.*) B, max payload is (.*) B/;
|
||||||
|
|
||||||
|
// Android 11 added `readable` part
|
||||||
|
// ref https://android-review.googlesource.com/c/platform/system/core/+/1390940
|
||||||
|
public static readonly LOG_SIZE_REGEX_11 = /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed, (.*) (.*)B readable\), max entry is (.*) B, max payload is (.*) B/;
|
||||||
|
|
||||||
|
public async getLogSize(ids?: LogId[]): Promise<LogSize[]> {
|
||||||
|
const { stdout } = await this.adb.subprocess.spawn([
|
||||||
|
'logcat',
|
||||||
|
'-g',
|
||||||
|
...(ids ? ['-b', Logcat.joinLogId(ids)] : [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result: LogSize[] = [];
|
||||||
|
await stdout
|
||||||
|
.pipeThrough(new DecodeUtf8Stream())
|
||||||
|
.pipeThrough(new SplitLineStream())
|
||||||
|
.pipeTo(new WritableStream({
|
||||||
|
write(chunk) {
|
||||||
|
let match = chunk.match(Logcat.LOG_SIZE_REGEX_11);
|
||||||
|
if (match) {
|
||||||
|
result.push({
|
||||||
|
id: Logcat.logNameToId(match[1]!),
|
||||||
|
size: Logcat.parseSize(Number.parseInt(match[2]!, 10), match[3]!),
|
||||||
|
readable: Logcat.parseSize(Number.parseInt(match[6]!, 10), match[7]!),
|
||||||
|
consumed: Logcat.parseSize(Number.parseInt(match[4]!, 10), match[5]!),
|
||||||
|
maxEntrySize: parseInt(match[8]!, 10),
|
||||||
|
maxPayloadSize: parseInt(match[9]!, 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match = chunk.match(Logcat.LOG_SIZE_REGEX_10);
|
||||||
|
if (match) {
|
||||||
|
result.push({
|
||||||
|
id: Logcat.logNameToId(match[1]!),
|
||||||
|
size: Logcat.parseSize(Number.parseInt(match[2]!, 10), match[3]!),
|
||||||
|
consumed: Logcat.parseSize(Number.parseInt(match[4]!, 10), match[5]!),
|
||||||
|
maxEntrySize: parseInt(match[6]!, 10),
|
||||||
|
maxPayloadSize: parseInt(match[7]!, 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async clear(ids?: LogId[]) {
|
||||||
|
await this.adb.subprocess.spawnAndWait([
|
||||||
|
'logcat',
|
||||||
|
'-c',
|
||||||
|
...(ids ? ['-b', Logcat.joinLogId(ids)] : []),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public binary(options?: LogcatOptions): ReadableStream<LogMessage> {
|
||||||
|
let bufferedStream: BufferedStream;
|
||||||
|
return new ReadableStream({
|
||||||
|
start: async () => {
|
||||||
|
const { stdout } = await this.adb.subprocess.spawn([
|
||||||
|
'logcat',
|
||||||
|
'-B',
|
||||||
|
...(options?.pid ? ['--pid', options.pid.toString()] : []),
|
||||||
|
...(options?.ids ? ['-b', Logcat.joinLogId(options.ids)] : [])
|
||||||
|
]);
|
||||||
|
bufferedStream = new BufferedStream(stdout);
|
||||||
|
},
|
||||||
|
async pull(controller) {
|
||||||
|
try {
|
||||||
|
const entry = await deserializeLogMessage(bufferedStream);
|
||||||
|
controller.enqueue(entry);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof BufferedStreamEndedError) {
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
bufferedStream.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,3 @@ export function delay(time: number): Promise<void> {
|
||||||
(globalThis as any).setTimeout(resolve, time);
|
(globalThis as any).setTimeout(resolve, time);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const Utf8Encoder = new TextEncoder();
|
|
||||||
const Utf8Decoder = new TextDecoder();
|
|
||||||
|
|
||||||
export function encodeUtf8(input: string): ArrayBuffer {
|
|
||||||
return Utf8Encoder.encode(input).buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeUtf8(buffer: ArrayBuffer): string {
|
|
||||||
return Utf8Decoder.decode(buffer);
|
|
||||||
}
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ export function placeholder<T>(): T {
|
||||||
|
|
||||||
// Node.js 8.3 ships `TextEncoder` and `TextDecoder` in `util` module.
|
// Node.js 8.3 ships `TextEncoder` and `TextDecoder` in `util` module.
|
||||||
// But using top level await to load them requires Node.js 14.1.
|
// But using top level await to load them requires Node.js 14.1.
|
||||||
// So there is no point to do that. Let's just assume they exists in global.
|
// So there is no point to do that. Let's just assume they exist in global.
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const Utf8Encoder = new TextEncoder();
|
const Utf8Encoder = new TextEncoder();
|
||||||
|
@ -65,6 +65,6 @@ export function encodeUtf8(input: string): Uint8Array {
|
||||||
return Utf8Encoder.encode(input);
|
return Utf8Encoder.encode(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeUtf8(buffer: Uint8Array): string {
|
export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string {
|
||||||
return Utf8Decoder.decode(buffer);
|
return Utf8Decoder.decode(buffer);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue