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.disconnected.then(() => {
|
||||
globalState.setDevice(undefined, undefined);
|
||||
}, (e) => {
|
||||
globalState.showErrorDialog(e);
|
||||
globalState.setDevice(undefined, undefined);
|
||||
});
|
||||
globalState.setDevice(selectedBackend, device);
|
||||
} catch (e) {
|
||||
|
@ -170,7 +173,7 @@ function _Connect(): JSX.Element | null {
|
|||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
globalState.showErrorDialog(e.message);
|
||||
globalState.showErrorDialog(e);
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
|
@ -180,7 +183,7 @@ function _Connect(): JSX.Element | null {
|
|||
await globalState.device!.dispose();
|
||||
globalState.setDevice(undefined, undefined);
|
||||
} catch (e: any) {
|
||||
globalState.showErrorDialog(e.message);
|
||||
globalState.showErrorDialog(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -140,8 +140,8 @@ class FileManagerState {
|
|||
const itemPath = path.resolve(this.path, item.name);
|
||||
await sync.read(itemPath)
|
||||
.pipeTo(saveFile(item.name, Number(item.size)));
|
||||
} catch (e) {
|
||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
||||
} catch (e: any) {
|
||||
globalState.showErrorDialog(e);
|
||||
} finally {
|
||||
sync.dispose();
|
||||
}
|
||||
|
@ -169,8 +169,8 @@ class FileManagerState {
|
|||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
||||
} catch (e: any) {
|
||||
globalState.showErrorDialog(e);
|
||||
} finally {
|
||||
this.loadFiles();
|
||||
}
|
||||
|
@ -481,8 +481,8 @@ class FileManagerState {
|
|||
} finally {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
} catch (e) {
|
||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
||||
} catch (e: any) {
|
||||
globalState.showErrorDialog(e);
|
||||
} finally {
|
||||
sync.dispose();
|
||||
this.loadFiles();
|
||||
|
|
|
@ -45,8 +45,8 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
|
|||
try {
|
||||
const framebuffer = await globalState.device.framebuffer();
|
||||
state.setImage(framebuffer);
|
||||
} catch (e) {
|
||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
||||
} catch (e: any) {
|
||||
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 { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useMemo } from "react";
|
||||
import { CommandBar, Grid, GridCellProps, GridColumn, GridHeaderProps, GridRowProps, HexViewer, toText } from "../components";
|
||||
import { globalState, PacketLogItem } from "../state";
|
||||
import { Icons, RouteStackProps, useCallbackRef, withDisplayName } from "../utils";
|
||||
|
@ -89,17 +88,15 @@ const useClasses = makeStyles({
|
|||
},
|
||||
});
|
||||
|
||||
const PacketLog: NextPage = () => {
|
||||
const classes = useClasses();
|
||||
|
||||
const columns: Column[] = useMemo(() => [
|
||||
const columns: Column[] = [
|
||||
{
|
||||
key: 'direction',
|
||||
title: 'Direction',
|
||||
width: 100,
|
||||
CellComponent: withDisplayName('Direction')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||
const item = globalState.logs[rowIndex];
|
||||
|
||||
const classes = useClasses();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={mergeClasses(className, classes.code)}
|
||||
|
@ -111,7 +108,6 @@ const PacketLog: NextPage = () => {
|
|||
}),
|
||||
},
|
||||
{
|
||||
key: 'command',
|
||||
title: 'Command',
|
||||
width: 100,
|
||||
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||
|
@ -123,6 +119,8 @@ const PacketLog: NextPage = () => {
|
|||
decodeUtf8(new Uint32Array([item.command]));
|
||||
}
|
||||
|
||||
const classes = useClasses();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={mergeClasses(className, classes.code)}
|
||||
|
@ -134,7 +132,6 @@ const PacketLog: NextPage = () => {
|
|||
}),
|
||||
},
|
||||
{
|
||||
key: 'arg0',
|
||||
title: 'Arg0',
|
||||
width: 100,
|
||||
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||
|
@ -144,6 +141,8 @@ const PacketLog: NextPage = () => {
|
|||
item.arg0String = item.arg0.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
const classes = useClasses();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={mergeClasses(className, classes.code)}
|
||||
|
@ -155,7 +154,6 @@ const PacketLog: NextPage = () => {
|
|||
}),
|
||||
},
|
||||
{
|
||||
key: 'arg1',
|
||||
title: 'Arg1',
|
||||
width: 100,
|
||||
CellComponent: withDisplayName('Command')(({ className, rowIndex, ...rest }: GridCellProps) => {
|
||||
|
@ -165,6 +163,8 @@ const PacketLog: NextPage = () => {
|
|||
item.arg1String = item.arg0.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
const classes = useClasses();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={mergeClasses(className, classes.code)}
|
||||
|
@ -176,7 +176,6 @@ const PacketLog: NextPage = () => {
|
|||
}),
|
||||
},
|
||||
{
|
||||
key: 'payload',
|
||||
title: 'Payload',
|
||||
width: 200,
|
||||
flexGrow: 1,
|
||||
|
@ -187,6 +186,8 @@ const PacketLog: NextPage = () => {
|
|||
item.payloadString = toText(item.payload.subarray(0, 100));
|
||||
}
|
||||
|
||||
const classes = useClasses();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={mergeClasses(className, classes.code)}
|
||||
|
@ -197,30 +198,29 @@ const PacketLog: NextPage = () => {
|
|||
);
|
||||
}),
|
||||
},
|
||||
], [classes.code]);
|
||||
];
|
||||
|
||||
const Header = useMemo(
|
||||
() => withDisplayName('Header')(({
|
||||
const Header = withDisplayName('Header')(({
|
||||
className,
|
||||
columnIndex,
|
||||
...rest
|
||||
}: GridHeaderProps) => {
|
||||
}: GridHeaderProps) => {
|
||||
const classes = useClasses();
|
||||
|
||||
return (
|
||||
<div className={mergeClasses(className, classes.header)} {...rest}>
|
||||
{columns[columnIndex].title}
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
[classes.header, columns]
|
||||
);
|
||||
});
|
||||
|
||||
const Row = useMemo(
|
||||
() => observer(function Row({
|
||||
const Row = observer(function Row({
|
||||
className,
|
||||
rowIndex,
|
||||
...rest
|
||||
}: GridRowProps) {
|
||||
/* eslint-disable-next-line */
|
||||
}: GridRowProps) {
|
||||
const classes = useClasses();
|
||||
|
||||
const handleClick = useCallbackRef(() => {
|
||||
runInAction(() => {
|
||||
state.selectedPacket = globalState.logs[rowIndex];
|
||||
|
@ -238,9 +238,10 @@ const PacketLog: NextPage = () => {
|
|||
{...rest}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
[classes]
|
||||
);
|
||||
});
|
||||
|
||||
const PacketLog: NextPage = () => {
|
||||
const classes = useClasses();
|
||||
|
||||
return (
|
||||
<Stack {...RouteStackProps} tokens={{}}>
|
||||
|
|
|
@ -519,7 +519,7 @@ class ScrcpyPageState {
|
|||
this.running = true;
|
||||
});
|
||||
} catch (e: any) {
|
||||
globalState.showErrorDialog(e.message);
|
||||
globalState.showErrorDialog(e);
|
||||
} finally {
|
||||
runInAction(() => {
|
||||
this.connecting = false;
|
||||
|
|
|
@ -51,8 +51,8 @@ const Shell: NextPage = (): JSX.Element | null => {
|
|||
connectingRef.current = true;
|
||||
const socket = await globalState.device.subprocess.shell();
|
||||
terminal.socket = socket;
|
||||
} catch (e) {
|
||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
||||
} catch (e: any) {
|
||||
globalState.showErrorDialog(e);
|
||||
} finally {
|
||||
connectingRef.current = false;
|
||||
}
|
||||
|
|
|
@ -34,10 +34,14 @@ export class GlobalState {
|
|||
this.device = device;
|
||||
}
|
||||
|
||||
showErrorDialog(message: string) {
|
||||
showErrorDialog(message: Error | string) {
|
||||
this.errorDialogVisible = true;
|
||||
if (message instanceof Error) {
|
||||
this.errorDialogMessage = message.stack!;
|
||||
} else {
|
||||
this.errorDialogMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
hideErrorDialog() {
|
||||
this.errorDialogVisible = false;
|
||||
|
|
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
|
||||
|
||||
import { calculateBase64EncodedLength, calculatePublicKey, calculatePublicKeyLength, decodeBase64, 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);
|
||||
}
|
||||
import { calculateBase64EncodedLength, calculatePublicKey, calculatePublicKeyLength, decodeBase64, decodeUtf8, encodeBase64, type AdbCredentialStore } from "@yume-chan/adb";
|
||||
|
||||
export default class AdbWebCredentialStore implements AdbCredentialStore {
|
||||
public readonly localStorageKey: string;
|
||||
|
|
|
@ -18,9 +18,10 @@ TypeScript implementation of Android Debug Bridge (ADB) protocol.
|
|||
- [AdbAuthenticator](#adbauthenticator)
|
||||
- [`authenticate`](#authenticate)
|
||||
- [Stream multiplex](#stream-multiplex)
|
||||
- [Backend](#backend-1)
|
||||
- [Commands](#commands)
|
||||
- [childProcess](#childprocess)
|
||||
- [subprocess](#subprocess)
|
||||
- [raw mode](#raw-mode)
|
||||
- [pty mode](#pty-mode)
|
||||
- [usb](#usb)
|
||||
- [tcpip](#tcpip)
|
||||
- [sync](#sync)
|
||||
|
@ -48,10 +49,9 @@ Each backend may have different requirements.
|
|||
| | 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 |
|
||||
| [Streams][MDN_Streams] | 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`.
|
||||
|
||||
|
@ -61,8 +61,6 @@ Each backend may have different requirements.
|
|||
| --------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
|
||||
| Top-level await | 89 | 89 | 89 | No | 15 | 14.8 |
|
||||
|
||||
[MDN_Streams]: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
|
||||
|
||||
## Connection
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -133,7 +131,7 @@ static async authenticate(
|
|||
|
||||
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
|
||||
|
||||
|
@ -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.
|
||||
4. Client/server sends a `CLSE` to close the stream.
|
||||
|
||||
The `Backend` is responsible for reading and writing data from underlying source.
|
||||
|
||||
### Backend
|
||||
|
||||
## 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` |
|
||||
| Implementation | `AdbNoneSubprocessProtocol` | `AdbShellSubprocessProtocol` |
|
||||
| Splitting stdout and stderr | 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 |
|
||||
|
||||
The `Adb#childProcess#shell` and `Adb#childProcess#spawn` methods accepts a list of implementations, and will use the first supported one.
|
||||
|
||||
For simple command invocation, usually the `AdbNoneSubprocessProtocol` is enough.
|
||||
Use `shell` method to create a subprocess in PTY mode.
|
||||
|
||||
### usb
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import Struct, { placeholder, type StructValueType } from "@yume-chan/struct";
|
|||
import type { Adb } from "../../../adb.js";
|
||||
import { AdbFeatures } from "../../../features.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 type { AdbSubprocessProtocol } from "./types.js";
|
||||
|
||||
|
@ -144,7 +144,7 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
|||
transform: (chunk, controller) => {
|
||||
if (chunk.id === AdbShellProtocolId.Exit) {
|
||||
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,
|
||||
// terminating child streams here is killing two birds with one stone.
|
||||
controller.terminate();
|
||||
|
@ -164,9 +164,10 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
|||
.pipeThrough(new StructSerializeStream(AdbShellProtocolPacket))
|
||||
.pipeTo(socket.writable);
|
||||
|
||||
const { readable, writable } = new StdinSerializeStream();
|
||||
this._stdin = writable;
|
||||
readable.pipeTo(multiplexer.createWriteable());
|
||||
this._stdin = pipeFrom(
|
||||
multiplexer.createWriteable(),
|
||||
new StdinSerializeStream()
|
||||
);
|
||||
|
||||
this._socketWriter = multiplexer.createWriteable().getWriter();
|
||||
}
|
||||
|
|
|
@ -151,7 +151,7 @@ export class AdbSync extends AutoDisposable {
|
|||
if (this.needPushMkdirWorkaround) {
|
||||
// It may fail if the path is already existed.
|
||||
// Ignore the result.
|
||||
// TODO: test this
|
||||
// TODO: sync: test this
|
||||
await this.adb.subprocess.spawnAndWait([
|
||||
'mkdir',
|
||||
'-p',
|
||||
|
|
|
@ -45,9 +45,6 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
private readonly incomingSocketEvent = this.addDisposable(new EventEmitter<AdbIncomingSocketEventArgs>());
|
||||
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();
|
||||
|
||||
public constructor(
|
||||
|
@ -83,8 +80,6 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorEvent.fire(e as Error);
|
||||
|
||||
// Throw error here will stop the pipe
|
||||
// But won't close `readable` because of `preventCancel: true`
|
||||
throw e;
|
||||
|
@ -96,8 +91,8 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
})
|
||||
.then(() => {
|
||||
this.dispose();
|
||||
}, () => {
|
||||
// TODO: AdbPacketDispatcher: reject `_disconnected` when pipe errored?
|
||||
}, (e) => {
|
||||
this._disconnected.reject(e);
|
||||
this.dispose();
|
||||
});
|
||||
|
||||
|
|
|
@ -475,8 +475,8 @@ export class PushReadableStream<T> extends ReadableStream<T> {
|
|||
waterMarkLow?.resolve();
|
||||
},
|
||||
cancel: async (reason) => {
|
||||
waterMarkLow?.reject(reason);
|
||||
canceled.abort();
|
||||
waterMarkLow?.reject(reason);
|
||||
},
|
||||
}, 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 './base64.js';
|
||||
export * from './encoding.js';
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/adb": "^0.0.12",
|
||||
"@yume-chan/struct": "^0.0.12",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// cspell: ignore logcat
|
||||
|
||||
export * from './bug-report.js';
|
||||
export * from './demo-mode.js';
|
||||
export * from './logcat.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);
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
// 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
|
||||
const Utf8Encoder = new TextEncoder();
|
||||
|
@ -65,6 +65,6 @@ export function encodeUtf8(input: string): Uint8Array {
|
|||
return Utf8Encoder.encode(input);
|
||||
}
|
||||
|
||||
export function decodeUtf8(buffer: Uint8Array): string {
|
||||
export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string {
|
||||
return Utf8Decoder.decode(buffer);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue