mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-06 03:50:18 +02:00
feat(adb): support ls_v2 and fixed_push_mkdir features
This commit is contained in:
parent
90b0a82d92
commit
5f1fa1192d
14 changed files with 216 additions and 53 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -16,6 +16,7 @@
|
||||||
"genymobile",
|
"genymobile",
|
||||||
"Genymobile's",
|
"Genymobile's",
|
||||||
"getprop",
|
"getprop",
|
||||||
|
"griffel",
|
||||||
"keyof",
|
"keyof",
|
||||||
"laggy",
|
"laggy",
|
||||||
"localabstract",
|
"localabstract",
|
||||||
|
|
|
@ -4,9 +4,18 @@ import { withDisplayName } from "../utils";
|
||||||
const useClasses = makeStyles({
|
const useClasses = makeStyles({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const HexViewer = withDisplayName('HexViewer')(({ }) => {
|
export interface HexViewer {
|
||||||
|
data: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HexViewer = withDisplayName('HexViewer')(({
|
||||||
|
|
||||||
|
}) => {
|
||||||
const classes = useClasses();
|
const classes = useClasses();
|
||||||
|
|
||||||
|
// Because ADB packets are usually small,
|
||||||
|
// so don't add virtualization now.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,11 @@ import { globalState } from '../state';
|
||||||
import { Icons, RouteStackProps } from "../utils";
|
import { Icons, RouteStackProps } from "../utils";
|
||||||
|
|
||||||
const KNOWN_FEATURES: Record<string, string> = {
|
const KNOWN_FEATURES: Record<string, string> = {
|
||||||
'shell_v2': `"shell" command now supports separating child process's stdout and stderr, and returning exit code`,
|
[AdbFeatures.ShellV2]: `"shell" command now supports separating child process's stdout and stderr, and returning exit code`,
|
||||||
// 'cmd': '',
|
// 'cmd': '',
|
||||||
[AdbFeatures.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.StatV2]: '"sync" command now supports "STA2" (returns more information of a file than old "STAT") and "LST2" (returns information of a directory) sub command',
|
||||||
'ls_v2': '"sync" command now supports "LST2" sub command which returns more information when listing a directory than old "LIST"',
|
[AdbFeatures.ListV2]: '"sync" command now supports "LST2" sub command which returns more information when listing a directory than old "LIST"',
|
||||||
// 'fixed_push_mkdir': '',
|
[AdbFeatures.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': '',
|
// 'apex': '',
|
||||||
// 'abb': '',
|
// 'abb': '',
|
||||||
// 'fixed_push_symlink_timestamp': '',
|
// 'fixed_push_symlink_timestamp': '',
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Breadcrumb, concatStyleSets, ContextualMenu, ContextualMenuItem, Detail
|
||||||
import { FileIconType, getFileTypeIconProps, initializeFileTypeIcons } from "@fluentui/react-file-type-icons";
|
import { FileIconType, getFileTypeIconProps, initializeFileTypeIcons } 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 { AdbSyncEntryResponse, ADB_SYNC_MAX_PACKET_SIZE, ChunkStream, LinuxFileType, ReadableStream } from '@yume-chan/adb';
|
import { AdbFeatures, ADB_SYNC_MAX_PACKET_SIZE, ChunkStream, LinuxFileType, type AdbSyncEntry } from '@yume-chan/adb';
|
||||||
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
|
@ -12,15 +12,15 @@ import path from 'path';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { CommandBar, NoSsr } from '../components';
|
import { CommandBar, NoSsr } from '../components';
|
||||||
import { globalState } from '../state';
|
import { globalState } from '../state';
|
||||||
import { asyncEffect, formatSize, formatSpeed, Icons, pickFile, ProgressStream, RouteStackProps, saveFile } from '../utils';
|
import { asyncEffect, createFileStream, formatSize, formatSpeed, Icons, pickFile, ProgressStream, RouteStackProps, saveFile } from '../utils';
|
||||||
|
|
||||||
initializeFileTypeIcons();
|
initializeFileTypeIcons();
|
||||||
|
|
||||||
interface ListItem extends AdbSyncEntryResponse {
|
interface ListItem extends AdbSyncEntry {
|
||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toListItem(item: AdbSyncEntryResponse): ListItem {
|
function toListItem(item: AdbSyncEntry): ListItem {
|
||||||
return { ...item, key: item.name! };
|
return { ...item, key: item.name! };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ class FileManagerState {
|
||||||
const item = this.selectedItems[0];
|
const item = this.selectedItems[0];
|
||||||
const itemPath = path.resolve(this.path, item.name);
|
const itemPath = path.resolve(this.path, item.name);
|
||||||
await sync.read(itemPath)
|
await sync.read(itemPath)
|
||||||
.pipeTo(saveFile(item.name, item.size));
|
.pipeTo(saveFile(item.name, Number(item.size)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -226,7 +226,7 @@ class FileManagerState {
|
||||||
minWidth: ICON_SIZE,
|
minWidth: ICON_SIZE,
|
||||||
maxWidth: ICON_SIZE,
|
maxWidth: ICON_SIZE,
|
||||||
isCollapsible: true,
|
isCollapsible: true,
|
||||||
onRender(item: AdbSyncEntryResponse) {
|
onRender(item: AdbSyncEntry) {
|
||||||
let iconName: string;
|
let iconName: string;
|
||||||
|
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
|
@ -254,7 +254,7 @@ class FileManagerState {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
isRowHeader: true,
|
isRowHeader: true,
|
||||||
onRender(item: AdbSyncEntryResponse) {
|
onRender(item: AdbSyncEntry) {
|
||||||
return (
|
return (
|
||||||
<span className={classNames.name} data-selection-invoke>
|
<span className={classNames.name} data-selection-invoke>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
@ -267,7 +267,7 @@ class FileManagerState {
|
||||||
name: 'Permission',
|
name: 'Permission',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
isCollapsible: true,
|
isCollapsible: true,
|
||||||
onRender(item: AdbSyncEntryResponse) {
|
onRender(item: AdbSyncEntry) {
|
||||||
return `${(item.mode >> 6 & 0b100).toString(8)}${(item.mode >> 3 & 0b100).toString(8)}${(item.mode & 0b100).toString(8)}`;
|
return `${(item.mode >> 6 & 0b100).toString(8)}${(item.mode >> 3 & 0b100).toString(8)}${(item.mode & 0b100).toString(8)}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -276,9 +276,9 @@ class FileManagerState {
|
||||||
name: 'Size',
|
name: 'Size',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
isCollapsible: true,
|
isCollapsible: true,
|
||||||
onRender(item: AdbSyncEntryResponse) {
|
onRender(item: AdbSyncEntry) {
|
||||||
if (item.type === LinuxFileType.File) {
|
if (item.type === LinuxFileType.File) {
|
||||||
return formatSize(item.size);
|
return formatSize(Number(item.size));
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -288,12 +288,35 @@ class FileManagerState {
|
||||||
name: 'Last Modified Time',
|
name: 'Last Modified Time',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
isCollapsible: true,
|
isCollapsible: true,
|
||||||
onRender(item: AdbSyncEntryResponse) {
|
onRender(item: AdbSyncEntry) {
|
||||||
return new Date(item.mtime * 1000).toLocaleString();
|
return new Date(Number(item.mtime) * 1000).toLocaleString();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (globalState.device?.features?.includes(AdbFeatures.ListV2)) {
|
||||||
|
list.push(
|
||||||
|
{
|
||||||
|
key: 'ctime',
|
||||||
|
name: 'Creation Time',
|
||||||
|
minWidth: 150,
|
||||||
|
isCollapsible: true,
|
||||||
|
onRender(item: AdbSyncEntry) {
|
||||||
|
return new Date(Number(item.ctime!) * 1000).toLocaleString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'atime',
|
||||||
|
name: 'Last Access Time',
|
||||||
|
minWidth: 150,
|
||||||
|
isCollapsible: true,
|
||||||
|
onRender(item: AdbSyncEntry) {
|
||||||
|
return new Date(Number(item.atime!) * 1000).toLocaleString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
item.onColumnClick = (e, column) => {
|
item.onColumnClick = (e, column) => {
|
||||||
if (this.sortKey === column.key) {
|
if (this.sortKey === column.key) {
|
||||||
|
@ -368,7 +391,7 @@ class FileManagerState {
|
||||||
const sync = await globalState.device.sync();
|
const sync = await globalState.device.sync();
|
||||||
|
|
||||||
const items: ListItem[] = [];
|
const items: ListItem[] = [];
|
||||||
const linkItems: AdbSyncEntryResponse[] = [];
|
const linkItems: AdbSyncEntry[] = [];
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
return;
|
return;
|
||||||
|
@ -401,7 +424,7 @@ class FileManagerState {
|
||||||
|
|
||||||
if (!await sync.isDirectory(path.resolve(currentPath, entry.name!))) {
|
if (!await sync.isDirectory(path.resolve(currentPath, entry.name!))) {
|
||||||
entry.mode = (LinuxFileType.File << 12) | entry.permission;
|
entry.mode = (LinuxFileType.File << 12) | entry.permission;
|
||||||
entry.size = 0;
|
entry.size = 0n;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(toListItem(entry));
|
items.push(toListItem(entry));
|
||||||
|
@ -440,7 +463,7 @@ class FileManagerState {
|
||||||
}), 1000);
|
}), 1000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (file.stream() as unknown as ReadableStream<Uint8Array>)
|
await createFileStream(file)
|
||||||
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
||||||
.pipeThrough(new ProgressStream(action((uploaded) => {
|
.pipeThrough(new ProgressStream(action((uploaded) => {
|
||||||
this.uploadedSize = uploaded;
|
this.uploadedSize = uploaded;
|
||||||
|
@ -535,7 +558,7 @@ const FileManager: NextPage = (): JSX.Element | null => {
|
||||||
setPreviewUrl(undefined);
|
setPreviewUrl(undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleItemInvoked = useCallback((item: AdbSyncEntryResponse) => {
|
const handleItemInvoked = useCallback((item: AdbSyncEntry) => {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case LinuxFileType.Link:
|
case LinuxFileType.Link:
|
||||||
case LinuxFileType.Directory:
|
case LinuxFileType.Directory:
|
||||||
|
@ -564,7 +587,7 @@ const FileManager: NextPage = (): JSX.Element | null => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const showContextMenu = useCallback((
|
const showContextMenu = useCallback((
|
||||||
item?: AdbSyncEntryResponse,
|
item?: AdbSyncEntry,
|
||||||
index?: number,
|
index?: number,
|
||||||
e?: Event
|
e?: Event
|
||||||
) => {
|
) => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { globalState } from "../state";
|
import { globalState } from "../state";
|
||||||
import { pickFile, ProgressStream, RouteStackProps } from "../utils";
|
import { createFileStream, pickFile, ProgressStream, RouteStackProps } from "../utils";
|
||||||
|
|
||||||
enum Stage {
|
enum Stage {
|
||||||
Uploading,
|
Uploading,
|
||||||
|
@ -56,7 +56,7 @@ class InstallPageState {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await (file.stream() as unknown as ReadableStream<Uint8Array>)
|
await createFileStream(file)
|
||||||
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
.pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE))
|
||||||
.pipeThrough(new ProgressStream(action((uploaded) => {
|
.pipeThrough(new ProgressStream(action((uploaded) => {
|
||||||
if (uploaded !== file.size) {
|
if (uploaded !== file.size) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import getConfig from "next/config";
|
import getConfig from "next/config";
|
||||||
import type { WritableStream } from '@yume-chan/adb';
|
import { WrapReadableStream, WritableStream, type ReadableStream } from '@yume-chan/adb';
|
||||||
|
|
||||||
interface PickFileOptions {
|
interface PickFileOptions {
|
||||||
accept?: string;
|
accept?: string;
|
||||||
|
@ -47,3 +47,10 @@ export function saveFile(fileName: string, size?: number | undefined) {
|
||||||
{ size }
|
{ size }
|
||||||
) as unknown as WritableStream<Uint8Array>;
|
) 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>);
|
||||||
|
}
|
||||||
|
|
|
@ -78,12 +78,12 @@ export class Adb {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 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 more feature constants, but some of them are only used by ADB server, not devices.
|
// There are some other feature constants, but some of them are only used by ADB server, not devices.
|
||||||
const features = [
|
const features = [
|
||||||
'shell_v2',
|
AdbFeatures.ShellV2,
|
||||||
'cmd',
|
AdbFeatures.Cmd,
|
||||||
AdbFeatures.StatV2,
|
AdbFeatures.StatV2,
|
||||||
'ls_v2',
|
AdbFeatures.ListV2,
|
||||||
'fixed_push_mkdir',
|
'fixed_push_mkdir',
|
||||||
'apex',
|
'apex',
|
||||||
'abb',
|
'abb',
|
||||||
|
|
|
@ -2,7 +2,11 @@ import Struct from '@yume-chan/struct';
|
||||||
import type { AdbBufferedStream, WritableStreamDefaultWriter } from '../../stream/index.js';
|
import type { AdbBufferedStream, WritableStreamDefaultWriter } from '../../stream/index.js';
|
||||||
import { AdbSyncRequestId, adbSyncWriteRequest } from './request.js';
|
import { AdbSyncRequestId, adbSyncWriteRequest } from './request.js';
|
||||||
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response.js';
|
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response.js';
|
||||||
import { AdbSyncLstatResponse } from './stat.js';
|
import { AdbSyncLstatResponse, AdbSyncStatResponse, type AdbSyncStat } from './stat.js';
|
||||||
|
|
||||||
|
export interface AdbSyncEntry extends AdbSyncStat {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const AdbSyncEntryResponse =
|
export const AdbSyncEntryResponse =
|
||||||
new Struct({ littleEndian: true })
|
new Struct({ littleEndian: true })
|
||||||
|
@ -13,22 +17,64 @@ export const AdbSyncEntryResponse =
|
||||||
|
|
||||||
export type AdbSyncEntryResponse = typeof AdbSyncEntryResponse['TDeserializeResult'];
|
export type AdbSyncEntryResponse = typeof AdbSyncEntryResponse['TDeserializeResult'];
|
||||||
|
|
||||||
const ResponseTypes = {
|
export const AdbSyncEntry2Response =
|
||||||
|
new Struct({ littleEndian: true })
|
||||||
|
.fields(AdbSyncStatResponse)
|
||||||
|
.uint32('nameLength')
|
||||||
|
.string('name', { lengthField: 'nameLength' })
|
||||||
|
.extra({ id: AdbSyncResponseId.Entry2 as const });
|
||||||
|
|
||||||
|
export type AdbSyncEntry2Response = typeof AdbSyncEntry2Response['TDeserializeResult'];
|
||||||
|
|
||||||
|
const LIST_V1_RESPONSE_TYPES = {
|
||||||
[AdbSyncResponseId.Entry]: AdbSyncEntryResponse,
|
[AdbSyncResponseId.Entry]: AdbSyncEntryResponse,
|
||||||
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntryResponse.size),
|
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntryResponse.size),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LIST_V2_RESPONSE_TYPES = {
|
||||||
|
[AdbSyncResponseId.Entry2]: AdbSyncEntry2Response,
|
||||||
|
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntry2Response.size),
|
||||||
|
};
|
||||||
|
|
||||||
export async function* adbSyncOpenDir(
|
export async function* adbSyncOpenDir(
|
||||||
stream: AdbBufferedStream,
|
stream: AdbBufferedStream,
|
||||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||||
path: string,
|
path: string,
|
||||||
): AsyncGenerator<AdbSyncEntryResponse, void, void> {
|
v2: boolean,
|
||||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.List, path);
|
): AsyncGenerator<AdbSyncEntry, void, void> {
|
||||||
|
let requestId: AdbSyncRequestId.List | AdbSyncRequestId.List2;
|
||||||
|
let responseType: typeof LIST_V1_RESPONSE_TYPES | typeof LIST_V2_RESPONSE_TYPES;
|
||||||
|
|
||||||
|
if (v2) {
|
||||||
|
requestId = AdbSyncRequestId.List2;
|
||||||
|
responseType = LIST_V2_RESPONSE_TYPES;
|
||||||
|
} else {
|
||||||
|
requestId = AdbSyncRequestId.List;
|
||||||
|
responseType = LIST_V1_RESPONSE_TYPES;
|
||||||
|
}
|
||||||
|
|
||||||
|
await adbSyncWriteRequest(writer, requestId, path);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const response = await adbSyncReadResponse(stream, ResponseTypes);
|
const response = await adbSyncReadResponse(stream, responseType);
|
||||||
switch (response.id) {
|
switch (response.id) {
|
||||||
case AdbSyncResponseId.Entry:
|
case AdbSyncResponseId.Entry:
|
||||||
|
yield {
|
||||||
|
mode: response.mode,
|
||||||
|
size: BigInt(response.size),
|
||||||
|
mtime: BigInt(response.mtime),
|
||||||
|
get type() { return response.type; },
|
||||||
|
get permission() { return response.permission; },
|
||||||
|
name: response.name,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case AdbSyncResponseId.Entry2:
|
||||||
|
// `LST2` can return error codes for failed `lstat` calls.
|
||||||
|
// `LIST` just ignores them.
|
||||||
|
// But they only contain `name` so still pretty useless.
|
||||||
|
if (response.error !== 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
yield response;
|
yield response;
|
||||||
break;
|
break;
|
||||||
case AdbSyncResponseId.Done:
|
case AdbSyncResponseId.Done:
|
||||||
|
|
|
@ -25,9 +25,6 @@ export function adbSyncPush(
|
||||||
return pipeFrom(
|
return pipeFrom(
|
||||||
new WritableStream<Uint8Array>({
|
new WritableStream<Uint8Array>({
|
||||||
async start() {
|
async start() {
|
||||||
// TODO: AdbSyncPush: support create directory using `mkdir`
|
|
||||||
// if device doesn't support `fixed_push_mkdir` feature.
|
|
||||||
|
|
||||||
const pathAndMode = `${filename},${mode.toString()}`;
|
const pathAndMode = `${filename},${mode.toString()}`;
|
||||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode);
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode);
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { encodeUtf8 } from "../../utils/index.js";
|
||||||
|
|
||||||
export enum AdbSyncRequestId {
|
export enum AdbSyncRequestId {
|
||||||
List = 'LIST',
|
List = 'LIST',
|
||||||
|
List2 = 'LIS2',
|
||||||
Send = 'SEND',
|
Send = 'SEND',
|
||||||
Lstat = 'STAT',
|
Lstat = 'STAT',
|
||||||
Stat = 'STA2',
|
Stat = 'STA2',
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { decodeUtf8 } from "../../utils/index.js";
|
||||||
|
|
||||||
export enum AdbSyncResponseId {
|
export enum AdbSyncResponseId {
|
||||||
Entry = 'DENT',
|
Entry = 'DENT',
|
||||||
|
Entry2 = 'DNT2',
|
||||||
Lstat = 'STAT',
|
Lstat = 'STAT',
|
||||||
Stat = 'STA2',
|
Stat = 'STA2',
|
||||||
Lstat2 = 'LST2',
|
Lstat2 = 'LST2',
|
||||||
|
@ -43,7 +44,12 @@ export const AdbSyncFailResponse =
|
||||||
export async function adbSyncReadResponse<T extends Record<string, StructLike<any>>>(
|
export async function adbSyncReadResponse<T extends Record<string, StructLike<any>>>(
|
||||||
stream: AdbBufferedStream,
|
stream: AdbBufferedStream,
|
||||||
types: T,
|
types: T,
|
||||||
): Promise<StructValueType<T[keyof T]>> {
|
// When `T` is a union type, `T[keyof T]` only includes their common keys.
|
||||||
|
// For example, let `type T = { a: string, b: string } | { a: string, c: string}`,
|
||||||
|
// `keyof T` is `'a'`, not `'a' | 'b' | 'c'`.
|
||||||
|
// However, `T extends unknown ? keyof T : never` will distribute `T`,
|
||||||
|
// so returns all keys.
|
||||||
|
): Promise<StructValueType<T extends unknown ? T[keyof T] : never>> {
|
||||||
const id = decodeUtf8(await stream.read(4));
|
const id = decodeUtf8(await stream.read(4));
|
||||||
|
|
||||||
if (id === AdbSyncResponseId.Fail) {
|
if (id === AdbSyncResponseId.Fail) {
|
||||||
|
@ -54,5 +60,5 @@ export async function adbSyncReadResponse<T extends Record<string, StructLike<an
|
||||||
return types[id]!.deserialize(stream);
|
return types[id]!.deserialize(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Unexpected response id');
|
throw new Error(`Expected '${Object.keys(types).join(', ')}', but got '${id}'`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,19 @@ export enum LinuxFileType {
|
||||||
Link = 0o12,
|
Link = 0o12,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdbSyncStat {
|
||||||
|
mode: number;
|
||||||
|
size: bigint;
|
||||||
|
mtime: bigint;
|
||||||
|
get type(): LinuxFileType;
|
||||||
|
get permission(): number;
|
||||||
|
|
||||||
|
uid?: number;
|
||||||
|
gid?: number;
|
||||||
|
atime?: bigint;
|
||||||
|
ctime?: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
export const AdbSyncLstatResponse =
|
export const AdbSyncLstatResponse =
|
||||||
new Struct({ littleEndian: true })
|
new Struct({ littleEndian: true })
|
||||||
.int32('mode')
|
.int32('mode')
|
||||||
|
@ -32,6 +45,7 @@ export const AdbSyncLstatResponse =
|
||||||
export type AdbSyncLstatResponse = typeof AdbSyncLstatResponse['TDeserializeResult'];
|
export type AdbSyncLstatResponse = typeof AdbSyncLstatResponse['TDeserializeResult'];
|
||||||
|
|
||||||
export enum AdbSyncStatErrorCode {
|
export enum AdbSyncStatErrorCode {
|
||||||
|
SUCCESS = 0,
|
||||||
EACCES = 13,
|
EACCES = 13,
|
||||||
EEXIST = 17,
|
EEXIST = 17,
|
||||||
EFAULT = 14,
|
EFAULT = 14,
|
||||||
|
@ -80,15 +94,15 @@ export const AdbSyncStatResponse =
|
||||||
|
|
||||||
export type AdbSyncStatResponse = typeof AdbSyncStatResponse['TDeserializeResult'];
|
export type AdbSyncStatResponse = typeof AdbSyncStatResponse['TDeserializeResult'];
|
||||||
|
|
||||||
const StatResponseType = {
|
const STAT_RESPONSE_TYPES = {
|
||||||
[AdbSyncResponseId.Stat]: AdbSyncStatResponse,
|
[AdbSyncResponseId.Stat]: AdbSyncStatResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LstatResponseType = {
|
const LSTAT_RESPONSE_TYPES = {
|
||||||
[AdbSyncResponseId.Lstat]: AdbSyncLstatResponse,
|
[AdbSyncResponseId.Lstat]: AdbSyncLstatResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Lstat2ResponseType = {
|
const LSTAT_V2_RESPONSE_TYPES = {
|
||||||
[AdbSyncResponseId.Lstat2]: AdbSyncStatResponse,
|
[AdbSyncResponseId.Lstat2]: AdbSyncStatResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -97,20 +111,33 @@ export async function adbSyncLstat(
|
||||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||||
path: string,
|
path: string,
|
||||||
v2: boolean,
|
v2: boolean,
|
||||||
): Promise<AdbSyncLstatResponse | AdbSyncStatResponse> {
|
): Promise<AdbSyncStat> {
|
||||||
let requestId: AdbSyncRequestId.Lstat | AdbSyncRequestId.Lstat2;
|
let requestId: AdbSyncRequestId.Lstat | AdbSyncRequestId.Lstat2;
|
||||||
let responseType: typeof LstatResponseType | typeof Lstat2ResponseType;
|
let responseType: typeof LSTAT_RESPONSE_TYPES | typeof LSTAT_V2_RESPONSE_TYPES;
|
||||||
|
|
||||||
if (v2) {
|
if (v2) {
|
||||||
requestId = AdbSyncRequestId.Lstat2;
|
requestId = AdbSyncRequestId.Lstat2;
|
||||||
responseType = Lstat2ResponseType;
|
responseType = LSTAT_V2_RESPONSE_TYPES;
|
||||||
} else {
|
} else {
|
||||||
requestId = AdbSyncRequestId.Lstat;
|
requestId = AdbSyncRequestId.Lstat;
|
||||||
responseType = LstatResponseType;
|
responseType = LSTAT_RESPONSE_TYPES;
|
||||||
}
|
}
|
||||||
|
|
||||||
await adbSyncWriteRequest(writer, requestId, path);
|
await adbSyncWriteRequest(writer, requestId, path);
|
||||||
return adbSyncReadResponse(stream, responseType);
|
const response = await adbSyncReadResponse(stream, responseType);
|
||||||
|
|
||||||
|
switch (response.id) {
|
||||||
|
case AdbSyncResponseId.Lstat:
|
||||||
|
return {
|
||||||
|
mode: response.mode,
|
||||||
|
size: BigInt(response.size),
|
||||||
|
mtime: BigInt(response.mtime),
|
||||||
|
get type() { return response.type; },
|
||||||
|
get permission() { return response.permission; },
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function adbSyncStat(
|
export async function adbSyncStat(
|
||||||
|
@ -119,5 +146,5 @@ export async function adbSyncStat(
|
||||||
path: string,
|
path: string,
|
||||||
): Promise<AdbSyncStatResponse> {
|
): Promise<AdbSyncStatResponse> {
|
||||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path);
|
await adbSyncWriteRequest(writer, AdbSyncRequestId.Stat, path);
|
||||||
return await adbSyncReadResponse(stream, StatResponseType);
|
return await adbSyncReadResponse(stream, STAT_RESPONSE_TYPES);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,28 @@ import { AdbFeatures } from '../../features.js';
|
||||||
import type { AdbSocket } from '../../socket/index.js';
|
import type { AdbSocket } from '../../socket/index.js';
|
||||||
import { AdbBufferedStream, ReadableStream, WrapReadableStream, WrapWritableStream, WritableStream, WritableStreamDefaultWriter } from '../../stream/index.js';
|
import { AdbBufferedStream, ReadableStream, WrapReadableStream, WrapWritableStream, WritableStream, WritableStreamDefaultWriter } from '../../stream/index.js';
|
||||||
import { AutoResetEvent } from '../../utils/index.js';
|
import { AutoResetEvent } from '../../utils/index.js';
|
||||||
import { AdbSyncEntryResponse, adbSyncOpenDir } from './list.js';
|
import { escapeArg } from "../index.js";
|
||||||
|
import { adbSyncOpenDir, type AdbSyncEntry } from './list.js';
|
||||||
import { adbSyncPull } from './pull.js';
|
import { adbSyncPull } from './pull.js';
|
||||||
import { adbSyncPush } from './push.js';
|
import { adbSyncPush } from './push.js';
|
||||||
import { adbSyncLstat, adbSyncStat } from './stat.js';
|
import { adbSyncLstat, adbSyncStat } from './stat.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simplified `dirname` function that only handles absolute unix paths.
|
||||||
|
* @param path an absolute unix path
|
||||||
|
* @returns the directory name of the input path
|
||||||
|
*/
|
||||||
|
export function dirname(path: string): string {
|
||||||
|
const end = path.lastIndexOf('/');
|
||||||
|
if (end === -1) {
|
||||||
|
throw new Error(`Invalid path`);
|
||||||
|
}
|
||||||
|
if (end === 0) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
return path.substring(0, end);
|
||||||
|
}
|
||||||
|
|
||||||
export class AdbSync extends AutoDisposable {
|
export class AdbSync extends AutoDisposable {
|
||||||
protected adb: Adb;
|
protected adb: Adb;
|
||||||
|
|
||||||
|
@ -22,6 +39,19 @@ export class AdbSync extends AutoDisposable {
|
||||||
return this.adb.features!.includes(AdbFeatures.StatV2);
|
return this.adb.features!.includes(AdbFeatures.StatV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get supportsList2(): boolean {
|
||||||
|
return this.adb.features!.includes(AdbFeatures.ListV2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get fixedPushMkdir(): boolean {
|
||||||
|
return this.adb.features!.includes(AdbFeatures.FixedPushMkdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get needPushMkdirWorkaround(): boolean {
|
||||||
|
// https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361
|
||||||
|
return this.adb.features!.includes(AdbFeatures.ShellV2) && !this.fixedPushMkdir;
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(adb: Adb, socket: AdbSocket) {
|
public constructor(adb: Adb, socket: AdbSocket) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -65,18 +95,18 @@ export class AdbSync extends AutoDisposable {
|
||||||
|
|
||||||
public async *opendir(
|
public async *opendir(
|
||||||
path: string
|
path: string
|
||||||
): AsyncGenerator<AdbSyncEntryResponse, void, void> {
|
): AsyncGenerator<AdbSyncEntry, void, void> {
|
||||||
await this.sendLock.wait();
|
await this.sendLock.wait();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield* adbSyncOpenDir(this.stream, this.writer, path);
|
yield* adbSyncOpenDir(this.stream, this.writer, path, this.supportsList2);
|
||||||
} finally {
|
} finally {
|
||||||
this.sendLock.notify();
|
this.sendLock.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readdir(path: string) {
|
public async readdir(path: string) {
|
||||||
const results: AdbSyncEntryResponse[] = [];
|
const results: AdbSyncEntry[] = [];
|
||||||
for await (const entry of this.opendir(path)) {
|
for await (const entry of this.opendir(path)) {
|
||||||
results.push(entry);
|
results.push(entry);
|
||||||
}
|
}
|
||||||
|
@ -117,6 +147,18 @@ export class AdbSync extends AutoDisposable {
|
||||||
return new WrapWritableStream({
|
return new WrapWritableStream({
|
||||||
start: async () => {
|
start: async () => {
|
||||||
await this.sendLock.wait();
|
await this.sendLock.wait();
|
||||||
|
|
||||||
|
if (this.needPushMkdirWorkaround) {
|
||||||
|
// It may fail if the path is already existed.
|
||||||
|
// Ignore the result.
|
||||||
|
// TODO: test this
|
||||||
|
await this.adb.subprocess.spawnAndWait([
|
||||||
|
'mkdir',
|
||||||
|
'-p',
|
||||||
|
escapeArg(dirname(filename)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return adbSyncPush(
|
return adbSyncPush(
|
||||||
this.stream,
|
this.stream,
|
||||||
this.writer,
|
this.writer,
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
// The order follows
|
||||||
|
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
|
||||||
export enum AdbFeatures {
|
export enum AdbFeatures {
|
||||||
StatV2 = 'stat_v2',
|
ShellV2 = "shell_v2",
|
||||||
Cmd = 'cmd',
|
Cmd = 'cmd',
|
||||||
ShellV2 = "shell_v2"
|
StatV2 = 'stat_v2',
|
||||||
|
ListV2 = 'ls_v2',
|
||||||
|
FixedPushMkdir = 'fixed_push_mkdir',
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue