mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-04 02:09:18 +02:00
feat(bin): add cmd wrapper
This commit is contained in:
parent
9c5d2d8a5c
commit
225e369f53
19 changed files with 511 additions and 332 deletions
|
@ -67,13 +67,15 @@ export class ScrcpyPageState {
|
||||||
|
|
||||||
async pushServer() {
|
async pushServer() {
|
||||||
const serverBuffer = await fetchServer();
|
const serverBuffer = await fetchServer();
|
||||||
|
await AdbScrcpyClient.pushServer(
|
||||||
await new ReadableStream<Uint8Array>({
|
GLOBAL_STATE.device!,
|
||||||
start(controller) {
|
new ReadableStream<Uint8Array>({
|
||||||
controller.enqueue(serverBuffer);
|
start(controller) {
|
||||||
controller.close();
|
controller.enqueue(serverBuffer);
|
||||||
},
|
controller.close();
|
||||||
}).pipeTo(AdbScrcpyClient.pushServer(GLOBAL_STATE.device!));
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
decoder: H264Decoder | undefined = undefined;
|
decoder: H264Decoder | undefined = undefined;
|
||||||
|
@ -186,21 +188,25 @@ export class ScrcpyPageState {
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new ReadableStream<Uint8Array>({
|
await AdbScrcpyClient.pushServer(
|
||||||
start(controller) {
|
GLOBAL_STATE.device!,
|
||||||
controller.enqueue(serverBuffer);
|
new ReadableStream<Uint8Array>({
|
||||||
controller.close();
|
start(controller) {
|
||||||
},
|
controller.enqueue(serverBuffer);
|
||||||
})
|
controller.close();
|
||||||
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
},
|
||||||
.pipeThrough(
|
})
|
||||||
new ProgressStream(
|
// In fact `pushServer` will pipe the stream through a ChunkStream,
|
||||||
action((progress) => {
|
// but without this pipeThrough, the progress will not be updated.
|
||||||
this.serverUploadedSize = progress;
|
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
||||||
})
|
.pipeThrough(
|
||||||
|
new ProgressStream(
|
||||||
|
action((progress) => {
|
||||||
|
this.serverUploadedSize = progress;
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
.pipeTo(AdbScrcpyClient.pushServer(GLOBAL_STATE.device!));
|
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.serverUploadSpeed =
|
this.serverUploadSpeed =
|
||||||
|
|
|
@ -2,21 +2,22 @@
|
||||||
|
|
||||||
import { AdbSubprocessProtocol, encodeUtf8 } from "@yume-chan/adb";
|
import { AdbSubprocessProtocol, encodeUtf8 } from "@yume-chan/adb";
|
||||||
import { AutoDisposable } from "@yume-chan/event";
|
import { AutoDisposable } from "@yume-chan/event";
|
||||||
import { AbortController, WritableStream } from '@yume-chan/stream-extra';
|
import { AbortController, WritableStream } from "@yume-chan/stream-extra";
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from "xterm";
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from "xterm-addon-fit";
|
||||||
import { SearchAddon } from 'xterm-addon-search';
|
import { SearchAddon } from "xterm-addon-search";
|
||||||
import { WebglAddon } from 'xterm-addon-webgl';
|
import { WebglAddon } from "xterm-addon-webgl";
|
||||||
|
|
||||||
export class AdbTerminal extends AutoDisposable {
|
export class AdbTerminal extends AutoDisposable {
|
||||||
private element = document.createElement('div');
|
private element = document.createElement("div");
|
||||||
|
|
||||||
public terminal: Terminal = new Terminal({
|
public terminal: Terminal = new Terminal({
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
cursorStyle: 'bar',
|
cursorStyle: "bar",
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontFamily: '"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"',
|
fontFamily:
|
||||||
|
'"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"',
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
scrollback: 9000,
|
scrollback: 9000,
|
||||||
smoothScrollDuration: 50,
|
smoothScrollDuration: 50,
|
||||||
|
@ -29,7 +30,9 @@ export class AdbTerminal extends AutoDisposable {
|
||||||
|
|
||||||
private _socket: AdbSubprocessProtocol | undefined;
|
private _socket: AdbSubprocessProtocol | undefined;
|
||||||
private _socketAbortController: AbortController | undefined;
|
private _socketAbortController: AbortController | undefined;
|
||||||
public get socket() { return this._socket; }
|
public get socket() {
|
||||||
|
return this._socket;
|
||||||
|
}
|
||||||
public set socket(value) {
|
public set socket(value) {
|
||||||
if (this._socket) {
|
if (this._socket) {
|
||||||
// Remove event listeners
|
// Remove event listeners
|
||||||
|
@ -46,19 +49,26 @@ export class AdbTerminal extends AutoDisposable {
|
||||||
this._socketAbortController = new AbortController();
|
this._socketAbortController = new AbortController();
|
||||||
|
|
||||||
// pty mode only has one stream
|
// pty mode only has one stream
|
||||||
value.stdout.pipeTo(new WritableStream<Uint8Array>({
|
value.stdout
|
||||||
write: (chunk) => {
|
.pipeTo(
|
||||||
this.terminal.write(chunk);
|
new WritableStream<Uint8Array>({
|
||||||
},
|
write: (chunk) => {
|
||||||
}), {
|
this.terminal.write(chunk);
|
||||||
signal: this._socketAbortController.signal,
|
},
|
||||||
});
|
}),
|
||||||
|
{
|
||||||
|
signal: this._socketAbortController.signal,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
const _writer = value.stdin.getWriter();
|
const _writer = value.stdin.getWriter();
|
||||||
this.addDisposable(this.terminal.onData(data => {
|
this.addDisposable(
|
||||||
const buffer = encodeUtf8(data);
|
this.terminal.onData((data) => {
|
||||||
_writer.write(buffer);
|
const buffer = encodeUtf8(data);
|
||||||
}));
|
_writer.write(buffer);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.fit();
|
this.fit();
|
||||||
}
|
}
|
||||||
|
@ -67,9 +77,9 @@ export class AdbTerminal extends AutoDisposable {
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.element.style.width = '100%';
|
this.element.style.width = "100%";
|
||||||
this.element.style.height = '100%';
|
this.element.style.height = "100%";
|
||||||
this.element.style.overflow = 'hidden';
|
this.element.style.overflow = "hidden";
|
||||||
|
|
||||||
this.terminal.loadAddon(this.searchAddon);
|
this.terminal.loadAddon(this.searchAddon);
|
||||||
this.terminal.loadAddon(this.fitAddon);
|
this.terminal.loadAddon(this.fitAddon);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
TooltipHost,
|
TooltipHost,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { AdbFeatures } from "@yume-chan/adb";
|
import { AdbFeature } from "@yume-chan/adb";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
@ -14,25 +14,28 @@ import { GLOBAL_STATE } from "../state";
|
||||||
import { Icons, RouteStackProps } from "../utils";
|
import { Icons, RouteStackProps } from "../utils";
|
||||||
|
|
||||||
const KNOWN_FEATURES: Record<string, string> = {
|
const KNOWN_FEATURES: Record<string, string> = {
|
||||||
[AdbFeatures.ShellV2]: `"shell" command now supports separating child process's stdout and stderr, and returning exit code`,
|
[AdbFeature.ShellV2]: `"shell" command now supports separating child process's stdout and stderr, and returning exit code`,
|
||||||
// 'cmd': '',
|
// 'cmd': '',
|
||||||
[AdbFeatures.StatV2]:
|
[AdbFeature.StatV2]:
|
||||||
'"sync" command now supports "STA2" (returns more information of a file than old "STAT") and "LST2" (returns information of a directory) sub command',
|
'"sync" command now supports "STA2" (returns more information of a file than old "STAT") and "LST2" (returns information of a directory) sub command',
|
||||||
[AdbFeatures.ListV2]:
|
[AdbFeature.ListV2]:
|
||||||
'"sync" command now supports "LST2" sub command which returns more information when listing a directory than old "LIST"',
|
'"sync" command now supports "LST2" sub command which returns more information when listing a directory than old "LIST"',
|
||||||
[AdbFeatures.FixedPushMkdir]:
|
[AdbFeature.FixedPushMkdir]:
|
||||||
"Android 9 (P) introduced a bug that pushing files to a non-existing directory would fail. This feature indicates it's fixed (Android 10)",
|
"Android 9 (P) introduced a bug that pushing files to a non-existing directory would fail. This feature indicates it's fixed (Android 10)",
|
||||||
// 'apex': '',
|
// 'apex': '',
|
||||||
// 'abb': '',
|
// 'abb': '',
|
||||||
// 'fixed_push_symlink_timestamp': '',
|
// 'fixed_push_symlink_timestamp': '',
|
||||||
abb_exec:
|
[AdbFeature.AbbExec]:
|
||||||
'Support "exec" command which can stream stdin into child process',
|
'Supports "abb_exec" variant that can be used to install App faster',
|
||||||
// 'remount_shell': '',
|
// 'remount_shell': '',
|
||||||
// 'track_app': '',
|
// 'track_app': '',
|
||||||
// 'sendrecv_v2': '',
|
// 'sendrecv_v2': '',
|
||||||
// 'sendrecv_v2_brotli': '',
|
sendrecv_v2_brotli:
|
||||||
// 'sendrecv_v2_lz4': '',
|
'Supports "brotli" compression algorithm when pushing/pulling files',
|
||||||
// 'sendrecv_v2_zstd': '',
|
sendrecv_v2_lz4:
|
||||||
|
'Supports "lz4" compression algorithm when pushing/pulling files',
|
||||||
|
sendrecv_v2_zstd:
|
||||||
|
'Supports "zstd" compression algorithm when pushing/pulling files',
|
||||||
// 'sendrecv_v2_dry_run_send': '',
|
// 'sendrecv_v2_dry_run_send': '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,7 @@ import {
|
||||||
} from "@fluentui/react-file-type-icons";
|
} from "@fluentui/react-file-type-icons";
|
||||||
import { useConst } from "@fluentui/react-hooks";
|
import { useConst } from "@fluentui/react-hooks";
|
||||||
import { getIcon } from "@fluentui/style-utilities";
|
import { getIcon } from "@fluentui/style-utilities";
|
||||||
import {
|
import { AdbFeature, LinuxFileType, type AdbSyncEntry } from "@yume-chan/adb";
|
||||||
ADB_SYNC_MAX_PACKET_SIZE,
|
|
||||||
AdbFeatures,
|
|
||||||
LinuxFileType,
|
|
||||||
type AdbSyncEntry,
|
|
||||||
} from "@yume-chan/adb";
|
|
||||||
import { ChunkStream } from "@yume-chan/stream-extra";
|
|
||||||
import {
|
import {
|
||||||
action,
|
action,
|
||||||
autorun,
|
autorun,
|
||||||
|
@ -270,6 +264,7 @@ class FileManagerState {
|
||||||
const bSortKey = b[this.sortKey]!;
|
const bSortKey = b[this.sortKey]!;
|
||||||
|
|
||||||
if (aSortKey === bSortKey) {
|
if (aSortKey === bSortKey) {
|
||||||
|
// use name as tie breaker
|
||||||
result = compareCaseInsensitively(a.name!, b.name!);
|
result = compareCaseInsensitively(a.name!, b.name!);
|
||||||
} else if (typeof aSortKey === "string") {
|
} else if (typeof aSortKey === "string") {
|
||||||
result = compareCaseInsensitively(
|
result = compareCaseInsensitively(
|
||||||
|
@ -277,7 +272,8 @@ class FileManagerState {
|
||||||
bSortKey as string
|
bSortKey as string
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = aSortKey < bSortKey ? -1 : 1;
|
result =
|
||||||
|
(aSortKey as number) < (bSortKey as number) ? -1 : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,7 +387,7 @@ class FileManagerState {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (GLOBAL_STATE.device?.features?.includes(AdbFeatures.ListV2)) {
|
if (GLOBAL_STATE.device?.supportsFeature(AdbFeature.ListV2)) {
|
||||||
list.push(
|
list.push(
|
||||||
{
|
{
|
||||||
key: "ctime",
|
key: "ctime",
|
||||||
|
@ -574,22 +570,18 @@ class FileManagerState {
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createFileStream(file)
|
await sync.write(
|
||||||
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
itemPath,
|
||||||
.pipeThrough(
|
createFileStream(file).pipeThrough(
|
||||||
new ProgressStream(
|
new ProgressStream(
|
||||||
action((uploaded) => {
|
action((uploaded) => {
|
||||||
this.uploadedSize = uploaded;
|
this.uploadedSize = uploaded;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
.pipeTo(
|
(LinuxFileType.File << 12) | 0o666,
|
||||||
sync.write(
|
file.lastModified / 1000
|
||||||
itemPath,
|
);
|
||||||
(LinuxFileType.File << 12) | 0o666,
|
|
||||||
file.lastModified / 1000
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.uploadSpeed =
|
this.uploadSpeed =
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import { DefaultButton, ProgressIndicator, Stack } from "@fluentui/react";
|
import {
|
||||||
import { ADB_SYNC_MAX_PACKET_SIZE } from "@yume-chan/adb";
|
Checkbox,
|
||||||
import { ChunkStream } from "@yume-chan/stream-extra";
|
PrimaryButton,
|
||||||
|
ProgressIndicator,
|
||||||
|
Stack,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import {
|
||||||
|
PackageManager,
|
||||||
|
PackageManagerInstallOptions,
|
||||||
|
} from "@yume-chan/android-bin";
|
||||||
|
import { WritableStream } from "@yume-chan/stream-extra";
|
||||||
import { action, makeAutoObservable, observable, runInAction } from "mobx";
|
import { action, 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";
|
||||||
|
@ -38,10 +46,17 @@ class InstallPageState {
|
||||||
|
|
||||||
progress: Progress | undefined = undefined;
|
progress: Progress | undefined = undefined;
|
||||||
|
|
||||||
|
log: string = "";
|
||||||
|
|
||||||
|
options: Partial<PackageManagerInstallOptions> = {
|
||||||
|
bypassLowTargetSdkBlock: false,
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
progress: observable.ref,
|
progress: observable.ref,
|
||||||
install: false,
|
install: false,
|
||||||
|
options: observable.deep,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,11 +75,14 @@ class InstallPageState {
|
||||||
totalSize: file.size,
|
totalSize: file.size,
|
||||||
value: 0,
|
value: 0,
|
||||||
};
|
};
|
||||||
|
this.log = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
await createFileStream(file)
|
const pm = new PackageManager(GLOBAL_STATE.device!);
|
||||||
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
const start = Date.now();
|
||||||
.pipeThrough(
|
const log = await pm.installStream(
|
||||||
|
file.size,
|
||||||
|
createFileStream(file).pipeThrough(
|
||||||
new ProgressStream(
|
new ProgressStream(
|
||||||
action((uploaded) => {
|
action((uploaded) => {
|
||||||
if (uploaded !== file.size) {
|
if (uploaded !== file.size) {
|
||||||
|
@ -87,7 +105,24 @@ class InstallPageState {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.pipeTo(GLOBAL_STATE.device!.install());
|
);
|
||||||
|
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
await log.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write: action((chunk) => {
|
||||||
|
this.log += chunk;
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const transferRate = (
|
||||||
|
file.size /
|
||||||
|
(elapsed / 1000) /
|
||||||
|
1024 /
|
||||||
|
1024
|
||||||
|
).toFixed(2);
|
||||||
|
this.log += `Install finished in ${elapsed}ms at ${transferRate}MB/s`;
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.progress = {
|
this.progress = {
|
||||||
|
@ -112,9 +147,24 @@ const Install: NextPage = () => {
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<DefaultButton
|
<Checkbox
|
||||||
|
label="--bypass-low-target-sdk-block (Android 14)"
|
||||||
|
checked={state.options.bypassLowTargetSdkBlock}
|
||||||
|
onChange={(_, checked) => {
|
||||||
|
if (checked === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runInAction(() => {
|
||||||
|
state.options.bypassLowTargetSdkBlock = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack horizontal>
|
||||||
|
<PrimaryButton
|
||||||
disabled={!GLOBAL_STATE.device || state.installing}
|
disabled={!GLOBAL_STATE.device || state.installing}
|
||||||
text="Open"
|
text="Browse APK"
|
||||||
onClick={state.install}
|
onClick={state.install}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -127,6 +177,8 @@ const Install: NextPage = () => {
|
||||||
description={Stage[state.progress.stage]}
|
description={Stage[state.progress.stage]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{state.log && <pre>{state.log}</pre>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
import { WrapReadableStream, WritableStream, type ReadableStream } from '@yume-chan/stream-extra';
|
import {
|
||||||
|
WrapReadableStream,
|
||||||
|
WritableStream,
|
||||||
|
type ReadableStream,
|
||||||
|
} from "@yume-chan/stream-extra";
|
||||||
import getConfig from "next/config";
|
import getConfig from "next/config";
|
||||||
|
|
||||||
interface PickFileOptions {
|
interface PickFileOptions {
|
||||||
accept?: string;
|
accept?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickFile(options: { multiple: true; } & PickFileOptions): Promise<FileList>;
|
export function pickFile(
|
||||||
export function pickFile(options: { multiple?: false; } & PickFileOptions): Promise<File | null>;
|
options: { multiple: true } & PickFileOptions
|
||||||
export function pickFile(options: { multiple?: boolean; } & PickFileOptions): Promise<FileList | File | null> {
|
): Promise<FileList>;
|
||||||
return new Promise<FileList | File | null>(resolve => {
|
export function pickFile(
|
||||||
const input = document.createElement('input');
|
options: { multiple?: false } & PickFileOptions
|
||||||
input.type = 'file';
|
): Promise<File | null>;
|
||||||
|
export function pickFile(
|
||||||
|
options: { multiple?: boolean } & PickFileOptions
|
||||||
|
): Promise<FileList | File | null> {
|
||||||
|
return new Promise<FileList | File | null>((resolve) => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
|
||||||
if (options.multiple) {
|
if (options.multiple) {
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
|
@ -32,25 +42,26 @@ export function pickFile(options: { multiple?: boolean; } & PickFileOptions): Pr
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let StreamSaver: typeof import('@yume-chan/stream-saver');
|
let StreamSaver: typeof import("@yume-chan/stream-saver");
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
const { publicRuntimeConfig } = getConfig();
|
const { publicRuntimeConfig } = getConfig();
|
||||||
// Can't use `import` here because ESM is read-only (can't set `mitm` field)
|
// Can't use `import` here because ESM is read-only (can't set `mitm` field)
|
||||||
// Add `await` here because top-level await is on, so every import can be a `Promise`
|
// Add `await` here because top-level await is on, so every import can be a `Promise`
|
||||||
StreamSaver = require('@yume-chan/stream-saver');
|
StreamSaver = require("@yume-chan/stream-saver");
|
||||||
StreamSaver.mitm = publicRuntimeConfig.basePath + '/StreamSaver/mitm.html';
|
StreamSaver.mitm = publicRuntimeConfig.basePath + "/StreamSaver/mitm.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveFile(fileName: string, size?: number | undefined) {
|
export function saveFile(fileName: string, size?: number | undefined) {
|
||||||
return StreamSaver!.createWriteStream(
|
return StreamSaver!.createWriteStream(fileName, {
|
||||||
fileName,
|
size,
|
||||||
{ size }
|
}) as unknown as WritableStream<Uint8Array>;
|
||||||
) as unknown as WritableStream<Uint8Array>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFileStream(file: File) {
|
export function createFileStream(file: File) {
|
||||||
// `@types/node` typing messed things up
|
// `@types/node` typing messed things up
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/58079
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/58079
|
||||||
// TODO: demo: remove the wrapper after switching to native stream implementation.
|
// TODO: demo: remove the wrapper after switching to native stream implementation.
|
||||||
return new WrapReadableStream<Uint8Array>(file.stream() as unknown as ReadableStream<Uint8Array>);
|
return new WrapReadableStream<Uint8Array>(
|
||||||
|
file.stream() as unknown as ReadableStream<Uint8Array>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,8 @@ import {
|
||||||
AdbTcpIpCommand,
|
AdbTcpIpCommand,
|
||||||
escapeArg,
|
escapeArg,
|
||||||
framebuffer,
|
framebuffer,
|
||||||
install,
|
|
||||||
} from "./commands/index.js";
|
} from "./commands/index.js";
|
||||||
import { AdbFeatures } from "./features.js";
|
import { AdbFeature } from "./features.js";
|
||||||
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
||||||
import { AdbCommand, calculateChecksum } from "./packet.js";
|
import { AdbCommand, calculateChecksum } from "./packet.js";
|
||||||
import type {
|
import type {
|
||||||
|
@ -119,17 +118,17 @@ export class Adb implements Closeable {
|
||||||
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
||||||
// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
|
// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
|
||||||
const features = [
|
const features = [
|
||||||
AdbFeatures.ShellV2,
|
AdbFeature.ShellV2,
|
||||||
AdbFeatures.Cmd,
|
AdbFeature.Cmd,
|
||||||
AdbFeatures.StatV2,
|
AdbFeature.StatV2,
|
||||||
AdbFeatures.ListV2,
|
AdbFeature.ListV2,
|
||||||
AdbFeatures.FixedPushMkdir,
|
AdbFeature.FixedPushMkdir,
|
||||||
"apex",
|
"apex",
|
||||||
"abb",
|
AdbFeature.Abb,
|
||||||
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
||||||
// No special handling required.
|
// No special handling required.
|
||||||
"fixed_push_symlink_timestamp",
|
"fixed_push_symlink_timestamp",
|
||||||
"abb_exec",
|
AdbFeature.AbbExec,
|
||||||
"remount_shell",
|
"remount_shell",
|
||||||
"track_app",
|
"track_app",
|
||||||
"sendrecv_v2",
|
"sendrecv_v2",
|
||||||
|
@ -188,7 +187,7 @@ export class Adb implements Closeable {
|
||||||
return this._device;
|
return this._device;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _features: AdbFeatures[] = [];
|
private _features: AdbFeature[] = [];
|
||||||
public get features() {
|
public get features() {
|
||||||
return this._features;
|
return this._features;
|
||||||
}
|
}
|
||||||
|
@ -256,14 +255,14 @@ export class Adb implements Closeable {
|
||||||
this._device = value;
|
this._device = value;
|
||||||
break;
|
break;
|
||||||
case AdbPropKey.Features:
|
case AdbPropKey.Features:
|
||||||
this._features = value!.split(",") as AdbFeatures[];
|
this._features = value!.split(",") as AdbFeature[];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public supportsFeature(feature: AdbFeatures): boolean {
|
public supportsFeature(feature: AdbFeature): boolean {
|
||||||
return this._features.includes(feature);
|
return this._features.includes(feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,18 +297,15 @@ export class Adb implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rm(...filenames: string[]): Promise<string> {
|
public async rm(...filenames: string[]): Promise<string> {
|
||||||
|
// https://android.googlesource.com/platform/packages/modules/adb/+/1a0fb8846d4e6b671c8aa7f137a8c21d7b248716/client/adb_install.cpp#984
|
||||||
const stdout = await this.subprocess.spawnAndWaitLegacy([
|
const stdout = await this.subprocess.spawnAndWaitLegacy([
|
||||||
"rm",
|
"rm",
|
||||||
"-rf",
|
|
||||||
...filenames.map((arg) => escapeArg(arg)),
|
...filenames.map((arg) => escapeArg(arg)),
|
||||||
|
"</dev/null",
|
||||||
]);
|
]);
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public install() {
|
|
||||||
return install(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sync(): Promise<AdbSync> {
|
public async sync(): Promise<AdbSync> {
|
||||||
const socket = await this.createSocket("sync:");
|
const socket = await this.createSocket("sync:");
|
||||||
return new AdbSync(this, socket);
|
return new AdbSync(this, socket);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export * from "./base.js";
|
export * from "./base.js";
|
||||||
export * from "./framebuffer.js";
|
export * from "./framebuffer.js";
|
||||||
export * from "./install.js";
|
|
||||||
export * from "./power.js";
|
export * from "./power.js";
|
||||||
export * from "./reverse.js";
|
export * from "./reverse.js";
|
||||||
export * from "./subprocess/index.js";
|
export * from "./subprocess/index.js";
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import type { WritableStream } from "@yume-chan/stream-extra";
|
|
||||||
import { WrapWritableStream } from "@yume-chan/stream-extra";
|
|
||||||
|
|
||||||
import type { Adb } from "../adb.js";
|
|
||||||
|
|
||||||
import { escapeArg } from "./subprocess/index.js";
|
|
||||||
import type { AdbSync } from "./sync/index.js";
|
|
||||||
|
|
||||||
export function install(adb: Adb): WritableStream<Uint8Array> {
|
|
||||||
const filename = `/data/local/tmp/${Math.random()
|
|
||||||
.toString()
|
|
||||||
.substring(2)}.apk`;
|
|
||||||
|
|
||||||
let sync!: AdbSync;
|
|
||||||
return new WrapWritableStream<Uint8Array>({
|
|
||||||
async start() {
|
|
||||||
// TODO: install: support other install apk methods (streaming, etc.)
|
|
||||||
// TODO: install: support split apk formats (`adb install-multiple`)
|
|
||||||
|
|
||||||
// Upload apk file to tmp folder
|
|
||||||
sync = await adb.sync();
|
|
||||||
return sync.write(filename, undefined, undefined);
|
|
||||||
},
|
|
||||||
async close() {
|
|
||||||
await sync.dispose();
|
|
||||||
|
|
||||||
// Invoke `pm install` to install it
|
|
||||||
await adb.subprocess.spawnAndWaitLegacy([
|
|
||||||
"pm",
|
|
||||||
"install",
|
|
||||||
escapeArg(filename),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Remove the temp file
|
|
||||||
await adb.rm(filename);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -67,7 +67,10 @@ export class AdbSubprocess extends AdbCommandBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spawns an executable in PTY (interactive) mode.
|
* Spawns an executable in PTY mode.
|
||||||
|
*
|
||||||
|
* Redirection mode is enough for most simple commands, but PTY mode is required for
|
||||||
|
* commands that manipulate the terminal, such as `vi` and `less`.
|
||||||
* @param command The command to run. If omitted, the default shell will be spawned.
|
* @param command The command to run. If omitted, the default shell will be spawned.
|
||||||
* @param options The options for creating the `AdbSubprocessProtocol`
|
* @param options The options for creating the `AdbSubprocessProtocol`
|
||||||
* @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
|
* @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
|
||||||
|
@ -80,7 +83,10 @@ export class AdbSubprocess extends AdbCommandBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spawns an executable and pipe the output.
|
* Spawns an executable and redirect the standard input/output stream.
|
||||||
|
*
|
||||||
|
* Redirection mode is enough for most simple commands, but PTY mode is required for
|
||||||
|
* commands that manipulate the terminal, such as `vi` and `less`.
|
||||||
* @param command The command to run, or an array of strings containing both command and args.
|
* @param command The command to run, or an array of strings containing both command and args.
|
||||||
* @param options The options for creating the `AdbSubprocessProtocol`
|
* @param options The options for creating the `AdbSubprocessProtocol`
|
||||||
* @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
|
* @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
|
||||||
|
|
|
@ -16,7 +16,7 @@ import type { StructValueType } from "@yume-chan/struct";
|
||||||
import Struct, { placeholder } from "@yume-chan/struct";
|
import Struct, { placeholder } from "@yume-chan/struct";
|
||||||
|
|
||||||
import type { Adb } from "../../../adb.js";
|
import type { Adb } from "../../../adb.js";
|
||||||
import { AdbFeatures } from "../../../features.js";
|
import { AdbFeature } from "../../../features.js";
|
||||||
import type { AdbSocket } from "../../../socket/index.js";
|
import type { AdbSocket } from "../../../socket/index.js";
|
||||||
import { encodeUtf8 } from "../../../utils/index.js";
|
import { encodeUtf8 } from "../../../utils/index.js";
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ class MultiplexStream<T> {
|
||||||
*/
|
*/
|
||||||
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
||||||
public static isSupported(adb: Adb) {
|
public static isSupported(adb: Adb) {
|
||||||
return adb.supportsFeature(AdbFeatures.ShellV2);
|
return adb.supportsFeature(AdbFeature.ShellV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async pty(adb: Adb, command: string) {
|
public static async pty(adb: Adb, command: string) {
|
||||||
|
|
44
libraries/adb/src/commands/subprocess/utils.spec.ts
Normal file
44
libraries/adb/src/commands/subprocess/utils.spec.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { describe, expect, it } from "@jest/globals";
|
||||||
|
|
||||||
|
import { escapeArg } from "./utils.js";
|
||||||
|
|
||||||
|
describe("escapeArg", () => {
|
||||||
|
it("should escape single quotes", () => {
|
||||||
|
// https://android.googlesource.com/platform/packages/modules/adb/+/72c82a8d8ea0648ecea9dd47e0f4176bad792cfa/adb_utils_test.cpp#84
|
||||||
|
expect(String.raw`''`).toBe(escapeArg(""));
|
||||||
|
|
||||||
|
expect(String.raw`'abc'`).toBe(escapeArg("abc"));
|
||||||
|
|
||||||
|
function wrap(x: string) {
|
||||||
|
return "'" + x + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = String.raw`'\''`;
|
||||||
|
expect(wrap(q)).toBe(escapeArg("'"));
|
||||||
|
expect(wrap(q + q)).toBe(escapeArg("''"));
|
||||||
|
expect(wrap(q + "abc" + q)).toBe(escapeArg("'abc'"));
|
||||||
|
expect(wrap(q + "abc")).toBe(escapeArg("'abc"));
|
||||||
|
expect(wrap("abc" + q)).toBe(escapeArg("abc'"));
|
||||||
|
expect(wrap("abc" + q + "def")).toBe(escapeArg("abc'def"));
|
||||||
|
expect(wrap("a" + q + "b" + q + "c")).toBe(escapeArg("a'b'c"));
|
||||||
|
expect(wrap("a" + q + "bcde" + q + "f")).toBe(escapeArg("a'bcde'f"));
|
||||||
|
|
||||||
|
expect(String.raw`' abc'`).toBe(escapeArg(" abc"));
|
||||||
|
expect(String.raw`'"abc'`).toBe(escapeArg('"abc'));
|
||||||
|
expect(String.raw`'\abc'`).toBe(escapeArg("\\abc"));
|
||||||
|
expect(String.raw`'(abc'`).toBe(escapeArg("(abc"));
|
||||||
|
expect(String.raw`')abc'`).toBe(escapeArg(")abc"));
|
||||||
|
|
||||||
|
expect(String.raw`'abc abc'`).toBe(escapeArg("abc abc"));
|
||||||
|
expect(String.raw`'abc"abc'`).toBe(escapeArg('abc"abc'));
|
||||||
|
expect(String.raw`'abc\abc'`).toBe(escapeArg("abc\\abc"));
|
||||||
|
expect(String.raw`'abc(abc'`).toBe(escapeArg("abc(abc"));
|
||||||
|
expect(String.raw`'abc)abc'`).toBe(escapeArg("abc)abc"));
|
||||||
|
|
||||||
|
expect(String.raw`'abc '`).toBe(escapeArg("abc "));
|
||||||
|
expect(String.raw`'abc"'`).toBe(escapeArg('abc"'));
|
||||||
|
expect(String.raw`'abc\'`).toBe(escapeArg("abc\\"));
|
||||||
|
expect(String.raw`'abc('`).toBe(escapeArg("abc("));
|
||||||
|
expect(String.raw`'abc)'`).toBe(escapeArg("abc)"));
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,8 +1,9 @@
|
||||||
import type {
|
import type {
|
||||||
BufferedReadableStream,
|
BufferedReadableStream,
|
||||||
|
ReadableStream,
|
||||||
WritableStreamDefaultWriter,
|
WritableStreamDefaultWriter,
|
||||||
} from "@yume-chan/stream-extra";
|
} from "@yume-chan/stream-extra";
|
||||||
import { ChunkStream, WritableStream, pipeFrom } from "@yume-chan/stream-extra";
|
import { ChunkStream, WritableStream } from "@yume-chan/stream-extra";
|
||||||
import Struct from "@yume-chan/struct";
|
import Struct from "@yume-chan/struct";
|
||||||
|
|
||||||
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
|
import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
|
||||||
|
@ -15,36 +16,26 @@ export const AdbSyncOkResponse = new Struct({ littleEndian: true }).uint32(
|
||||||
|
|
||||||
export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
|
export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
|
||||||
|
|
||||||
export function adbSyncPush(
|
export async function adbSyncPush(
|
||||||
stream: BufferedReadableStream,
|
stream: BufferedReadableStream,
|
||||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||||
filename: string,
|
filename: string,
|
||||||
|
file: ReadableStream<Uint8Array>,
|
||||||
mode: number = (LinuxFileType.File << 12) | 0o666,
|
mode: number = (LinuxFileType.File << 12) | 0o666,
|
||||||
mtime: number = (Date.now() / 1000) | 0,
|
mtime: number = (Date.now() / 1000) | 0,
|
||||||
packetSize: number = ADB_SYNC_MAX_PACKET_SIZE
|
packetSize: number = ADB_SYNC_MAX_PACKET_SIZE
|
||||||
): WritableStream<Uint8Array> {
|
) {
|
||||||
return pipeFrom(
|
const pathAndMode = `${filename},${mode.toString()}`;
|
||||||
new WritableStream<Uint8Array>({
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode);
|
||||||
async start() {
|
|
||||||
const pathAndMode = `${filename},${mode.toString()}`;
|
await file.pipeThrough(new ChunkStream(packetSize)).pipeTo(
|
||||||
await adbSyncWriteRequest(
|
new WritableStream({
|
||||||
writer,
|
write: async (chunk) => {
|
||||||
AdbSyncRequestId.Send,
|
|
||||||
pathAndMode
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async write(chunk) {
|
|
||||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, chunk);
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, chunk);
|
||||||
},
|
},
|
||||||
async close() {
|
})
|
||||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
|
|
||||||
await adbSyncReadResponse(
|
|
||||||
stream,
|
|
||||||
AdbSyncResponseId.Ok,
|
|
||||||
AdbSyncOkResponse
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
new ChunkStream(packetSize)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
|
||||||
|
await adbSyncReadResponse(stream, AdbSyncResponseId.Ok, AdbSyncOkResponse);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import { AutoDisposable } from "@yume-chan/event";
|
import { AutoDisposable } from "@yume-chan/event";
|
||||||
import type {
|
import type {
|
||||||
ReadableStream,
|
ReadableStream,
|
||||||
WritableStream,
|
|
||||||
WritableStreamDefaultWriter,
|
WritableStreamDefaultWriter,
|
||||||
} from "@yume-chan/stream-extra";
|
} from "@yume-chan/stream-extra";
|
||||||
import {
|
import {
|
||||||
BufferedReadableStream,
|
BufferedReadableStream,
|
||||||
WrapReadableStream,
|
WrapReadableStream,
|
||||||
WrapWritableStream,
|
|
||||||
} from "@yume-chan/stream-extra";
|
} from "@yume-chan/stream-extra";
|
||||||
|
|
||||||
import type { Adb } from "../../adb.js";
|
import type { Adb } from "../../adb.js";
|
||||||
import { AdbFeatures } from "../../features.js";
|
import { AdbFeature } from "../../features.js";
|
||||||
import type { AdbSocket } from "../../socket/index.js";
|
import type { AdbSocket } from "../../socket/index.js";
|
||||||
import { AutoResetEvent } from "../../utils/index.js";
|
import { AutoResetEvent } from "../../utils/index.js";
|
||||||
import { escapeArg } from "../subprocess/index.js";
|
import { escapeArg } from "../subprocess/index.js";
|
||||||
|
@ -49,22 +47,21 @@ export class AdbSync extends AutoDisposable {
|
||||||
protected sendLock = this.addDisposable(new AutoResetEvent());
|
protected sendLock = this.addDisposable(new AutoResetEvent());
|
||||||
|
|
||||||
public get supportsStat(): boolean {
|
public get supportsStat(): boolean {
|
||||||
return this.adb.supportsFeature(AdbFeatures.StatV2);
|
return this.adb.supportsFeature(AdbFeature.StatV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get supportsList2(): boolean {
|
public get supportsList2(): boolean {
|
||||||
return this.adb.supportsFeature(AdbFeatures.ListV2);
|
return this.adb.supportsFeature(AdbFeature.ListV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get fixedPushMkdir(): boolean {
|
public get fixedPushMkdir(): boolean {
|
||||||
return this.adb.supportsFeature(AdbFeatures.FixedPushMkdir);
|
return this.adb.supportsFeature(AdbFeature.FixedPushMkdir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get needPushMkdirWorkaround(): boolean {
|
public get needPushMkdirWorkaround(): boolean {
|
||||||
// https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361
|
// https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361
|
||||||
return (
|
return (
|
||||||
this.adb.supportsFeature(AdbFeatures.ShellV2) &&
|
this.adb.supportsFeature(AdbFeature.ShellV2) && !this.fixedPushMkdir
|
||||||
!this.fixedPushMkdir
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,42 +158,42 @@ export class AdbSync extends AutoDisposable {
|
||||||
* Write (or overwrite) a file on device.
|
* Write (or overwrite) a file on device.
|
||||||
*
|
*
|
||||||
* @param filename The full path of the file on device to write.
|
* @param filename The full path of the file on device to write.
|
||||||
|
* @param file The content to write.
|
||||||
* @param mode The unix permissions of the file.
|
* @param mode The unix permissions of the file.
|
||||||
* @param mtime The modified time of the file.
|
* @param mtime The modified time of the file.
|
||||||
* @returns A `WritableStream` that writes to the file.
|
* @returns A `WritableStream` that writes to the file.
|
||||||
*/
|
*/
|
||||||
public write(
|
public async write(
|
||||||
filename: string,
|
filename: string,
|
||||||
|
file: ReadableStream<Uint8Array>,
|
||||||
mode?: number,
|
mode?: number,
|
||||||
mtime?: number
|
mtime?: number
|
||||||
): WritableStream<Uint8Array> {
|
) {
|
||||||
return new WrapWritableStream({
|
await this.sendLock.wait();
|
||||||
start: async () => {
|
|
||||||
await this.sendLock.wait();
|
|
||||||
|
|
||||||
if (this.needPushMkdirWorkaround) {
|
try {
|
||||||
// It may fail if the path is already existed.
|
if (this.needPushMkdirWorkaround) {
|
||||||
// Ignore the result.
|
// It may fail if the path is already existed.
|
||||||
// TODO: sync: test push mkdir workaround (need an Android 8 device)
|
// Ignore the result.
|
||||||
await this.adb.subprocess.spawnAndWait([
|
// TODO: sync: test push mkdir workaround (need an Android 8 device)
|
||||||
"mkdir",
|
await this.adb.subprocess.spawnAndWait([
|
||||||
"-p",
|
"mkdir",
|
||||||
escapeArg(dirname(filename)),
|
"-p",
|
||||||
]);
|
escapeArg(dirname(filename)),
|
||||||
}
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return adbSyncPush(
|
await adbSyncPush(
|
||||||
this.stream,
|
this.stream,
|
||||||
this.writer,
|
this.writer,
|
||||||
filename,
|
filename,
|
||||||
mode,
|
file,
|
||||||
mtime
|
mode,
|
||||||
);
|
mtime
|
||||||
},
|
);
|
||||||
close: () => {
|
} finally {
|
||||||
this.sendLock.notifyOne();
|
this.sendLock.notifyOne();
|
||||||
},
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async dispose() {
|
public override async dispose() {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
// The order follows
|
// The order follows
|
||||||
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
||||||
export enum AdbFeatures {
|
export enum AdbFeature {
|
||||||
ShellV2 = "shell_v2",
|
ShellV2 = "shell_v2",
|
||||||
Cmd = "cmd",
|
Cmd = "cmd",
|
||||||
StatV2 = "stat_v2",
|
StatV2 = "stat_v2",
|
||||||
ListV2 = "ls_v2",
|
ListV2 = "ls_v2",
|
||||||
|
Abb = "abb",
|
||||||
|
AbbExec = "abb_exec",
|
||||||
FixedPushMkdir = "fixed_push_mkdir",
|
FixedPushMkdir = "fixed_push_mkdir",
|
||||||
}
|
}
|
||||||
|
|
67
libraries/android-bin/src/cmd.ts
Normal file
67
libraries/android-bin/src/cmd.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import type { Adb, AdbSubprocessProtocolConstructor } from "@yume-chan/adb";
|
||||||
|
import {
|
||||||
|
AdbCommandBase,
|
||||||
|
AdbFeature,
|
||||||
|
AdbSubprocessNoneProtocol,
|
||||||
|
AdbSubprocessShellProtocol,
|
||||||
|
} from "@yume-chan/adb";
|
||||||
|
|
||||||
|
export class Cmd extends AdbCommandBase {
|
||||||
|
private _supportsShellV2: boolean;
|
||||||
|
private _supportsCmd: boolean;
|
||||||
|
private _supportsAbb: boolean;
|
||||||
|
private _supportsAbbExec: boolean;
|
||||||
|
|
||||||
|
public get supportsShellV2() {
|
||||||
|
return this._supportsShellV2;
|
||||||
|
}
|
||||||
|
public get supportsCmd() {
|
||||||
|
return this._supportsCmd;
|
||||||
|
}
|
||||||
|
public get supportsAbb() {
|
||||||
|
return this._supportsAbb;
|
||||||
|
}
|
||||||
|
public get supportsAbbExec() {
|
||||||
|
return this._supportsAbbExec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(adb: Adb) {
|
||||||
|
super(adb);
|
||||||
|
this._supportsShellV2 = adb.supportsFeature(AdbFeature.ShellV2);
|
||||||
|
this._supportsCmd = adb.supportsFeature(AdbFeature.Cmd);
|
||||||
|
this._supportsAbb = adb.supportsFeature(AdbFeature.Abb);
|
||||||
|
this._supportsAbbExec = adb.supportsFeature(AdbFeature.AbbExec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async spawn(
|
||||||
|
shellProtocol: boolean,
|
||||||
|
command: string,
|
||||||
|
...args: string[]
|
||||||
|
) {
|
||||||
|
let supportAbb: boolean;
|
||||||
|
let supportCmd: boolean = this.supportsCmd;
|
||||||
|
let service: string;
|
||||||
|
let Protocol: AdbSubprocessProtocolConstructor;
|
||||||
|
if (shellProtocol) {
|
||||||
|
supportAbb = this._supportsAbb;
|
||||||
|
supportCmd &&= this.supportsShellV2;
|
||||||
|
service = "abb";
|
||||||
|
Protocol = AdbSubprocessShellProtocol;
|
||||||
|
} else {
|
||||||
|
supportAbb = this._supportsAbbExec;
|
||||||
|
service = "abb_exec";
|
||||||
|
Protocol = AdbSubprocessNoneProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportAbb) {
|
||||||
|
const socket = await this.adb.createSocket(
|
||||||
|
`${service}:${command}\0${args.join("\0")}\0`
|
||||||
|
);
|
||||||
|
return new Protocol(socket);
|
||||||
|
} else if (supportCmd) {
|
||||||
|
return Protocol.raw(this.adb, `cmd ${command} ${args.join(" ")}`);
|
||||||
|
} else {
|
||||||
|
throw new Error("Not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
// cspell: ignore logcat
|
// cspell: ignore logcat
|
||||||
|
|
||||||
export * from "./bug-report.js";
|
export * from "./bug-report.js";
|
||||||
|
export * from "./cmd.js";
|
||||||
export * from "./demo-mode.js";
|
export * from "./demo-mode.js";
|
||||||
export * from "./logcat.js";
|
export * from "./logcat.js";
|
||||||
export * from "./pm.js";
|
export * from "./pm.js";
|
||||||
|
|
|
@ -2,8 +2,30 @@
|
||||||
// cspell:ignore instantapp
|
// cspell:ignore instantapp
|
||||||
// cspell:ignore apks
|
// cspell:ignore apks
|
||||||
|
|
||||||
import { AdbCommandBase } from "@yume-chan/adb";
|
import type { Adb } from "@yume-chan/adb";
|
||||||
import type { WritableStream } from "@yume-chan/stream-extra";
|
import {
|
||||||
|
AdbCommandBase,
|
||||||
|
AdbSubprocessNoneProtocol,
|
||||||
|
escapeArg,
|
||||||
|
} from "@yume-chan/adb";
|
||||||
|
import type { ReadableStream } from "@yume-chan/stream-extra";
|
||||||
|
import { DecodeUtf8Stream, WrapReadableStream } from "@yume-chan/stream-extra";
|
||||||
|
|
||||||
|
import { Cmd } from "./cmd.js";
|
||||||
|
|
||||||
|
export enum PackageManagerInstallLocation {
|
||||||
|
Auto,
|
||||||
|
InternalOnly,
|
||||||
|
PreferExternal,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PackageManagerInstallReason {
|
||||||
|
Unknown,
|
||||||
|
AdminPolicy,
|
||||||
|
DeviceRestore,
|
||||||
|
DeviceSetup,
|
||||||
|
UserRequest,
|
||||||
|
}
|
||||||
|
|
||||||
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3046;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
|
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3046;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
|
||||||
export interface PackageManagerInstallOptions {
|
export interface PackageManagerInstallOptions {
|
||||||
|
@ -22,7 +44,7 @@ export interface PackageManagerInstallOptions {
|
||||||
/**
|
/**
|
||||||
* `-f`
|
* `-f`
|
||||||
*/
|
*/
|
||||||
internal: boolean;
|
internalStorage: boolean;
|
||||||
/**
|
/**
|
||||||
* `-d`
|
* `-d`
|
||||||
*/
|
*/
|
||||||
|
@ -78,11 +100,11 @@ export interface PackageManagerInstallOptions {
|
||||||
/**
|
/**
|
||||||
* `--install-location`
|
* `--install-location`
|
||||||
*/
|
*/
|
||||||
installLocation: number;
|
installLocation: PackageManagerInstallLocation;
|
||||||
/**
|
/**
|
||||||
* `--install-reason`
|
* `--install-reason`
|
||||||
*/
|
*/
|
||||||
installReason: number;
|
installReason: PackageManagerInstallReason;
|
||||||
/**
|
/**
|
||||||
* `--force-uuid`
|
* `--force-uuid`
|
||||||
*/
|
*/
|
||||||
|
@ -121,116 +143,135 @@ export interface PackageManagerInstallOptions {
|
||||||
bypassLowTargetSdkBlock: boolean;
|
bypassLowTargetSdkBlock: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
|
||||||
|
keyof PackageManagerInstallOptions,
|
||||||
|
string
|
||||||
|
> = {
|
||||||
|
skipExisting: "-R",
|
||||||
|
installerPackageName: "-i",
|
||||||
|
allowTest: "-t",
|
||||||
|
internalStorage: "-f",
|
||||||
|
requestDowngrade: "-d",
|
||||||
|
grantRuntimePermissions: "-g",
|
||||||
|
restrictPermissions: "--restrict-permissions",
|
||||||
|
doNotKill: "--dont-kill",
|
||||||
|
originatingUri: "--originating-uri",
|
||||||
|
refererUri: "--referrer",
|
||||||
|
inheritFrom: "-p",
|
||||||
|
packageName: "--pkg",
|
||||||
|
abi: "--abi",
|
||||||
|
instantApp: "--instant",
|
||||||
|
full: "--full",
|
||||||
|
preload: "--preload",
|
||||||
|
userId: "--user",
|
||||||
|
installLocation: "--install-location",
|
||||||
|
installReason: "--install-reason",
|
||||||
|
forceUuid: "--force-uuid",
|
||||||
|
apex: "--apex",
|
||||||
|
forceNonStaged: "--force-non-staged",
|
||||||
|
staged: "--staged",
|
||||||
|
forceQueryable: "--force-queryable",
|
||||||
|
enableRollback: "--enable-rollback",
|
||||||
|
stagedReadyTimeout: "--staged-ready-timeout",
|
||||||
|
skipVerification: "--skip-verification",
|
||||||
|
bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block",
|
||||||
|
};
|
||||||
|
|
||||||
export class PackageManager extends AdbCommandBase {
|
export class PackageManager extends AdbCommandBase {
|
||||||
|
private _cmd: Cmd;
|
||||||
|
|
||||||
|
public constructor(adb: Adb) {
|
||||||
|
super(adb);
|
||||||
|
this._cmd = new Cmd(adb);
|
||||||
|
}
|
||||||
|
|
||||||
private buildInstallArgs(
|
private buildInstallArgs(
|
||||||
options: Partial<PackageManagerInstallOptions>
|
options?: Partial<PackageManagerInstallOptions>
|
||||||
): string[] {
|
): string[] {
|
||||||
const args = ["pm", "install"];
|
const args = ["pm", "install"];
|
||||||
if (options.skipExisting) {
|
if (options) {
|
||||||
args.push("-R");
|
for (const [key, value] of Object.entries(options)) {
|
||||||
}
|
if (value) {
|
||||||
if (options.installerPackageName) {
|
const option =
|
||||||
args.push("-i", options.installerPackageName);
|
PACKAGE_MANAGER_INSTALL_OPTIONS_MAP[
|
||||||
}
|
key as keyof PackageManagerInstallOptions
|
||||||
if (options.allowTest) {
|
];
|
||||||
args.push("-t");
|
if (option) {
|
||||||
}
|
args.push(option);
|
||||||
if (options.internal) {
|
switch (typeof value) {
|
||||||
args.push("-f");
|
case "number":
|
||||||
}
|
args.push(value.toString());
|
||||||
if (options.requestDowngrade) {
|
break;
|
||||||
args.push("-d");
|
case "string":
|
||||||
}
|
args.push(value);
|
||||||
if (options.grantRuntimePermissions) {
|
break;
|
||||||
args.push("-g");
|
}
|
||||||
}
|
}
|
||||||
if (options.restrictPermissions) {
|
}
|
||||||
args.push("--restrict-permissions");
|
}
|
||||||
}
|
|
||||||
if (options.doNotKill) {
|
|
||||||
args.push("--dont-kill");
|
|
||||||
}
|
|
||||||
if (options.originatingUri) {
|
|
||||||
args.push("--originating-uri", options.originatingUri);
|
|
||||||
}
|
|
||||||
if (options.refererUri) {
|
|
||||||
args.push("--referrer", options.refererUri);
|
|
||||||
}
|
|
||||||
if (options.inheritFrom) {
|
|
||||||
args.push("-p", options.inheritFrom);
|
|
||||||
}
|
|
||||||
if (options.packageName) {
|
|
||||||
args.push("--pkg", options.packageName);
|
|
||||||
}
|
|
||||||
if (options.abi) {
|
|
||||||
args.push("--abi", options.abi);
|
|
||||||
}
|
|
||||||
if (options.instantApp) {
|
|
||||||
args.push("--instant");
|
|
||||||
}
|
|
||||||
if (options.full) {
|
|
||||||
args.push("--full");
|
|
||||||
}
|
|
||||||
if (options.preload) {
|
|
||||||
args.push("--preload");
|
|
||||||
}
|
|
||||||
if (options.userId) {
|
|
||||||
args.push("--user", options.userId.toString());
|
|
||||||
}
|
|
||||||
if (options.installLocation) {
|
|
||||||
args.push("--install-location", options.installLocation.toString());
|
|
||||||
}
|
|
||||||
if (options.installReason) {
|
|
||||||
args.push("--install-reason", options.installReason.toString());
|
|
||||||
}
|
|
||||||
if (options.forceUuid) {
|
|
||||||
args.push("--force-uuid", options.forceUuid);
|
|
||||||
}
|
|
||||||
if (options.apex) {
|
|
||||||
args.push("--apex");
|
|
||||||
}
|
|
||||||
if (options.forceNonStaged) {
|
|
||||||
args.push("--force-non-staged");
|
|
||||||
}
|
|
||||||
if (options.staged) {
|
|
||||||
args.push("--staged");
|
|
||||||
}
|
|
||||||
if (options.forceQueryable) {
|
|
||||||
args.push("--force-queryable");
|
|
||||||
}
|
|
||||||
if (options.enableRollback) {
|
|
||||||
args.push("--enable-rollback");
|
|
||||||
}
|
|
||||||
if (options.stagedReadyTimeout) {
|
|
||||||
args.push(
|
|
||||||
"--staged-ready-timeout",
|
|
||||||
options.stagedReadyTimeout.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (options.skipVerification) {
|
|
||||||
args.push("--skip-verification");
|
|
||||||
}
|
|
||||||
if (options.bypassLowTargetSdkBlock) {
|
|
||||||
args.push("--bypass-low-target-sdk-block");
|
|
||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async install(
|
public async install(
|
||||||
options: Partial<PackageManagerInstallOptions>,
|
apks: string[],
|
||||||
...apks: string[]
|
options?: Partial<PackageManagerInstallOptions>
|
||||||
): Promise<void> {
|
): Promise<string> {
|
||||||
const args = this.buildInstallArgs(options);
|
const args = this.buildInstallArgs(options);
|
||||||
|
// WIP: old version of pm doesn't support multiple apks
|
||||||
args.push(...apks);
|
args.push(...apks);
|
||||||
await this.adb.subprocess.spawnAndWaitLegacy(args);
|
return await this.adb.subprocess.spawnAndWaitLegacy(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pushAndInstallStream(
|
||||||
|
stream: ReadableStream<Uint8Array>,
|
||||||
|
options?: Partial<PackageManagerInstallOptions>
|
||||||
|
): Promise<ReadableStream<string>> {
|
||||||
|
const sync = await this.adb.sync();
|
||||||
|
|
||||||
|
const fileName = Math.random().toString().substring(2);
|
||||||
|
const filePath = `/data/local/tmp/${fileName}.apk`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sync.write(filePath, stream);
|
||||||
|
} finally {
|
||||||
|
await sync.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = this.buildInstallArgs(options);
|
||||||
|
const process = await AdbSubprocessNoneProtocol.raw(
|
||||||
|
this.adb,
|
||||||
|
args.map(escapeArg).join(" ")
|
||||||
|
);
|
||||||
|
return new WrapReadableStream({
|
||||||
|
start: () => process.stdout.pipeThrough(new DecodeUtf8Stream()),
|
||||||
|
close: async () => {
|
||||||
|
await this.adb.rm(filePath);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async installStream(
|
public async installStream(
|
||||||
options: Partial<PackageManagerInstallOptions>,
|
size: number,
|
||||||
size: number
|
stream: ReadableStream<Uint8Array>,
|
||||||
): Promise<WritableStream<Uint8Array>> {
|
options?: Partial<PackageManagerInstallOptions>
|
||||||
|
): Promise<ReadableStream<string>> {
|
||||||
|
if (!this._cmd.supportsCmd) {
|
||||||
|
return this.pushAndInstallStream(stream, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 7 added `cmd package` and piping apk to stdin,
|
||||||
|
// the source code suggests using `cmd package` over `pm`, but didn't say why.
|
||||||
|
// However, `cmd package install` can't read `/data/local/tmp` folder due to SELinux policy,
|
||||||
|
// so even ADB today is still using `pm install` for non-streaming installs.
|
||||||
const args = this.buildInstallArgs(options);
|
const args = this.buildInstallArgs(options);
|
||||||
|
// Remove `pm` from args, final command will be `cmd package install`
|
||||||
|
args.shift();
|
||||||
args.push("-S", size.toString());
|
args.push("-S", size.toString());
|
||||||
return (await this.adb.subprocess.spawn(args)).stdin;
|
const process = await this._cmd.spawn(false, "package", ...args);
|
||||||
|
await stream.pipeTo(process.stdin);
|
||||||
|
return process.stdout.pipeThrough(new DecodeUtf8Stream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: install: support split apk formats (`adb install-multiple`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Adb, AdbSubprocessProtocol, AdbSync } from "@yume-chan/adb";
|
import type { Adb, AdbSubprocessProtocol } from "@yume-chan/adb";
|
||||||
import {
|
import {
|
||||||
AdbReverseNotSupportedError,
|
AdbReverseNotSupportedError,
|
||||||
AdbSubprocessNoneProtocol,
|
AdbSubprocessNoneProtocol,
|
||||||
|
@ -14,7 +14,6 @@ import {
|
||||||
InspectStream,
|
InspectStream,
|
||||||
ReadableStream,
|
ReadableStream,
|
||||||
SplitStringStream,
|
SplitStringStream,
|
||||||
WrapWritableStream,
|
|
||||||
WritableStream,
|
WritableStream,
|
||||||
} from "@yume-chan/stream-extra";
|
} from "@yume-chan/stream-extra";
|
||||||
|
|
||||||
|
@ -96,17 +95,17 @@ export class ScrcpyExitedError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdbScrcpyClient {
|
export class AdbScrcpyClient {
|
||||||
public static pushServer(adb: Adb, path = DEFAULT_SERVER_PATH) {
|
public static async pushServer(
|
||||||
let sync!: AdbSync;
|
adb: Adb,
|
||||||
return new WrapWritableStream<Uint8Array>({
|
file: ReadableStream<Uint8Array>,
|
||||||
async start() {
|
path = DEFAULT_SERVER_PATH
|
||||||
sync = await adb.sync();
|
) {
|
||||||
return sync.write(path);
|
const sync = await adb.sync();
|
||||||
},
|
try {
|
||||||
async close() {
|
await sync.write(path, file);
|
||||||
await sync.dispose();
|
} finally {
|
||||||
},
|
await sync.dispose();
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async start(
|
public static async start(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue