mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 17:59:50 +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() {
|
||||
const serverBuffer = await fetchServer();
|
||||
|
||||
await new ReadableStream<Uint8Array>({
|
||||
await AdbScrcpyClient.pushServer(
|
||||
GLOBAL_STATE.device!,
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(serverBuffer);
|
||||
controller.close();
|
||||
},
|
||||
}).pipeTo(AdbScrcpyClient.pushServer(GLOBAL_STATE.device!));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
decoder: H264Decoder | undefined = undefined;
|
||||
|
@ -186,12 +188,16 @@ export class ScrcpyPageState {
|
|||
);
|
||||
|
||||
try {
|
||||
await new ReadableStream<Uint8Array>({
|
||||
await AdbScrcpyClient.pushServer(
|
||||
GLOBAL_STATE.device!,
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(serverBuffer);
|
||||
controller.close();
|
||||
},
|
||||
})
|
||||
// In fact `pushServer` will pipe the stream through a ChunkStream,
|
||||
// but without this pipeThrough, the progress will not be updated.
|
||||
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
||||
.pipeThrough(
|
||||
new ProgressStream(
|
||||
|
@ -200,7 +206,7 @@ export class ScrcpyPageState {
|
|||
})
|
||||
)
|
||||
)
|
||||
.pipeTo(AdbScrcpyClient.pushServer(GLOBAL_STATE.device!));
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.serverUploadSpeed =
|
||||
|
|
|
@ -2,21 +2,22 @@
|
|||
|
||||
import { AdbSubprocessProtocol, encodeUtf8 } from "@yume-chan/adb";
|
||||
import { AutoDisposable } from "@yume-chan/event";
|
||||
import { AbortController, WritableStream } from '@yume-chan/stream-extra';
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { SearchAddon } from 'xterm-addon-search';
|
||||
import { WebglAddon } from 'xterm-addon-webgl';
|
||||
import { AbortController, WritableStream } from "@yume-chan/stream-extra";
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import { SearchAddon } from "xterm-addon-search";
|
||||
import { WebglAddon } from "xterm-addon-webgl";
|
||||
|
||||
export class AdbTerminal extends AutoDisposable {
|
||||
private element = document.createElement('div');
|
||||
private element = document.createElement("div");
|
||||
|
||||
public terminal: Terminal = new Terminal({
|
||||
allowProposedApi: true,
|
||||
allowTransparency: true,
|
||||
cursorStyle: 'bar',
|
||||
cursorStyle: "bar",
|
||||
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,
|
||||
scrollback: 9000,
|
||||
smoothScrollDuration: 50,
|
||||
|
@ -29,7 +30,9 @@ export class AdbTerminal extends AutoDisposable {
|
|||
|
||||
private _socket: AdbSubprocessProtocol | undefined;
|
||||
private _socketAbortController: AbortController | undefined;
|
||||
public get socket() { return this._socket; }
|
||||
public get socket() {
|
||||
return this._socket;
|
||||
}
|
||||
public set socket(value) {
|
||||
if (this._socket) {
|
||||
// Remove event listeners
|
||||
|
@ -46,19 +49,26 @@ export class AdbTerminal extends AutoDisposable {
|
|||
this._socketAbortController = new AbortController();
|
||||
|
||||
// pty mode only has one stream
|
||||
value.stdout.pipeTo(new WritableStream<Uint8Array>({
|
||||
value.stdout
|
||||
.pipeTo(
|
||||
new WritableStream<Uint8Array>({
|
||||
write: (chunk) => {
|
||||
this.terminal.write(chunk);
|
||||
},
|
||||
}), {
|
||||
}),
|
||||
{
|
||||
signal: this._socketAbortController.signal,
|
||||
});
|
||||
}
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
const _writer = value.stdin.getWriter();
|
||||
this.addDisposable(this.terminal.onData(data => {
|
||||
this.addDisposable(
|
||||
this.terminal.onData((data) => {
|
||||
const buffer = encodeUtf8(data);
|
||||
_writer.write(buffer);
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
this.fit();
|
||||
}
|
||||
|
@ -67,9 +77,9 @@ export class AdbTerminal extends AutoDisposable {
|
|||
public constructor() {
|
||||
super();
|
||||
|
||||
this.element.style.width = '100%';
|
||||
this.element.style.height = '100%';
|
||||
this.element.style.overflow = 'hidden';
|
||||
this.element.style.width = "100%";
|
||||
this.element.style.height = "100%";
|
||||
this.element.style.overflow = "hidden";
|
||||
|
||||
this.terminal.loadAddon(this.searchAddon);
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
Stack,
|
||||
TooltipHost,
|
||||
} from "@fluentui/react";
|
||||
import { AdbFeatures } from "@yume-chan/adb";
|
||||
import { AdbFeature } from "@yume-chan/adb";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import type { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
|
@ -14,25 +14,28 @@ import { GLOBAL_STATE } from "../state";
|
|||
import { Icons, RouteStackProps } from "../utils";
|
||||
|
||||
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': '',
|
||||
[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',
|
||||
[AdbFeatures.ListV2]:
|
||||
[AdbFeature.ListV2]:
|
||||
'"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)",
|
||||
// 'apex': '',
|
||||
// 'abb': '',
|
||||
// 'fixed_push_symlink_timestamp': '',
|
||||
abb_exec:
|
||||
'Support "exec" command which can stream stdin into child process',
|
||||
[AdbFeature.AbbExec]:
|
||||
'Supports "abb_exec" variant that can be used to install App faster',
|
||||
// 'remount_shell': '',
|
||||
// 'track_app': '',
|
||||
// 'sendrecv_v2': '',
|
||||
// 'sendrecv_v2_brotli': '',
|
||||
// 'sendrecv_v2_lz4': '',
|
||||
// 'sendrecv_v2_zstd': '',
|
||||
sendrecv_v2_brotli:
|
||||
'Supports "brotli" compression algorithm when pushing/pulling files',
|
||||
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': '',
|
||||
};
|
||||
|
||||
|
|
|
@ -29,13 +29,7 @@ import {
|
|||
} from "@fluentui/react-file-type-icons";
|
||||
import { useConst } from "@fluentui/react-hooks";
|
||||
import { getIcon } from "@fluentui/style-utilities";
|
||||
import {
|
||||
ADB_SYNC_MAX_PACKET_SIZE,
|
||||
AdbFeatures,
|
||||
LinuxFileType,
|
||||
type AdbSyncEntry,
|
||||
} from "@yume-chan/adb";
|
||||
import { ChunkStream } from "@yume-chan/stream-extra";
|
||||
import { AdbFeature, LinuxFileType, type AdbSyncEntry } from "@yume-chan/adb";
|
||||
import {
|
||||
action,
|
||||
autorun,
|
||||
|
@ -270,6 +264,7 @@ class FileManagerState {
|
|||
const bSortKey = b[this.sortKey]!;
|
||||
|
||||
if (aSortKey === bSortKey) {
|
||||
// use name as tie breaker
|
||||
result = compareCaseInsensitively(a.name!, b.name!);
|
||||
} else if (typeof aSortKey === "string") {
|
||||
result = compareCaseInsensitively(
|
||||
|
@ -277,7 +272,8 @@ class FileManagerState {
|
|||
bSortKey as string
|
||||
);
|
||||
} 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(
|
||||
{
|
||||
key: "ctime",
|
||||
|
@ -574,21 +570,17 @@ class FileManagerState {
|
|||
);
|
||||
|
||||
try {
|
||||
await createFileStream(file)
|
||||
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
||||
.pipeThrough(
|
||||
await sync.write(
|
||||
itemPath,
|
||||
createFileStream(file).pipeThrough(
|
||||
new ProgressStream(
|
||||
action((uploaded) => {
|
||||
this.uploadedSize = uploaded;
|
||||
})
|
||||
)
|
||||
)
|
||||
.pipeTo(
|
||||
sync.write(
|
||||
itemPath,
|
||||
),
|
||||
(LinuxFileType.File << 12) | 0o666,
|
||||
file.lastModified / 1000
|
||||
)
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import { DefaultButton, ProgressIndicator, Stack } from "@fluentui/react";
|
||||
import { ADB_SYNC_MAX_PACKET_SIZE } from "@yume-chan/adb";
|
||||
import { ChunkStream } from "@yume-chan/stream-extra";
|
||||
import {
|
||||
Checkbox,
|
||||
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 { observer } from "mobx-react-lite";
|
||||
import { NextPage } from "next";
|
||||
|
@ -38,10 +46,17 @@ class InstallPageState {
|
|||
|
||||
progress: Progress | undefined = undefined;
|
||||
|
||||
log: string = "";
|
||||
|
||||
options: Partial<PackageManagerInstallOptions> = {
|
||||
bypassLowTargetSdkBlock: false,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
progress: observable.ref,
|
||||
install: false,
|
||||
options: observable.deep,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -60,11 +75,14 @@ class InstallPageState {
|
|||
totalSize: file.size,
|
||||
value: 0,
|
||||
};
|
||||
this.log = "";
|
||||
});
|
||||
|
||||
await createFileStream(file)
|
||||
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
||||
.pipeThrough(
|
||||
const pm = new PackageManager(GLOBAL_STATE.device!);
|
||||
const start = Date.now();
|
||||
const log = await pm.installStream(
|
||||
file.size,
|
||||
createFileStream(file).pipeThrough(
|
||||
new ProgressStream(
|
||||
action((uploaded) => {
|
||||
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(() => {
|
||||
this.progress = {
|
||||
|
@ -112,9 +147,24 @@ const Install: NextPage = () => {
|
|||
</Head>
|
||||
|
||||
<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}
|
||||
text="Open"
|
||||
text="Browse APK"
|
||||
onClick={state.install}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -127,6 +177,8 @@ const Install: NextPage = () => {
|
|||
description={Stage[state.progress.stage]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state.log && <pre>{state.log}</pre>}
|
||||
</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";
|
||||
|
||||
interface PickFileOptions {
|
||||
accept?: string;
|
||||
}
|
||||
|
||||
export function pickFile(options: { multiple: true; } & PickFileOptions): Promise<FileList>;
|
||||
export function pickFile(options: { multiple?: false; } & PickFileOptions): 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';
|
||||
export function pickFile(
|
||||
options: { multiple: true } & PickFileOptions
|
||||
): Promise<FileList>;
|
||||
export function pickFile(
|
||||
options: { multiple?: false } & PickFileOptions
|
||||
): 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) {
|
||||
input.multiple = true;
|
||||
|
@ -32,25 +42,26 @@ export function pickFile(options: { multiple?: boolean; } & PickFileOptions): Pr
|
|||
});
|
||||
}
|
||||
|
||||
let StreamSaver: typeof import('@yume-chan/stream-saver');
|
||||
if (typeof window !== 'undefined') {
|
||||
let StreamSaver: typeof import("@yume-chan/stream-saver");
|
||||
if (typeof window !== "undefined") {
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
// 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`
|
||||
StreamSaver = require('@yume-chan/stream-saver');
|
||||
StreamSaver.mitm = publicRuntimeConfig.basePath + '/StreamSaver/mitm.html';
|
||||
StreamSaver = require("@yume-chan/stream-saver");
|
||||
StreamSaver.mitm = publicRuntimeConfig.basePath + "/StreamSaver/mitm.html";
|
||||
}
|
||||
|
||||
export function saveFile(fileName: string, size?: number | undefined) {
|
||||
return StreamSaver!.createWriteStream(
|
||||
fileName,
|
||||
{ size }
|
||||
) as unknown as WritableStream<Uint8Array>;
|
||||
return StreamSaver!.createWriteStream(fileName, {
|
||||
size,
|
||||
}) as unknown as WritableStream<Uint8Array>;
|
||||
}
|
||||
|
||||
export function createFileStream(file: File) {
|
||||
// `@types/node` typing messed things up
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/58079
|
||||
// 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,
|
||||
escapeArg,
|
||||
framebuffer,
|
||||
install,
|
||||
} from "./commands/index.js";
|
||||
import { AdbFeatures } from "./features.js";
|
||||
import { AdbFeature } from "./features.js";
|
||||
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
||||
import { AdbCommand, calculateChecksum } from "./packet.js";
|
||||
import type {
|
||||
|
@ -119,17 +118,17 @@ export class Adb implements Closeable {
|
|||
// 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).
|
||||
const features = [
|
||||
AdbFeatures.ShellV2,
|
||||
AdbFeatures.Cmd,
|
||||
AdbFeatures.StatV2,
|
||||
AdbFeatures.ListV2,
|
||||
AdbFeatures.FixedPushMkdir,
|
||||
AdbFeature.ShellV2,
|
||||
AdbFeature.Cmd,
|
||||
AdbFeature.StatV2,
|
||||
AdbFeature.ListV2,
|
||||
AdbFeature.FixedPushMkdir,
|
||||
"apex",
|
||||
"abb",
|
||||
AdbFeature.Abb,
|
||||
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
|
||||
// No special handling required.
|
||||
"fixed_push_symlink_timestamp",
|
||||
"abb_exec",
|
||||
AdbFeature.AbbExec,
|
||||
"remount_shell",
|
||||
"track_app",
|
||||
"sendrecv_v2",
|
||||
|
@ -188,7 +187,7 @@ export class Adb implements Closeable {
|
|||
return this._device;
|
||||
}
|
||||
|
||||
private _features: AdbFeatures[] = [];
|
||||
private _features: AdbFeature[] = [];
|
||||
public get features() {
|
||||
return this._features;
|
||||
}
|
||||
|
@ -256,14 +255,14 @@ export class Adb implements Closeable {
|
|||
this._device = value;
|
||||
break;
|
||||
case AdbPropKey.Features:
|
||||
this._features = value!.split(",") as AdbFeatures[];
|
||||
this._features = value!.split(",") as AdbFeature[];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public supportsFeature(feature: AdbFeatures): boolean {
|
||||
public supportsFeature(feature: AdbFeature): boolean {
|
||||
return this._features.includes(feature);
|
||||
}
|
||||
|
||||
|
@ -298,18 +297,15 @@ export class Adb implements Closeable {
|
|||
}
|
||||
|
||||
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([
|
||||
"rm",
|
||||
"-rf",
|
||||
...filenames.map((arg) => escapeArg(arg)),
|
||||
"</dev/null",
|
||||
]);
|
||||
return stdout;
|
||||
}
|
||||
|
||||
public install() {
|
||||
return install(this);
|
||||
}
|
||||
|
||||
public async sync(): Promise<AdbSync> {
|
||||
const socket = await this.createSocket("sync:");
|
||||
return new AdbSync(this, socket);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export * from "./base.js";
|
||||
export * from "./framebuffer.js";
|
||||
export * from "./install.js";
|
||||
export * from "./power.js";
|
||||
export * from "./reverse.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 options The options for creating the `AdbSubprocessProtocol`
|
||||
* @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 options The options for creating the `AdbSubprocessProtocol`
|
||||
* @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 type { Adb } from "../../../adb.js";
|
||||
import { AdbFeatures } from "../../../features.js";
|
||||
import { AdbFeature } from "../../../features.js";
|
||||
import type { AdbSocket } from "../../../socket/index.js";
|
||||
import { encodeUtf8 } from "../../../utils/index.js";
|
||||
|
||||
|
@ -124,7 +124,7 @@ class MultiplexStream<T> {
|
|||
*/
|
||||
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
|
||||
public static isSupported(adb: Adb) {
|
||||
return adb.supportsFeature(AdbFeatures.ShellV2);
|
||||
return adb.supportsFeature(AdbFeature.ShellV2);
|
||||
}
|
||||
|
||||
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 {
|
||||
BufferedReadableStream,
|
||||
ReadableStream,
|
||||
WritableStreamDefaultWriter,
|
||||
} 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 { 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 function adbSyncPush(
|
||||
export async function adbSyncPush(
|
||||
stream: BufferedReadableStream,
|
||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||
filename: string,
|
||||
file: ReadableStream<Uint8Array>,
|
||||
mode: number = (LinuxFileType.File << 12) | 0o666,
|
||||
mtime: number = (Date.now() / 1000) | 0,
|
||||
packetSize: number = ADB_SYNC_MAX_PACKET_SIZE
|
||||
): WritableStream<Uint8Array> {
|
||||
return pipeFrom(
|
||||
new WritableStream<Uint8Array>({
|
||||
async start() {
|
||||
) {
|
||||
const pathAndMode = `${filename},${mode.toString()}`;
|
||||
await adbSyncWriteRequest(
|
||||
writer,
|
||||
AdbSyncRequestId.Send,
|
||||
pathAndMode
|
||||
);
|
||||
},
|
||||
async write(chunk) {
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode);
|
||||
|
||||
await file.pipeThrough(new ChunkStream(packetSize)).pipeTo(
|
||||
new WritableStream({
|
||||
write: async (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 adbSyncReadResponse(stream, AdbSyncResponseId.Ok, AdbSyncOkResponse);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import { AutoDisposable } from "@yume-chan/event";
|
||||
import type {
|
||||
ReadableStream,
|
||||
WritableStream,
|
||||
WritableStreamDefaultWriter,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import {
|
||||
BufferedReadableStream,
|
||||
WrapReadableStream,
|
||||
WrapWritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
|
||||
import type { Adb } from "../../adb.js";
|
||||
import { AdbFeatures } from "../../features.js";
|
||||
import { AdbFeature } from "../../features.js";
|
||||
import type { AdbSocket } from "../../socket/index.js";
|
||||
import { AutoResetEvent } from "../../utils/index.js";
|
||||
import { escapeArg } from "../subprocess/index.js";
|
||||
|
@ -49,22 +47,21 @@ export class AdbSync extends AutoDisposable {
|
|||
protected sendLock = this.addDisposable(new AutoResetEvent());
|
||||
|
||||
public get supportsStat(): boolean {
|
||||
return this.adb.supportsFeature(AdbFeatures.StatV2);
|
||||
return this.adb.supportsFeature(AdbFeature.StatV2);
|
||||
}
|
||||
|
||||
public get supportsList2(): boolean {
|
||||
return this.adb.supportsFeature(AdbFeatures.ListV2);
|
||||
return this.adb.supportsFeature(AdbFeature.ListV2);
|
||||
}
|
||||
|
||||
public get fixedPushMkdir(): boolean {
|
||||
return this.adb.supportsFeature(AdbFeatures.FixedPushMkdir);
|
||||
return this.adb.supportsFeature(AdbFeature.FixedPushMkdir);
|
||||
}
|
||||
|
||||
public get needPushMkdirWorkaround(): boolean {
|
||||
// https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361
|
||||
return (
|
||||
this.adb.supportsFeature(AdbFeatures.ShellV2) &&
|
||||
!this.fixedPushMkdir
|
||||
this.adb.supportsFeature(AdbFeature.ShellV2) && !this.fixedPushMkdir
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -161,19 +158,20 @@ export class AdbSync extends AutoDisposable {
|
|||
* Write (or overwrite) a file on device.
|
||||
*
|
||||
* @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 mtime The modified time of the file.
|
||||
* @returns A `WritableStream` that writes to the file.
|
||||
*/
|
||||
public write(
|
||||
public async write(
|
||||
filename: string,
|
||||
file: ReadableStream<Uint8Array>,
|
||||
mode?: number,
|
||||
mtime?: number
|
||||
): WritableStream<Uint8Array> {
|
||||
return new WrapWritableStream({
|
||||
start: async () => {
|
||||
) {
|
||||
await this.sendLock.wait();
|
||||
|
||||
try {
|
||||
if (this.needPushMkdirWorkaround) {
|
||||
// It may fail if the path is already existed.
|
||||
// Ignore the result.
|
||||
|
@ -185,18 +183,17 @@ export class AdbSync extends AutoDisposable {
|
|||
]);
|
||||
}
|
||||
|
||||
return adbSyncPush(
|
||||
await adbSyncPush(
|
||||
this.stream,
|
||||
this.writer,
|
||||
filename,
|
||||
file,
|
||||
mode,
|
||||
mtime
|
||||
);
|
||||
},
|
||||
close: () => {
|
||||
} finally {
|
||||
this.sendLock.notifyOne();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override async dispose() {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
// The order follows
|
||||
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
||||
export enum AdbFeatures {
|
||||
export enum AdbFeature {
|
||||
ShellV2 = "shell_v2",
|
||||
Cmd = "cmd",
|
||||
StatV2 = "stat_v2",
|
||||
ListV2 = "ls_v2",
|
||||
Abb = "abb",
|
||||
AbbExec = "abb_exec",
|
||||
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
|
||||
|
||||
export * from "./bug-report.js";
|
||||
export * from "./cmd.js";
|
||||
export * from "./demo-mode.js";
|
||||
export * from "./logcat.js";
|
||||
export * from "./pm.js";
|
||||
|
|
|
@ -2,8 +2,30 @@
|
|||
// cspell:ignore instantapp
|
||||
// cspell:ignore apks
|
||||
|
||||
import { AdbCommandBase } from "@yume-chan/adb";
|
||||
import type { WritableStream } from "@yume-chan/stream-extra";
|
||||
import type { Adb } from "@yume-chan/adb";
|
||||
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
|
||||
export interface PackageManagerInstallOptions {
|
||||
|
@ -22,7 +44,7 @@ export interface PackageManagerInstallOptions {
|
|||
/**
|
||||
* `-f`
|
||||
*/
|
||||
internal: boolean;
|
||||
internalStorage: boolean;
|
||||
/**
|
||||
* `-d`
|
||||
*/
|
||||
|
@ -78,11 +100,11 @@ export interface PackageManagerInstallOptions {
|
|||
/**
|
||||
* `--install-location`
|
||||
*/
|
||||
installLocation: number;
|
||||
installLocation: PackageManagerInstallLocation;
|
||||
/**
|
||||
* `--install-reason`
|
||||
*/
|
||||
installReason: number;
|
||||
installReason: PackageManagerInstallReason;
|
||||
/**
|
||||
* `--force-uuid`
|
||||
*/
|
||||
|
@ -121,116 +143,135 @@ export interface PackageManagerInstallOptions {
|
|||
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 {
|
||||
private _cmd: Cmd;
|
||||
|
||||
public constructor(adb: Adb) {
|
||||
super(adb);
|
||||
this._cmd = new Cmd(adb);
|
||||
}
|
||||
|
||||
private buildInstallArgs(
|
||||
options: Partial<PackageManagerInstallOptions>
|
||||
options?: Partial<PackageManagerInstallOptions>
|
||||
): string[] {
|
||||
const args = ["pm", "install"];
|
||||
if (options.skipExisting) {
|
||||
args.push("-R");
|
||||
if (options) {
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (value) {
|
||||
const option =
|
||||
PACKAGE_MANAGER_INSTALL_OPTIONS_MAP[
|
||||
key as keyof PackageManagerInstallOptions
|
||||
];
|
||||
if (option) {
|
||||
args.push(option);
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
args.push(value.toString());
|
||||
break;
|
||||
case "string":
|
||||
args.push(value);
|
||||
break;
|
||||
}
|
||||
if (options.installerPackageName) {
|
||||
args.push("-i", options.installerPackageName);
|
||||
}
|
||||
if (options.allowTest) {
|
||||
args.push("-t");
|
||||
}
|
||||
if (options.internal) {
|
||||
args.push("-f");
|
||||
}
|
||||
if (options.requestDowngrade) {
|
||||
args.push("-d");
|
||||
}
|
||||
if (options.grantRuntimePermissions) {
|
||||
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;
|
||||
}
|
||||
|
||||
public async install(
|
||||
options: Partial<PackageManagerInstallOptions>,
|
||||
...apks: string[]
|
||||
): Promise<void> {
|
||||
apks: string[],
|
||||
options?: Partial<PackageManagerInstallOptions>
|
||||
): Promise<string> {
|
||||
const args = this.buildInstallArgs(options);
|
||||
// WIP: old version of pm doesn't support multiple 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(
|
||||
options: Partial<PackageManagerInstallOptions>,
|
||||
size: number
|
||||
): Promise<WritableStream<Uint8Array>> {
|
||||
const args = this.buildInstallArgs(options);
|
||||
args.push("-S", size.toString());
|
||||
return (await this.adb.subprocess.spawn(args)).stdin;
|
||||
size: number,
|
||||
stream: ReadableStream<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);
|
||||
// Remove `pm` from args, final command will be `cmd package install`
|
||||
args.shift();
|
||||
args.push("-S", size.toString());
|
||||
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 {
|
||||
AdbReverseNotSupportedError,
|
||||
AdbSubprocessNoneProtocol,
|
||||
|
@ -14,7 +14,6 @@ import {
|
|||
InspectStream,
|
||||
ReadableStream,
|
||||
SplitStringStream,
|
||||
WrapWritableStream,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
|
||||
|
@ -96,17 +95,17 @@ export class ScrcpyExitedError extends Error {
|
|||
}
|
||||
|
||||
export class AdbScrcpyClient {
|
||||
public static pushServer(adb: Adb, path = DEFAULT_SERVER_PATH) {
|
||||
let sync!: AdbSync;
|
||||
return new WrapWritableStream<Uint8Array>({
|
||||
async start() {
|
||||
sync = await adb.sync();
|
||||
return sync.write(path);
|
||||
},
|
||||
async close() {
|
||||
public static async pushServer(
|
||||
adb: Adb,
|
||||
file: ReadableStream<Uint8Array>,
|
||||
path = DEFAULT_SERVER_PATH
|
||||
) {
|
||||
const sync = await adb.sync();
|
||||
try {
|
||||
await sync.write(path, file);
|
||||
} finally {
|
||||
await sync.dispose();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static async start(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue