Supra_App/src/lib/core/adb/adb.ts

375 lines
9.3 KiB
TypeScript
Raw Normal View History

2026-02-17 14:30:02 +07:00
import { Adb, AdbDaemonTransport, encodeUtf8 } from '@yume-chan/adb';
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
import {
AdbDaemonWebUsbDevice,
2026-02-17 14:30:02 +07:00
AdbDaemonWebUsbDeviceManager,
AdbDaemonWebUsbDeviceObserver
2026-02-17 14:30:02 +07:00
} from '@yume-chan/adb-daemon-webusb';
import { AdbInstance } from '../../../routes/state.svelte';
import { deviceCredentialManager } from './deviceCredManager';
import { Consumable, MaybeConsumable, ReadableStream } from '@yume-chan/stream-extra';
import { AdbScrcpyClient } from '@yume-chan/adb-scrcpy';
import { addNotification } from '../stores/noti';
import { handleAdbPayload } from '../handlers/adbPayloadHandler';
import { adbWriter } from '../stores/adbWriter';
import { WritableStream } from '@yume-chan/stream-extra';
import { env } from '$env/dynamic/public';
import type Dice_2 from '@lucide/svelte/icons/dice-2';
let syncConnection: any = null;
2026-02-17 14:30:02 +07:00
function isRecoverableError(error: any): boolean {
if (!error) return false;
const errorMessage = error.message ? String(error.message).toLowerCase() : '';
const errorName = error.name ? String(error.name).toLowerCase() : '';
// Network-related errors that are typically recoverable
const recoverablePatterns = [
'connection refused',
'connection reset',
'connection timeout',
'network is unreachable',
'host is unreachable',
'temporary failure',
'operation timed out',
'failed to connect',
'connection lost',
'broken pipe',
'socket closed',
'eof',
'end of file',
'disconnected'
];
for (const pattern of recoverablePatterns) {
if (errorMessage.includes(pattern) || errorName.includes(pattern)) {
return true;
}
}
if (
(error.name && error.name.includes('Error')) ||
error.name.includes('Exception') ||
error.name === 'IOError' ||
error.name === 'NetworkError'
) {
return true;
}
return false;
}
async function connectWithRetry<T>(
connectionFn: () => Promise<T>,
description: string,
maxRetries: number = 5,
baseDelayMs: number = 1000
): Promise<T> {
let lastError: any;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Connection timeout for ${description}`)), 10000);
});
const result = await Promise.race([connectionFn(), timeoutPromise]);
return result;
} catch (e) {
lastError = e;
if (attempt === maxRetries - 1) {
break;
}
if (!isRecoverableError(e)) {
break;
}
const delay = Math.min(baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000, 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error(
`failed to ${description} after ${maxRetries} attempts. Last error: ${lastError.message}`
);
}
2026-02-17 14:30:02 +07:00
export async function connnectViaWebUSB() {
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
console.log('usb ok', globalThis.navigator.usb);
if (device) {
console.log('connect ', device.name);
try {
const credentialStore = new AdbWebCredentialStore();
const connection = await device.connect();
const transport = await AdbDaemonTransport.authenticate({
connection: connection,
serial: device.serial,
credentialStore: credentialStore
});
const adb = new Adb(transport);
await saveAdbInstance(adb);
await connectToAndroidServer();
2026-02-17 14:30:02 +07:00
// save device info
await deviceCredentialManager.saveDeviceInfo(device);
} catch (e: any) {
console.error('error on connect', e);
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
addNotification(
'ERR:Device is already in use by another program, please close the program and try again'
);
}
throw e;
2026-02-17 14:30:02 +07:00
}
}
}
export async function connectDeviceByCred(
device: AdbDaemonWebUsbDevice,
credStore: AdbWebCredentialStore
) {
try {
const connection = await device.connect();
const transport = await AdbDaemonTransport.authenticate({
connection: connection,
serial: device.serial,
credentialStore: credStore
});
const adb = new Adb(transport);
await saveAdbInstance(adb);
await connectToAndroidServer();
2026-02-17 14:30:02 +07:00
return true;
} catch (error) {
throw error;
}
}
export async function saveAdbInstance(adb: Adb | undefined) {
await cleanupSync();
2026-02-17 14:30:02 +07:00
AdbInstance.instance = adb;
}
export function getAdbInstance() {
return AdbInstance.instance;
}
export async function executeCmd(command: string) {
let instance = getAdbInstance();
if (!instance) {
console.error('instance not found');
return {};
}
try {
if (instance?.subprocess.shellProtocol?.isSupported) {
const result = await instance.subprocess.shellProtocol.spawnWaitText(command);
return {
output: result.stdout,
error: result.stderr,
exitCode: result.exitCode
};
} else {
const process = await instance.subprocess.noneProtocol.spawn(command);
const reader = process.output.getReader();
const chunks = [];
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(decoder.decode(value, { stream: true }));
}
return {
output: chunks.join('')
};
}
} catch (e: any) {
// console.log(e.message);
2026-02-17 14:30:02 +07:00
//ExactReadable ended
if (e.message.includes('ExactReadable ended')) {
return {
output: '',
exitCode: 1,
2026-02-17 14:30:02 +07:00
error: 'ExactReadableEndedError'
};
}
console.error('error while execute command', e);
return {};
}
}
export async function disconnect() {
let instance = getAdbInstance();
if (instance) {
try {
await instance.close();
console.log('close instance');
} finally {
await saveAdbInstance(undefined);
}
2026-02-17 14:30:02 +07:00
}
}
export async function cleanupSync() {
if (syncConnection) {
try {
await syncConnection.dispose();
} catch (e) {
console.error('error on dispose sync', e);
}
}
syncConnection = null;
}
export async function pull(filename: string, timeoutMs: number = 5000) {
2026-02-17 14:30:02 +07:00
let instance = getAdbInstance();
await cleanupSync();
2026-02-17 14:30:02 +07:00
try {
if (instance) {
let chunkList: Uint8Array<ArrayBufferLike>[] = [];
const syncProm = instance.sync();
const timeoutProm = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('sync timeout')), timeoutMs);
});
syncConnection = await Promise.race([syncProm, timeoutProm]);
const content = syncConnection.read(filename);
let result_string = '';
for await (const chunk of content) {
result_string += new TextDecoder().decode(chunk);
2026-02-17 14:30:02 +07:00
}
return result_string;
}
} catch (pull_error: any) {
console.log('pulling error', pull_error);
} finally {
await cleanupSync();
2026-02-17 14:30:02 +07:00
}
}
export async function push(path: string, obj: string) {
let instance = getAdbInstance();
if (instance) {
let sync = await instance.sync();
const encoder = new TextEncoder();
const file: ReadableStream<MaybeConsumable<Uint8Array>> = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(encoder.encode(obj)));
controller.close();
}
});
try {
console.log('support push v2', sync.supportsSendReceiveV2);
await sync.write({
filename: path,
file
});
} catch (error) {
console.log('error while trying to write to machine', error);
} finally {
await sync.dispose();
}
}
}
// NOTE: adb reverse is not work by unavailable features support
async function connectToAndroidServer(maxRetries = 5) {
let lastError: any;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
let inst = getAdbInstance();
if (!inst) {
console.warn('adb instance not found');
return;
}
// add retry mechanism
const stream = await connectWithRetry(
async () => inst.transport.connect(env.PUBLIC_BREW_CONN_PORT),
`connect to Android server port ${env.PUBLIC_BREW_CONN_PORT}`,
3,
500
);
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
console.log('checking on writer ', writer);
adbWriter.set(writer);
if (writer) {
addNotification('INFO:Enable Brewing Mode T on machine');
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
handleAdbPayload(new TextDecoder().decode(value));
}
} catch (e) {
console.error('read error', e);
if (isRecoverableError(e)) {
throw e;
}
throw e;
} finally {
adbWriter.set(null);
addNotification('WARN:Brewing Mode T Offline ...');
}
} else {
addNotification('WARN:Brewing Mode T unavailable');
if (attempt < maxRetries - 1) {
const delay = Math.min(500 * Math.pow(2, attempt) + Math.random() * 500, 5000);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
} else {
throw new Error('Brewing Mode T unavailable after all retries');
}
}
} catch (err) {
lastError = err;
if (attempt == maxRetries - 1) {
break;
}
if (!isRecoverableError(err)) {
break;
}
const delay = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
if (lastError) {
console.error('Connection failed. Suspect java running or not', lastError);
addNotification(`ERR:Fail to enable brewing mode T\n${lastError.message ?? ''}`);
}
}
2026-02-17 14:30:02 +07:00
// logcat stream
// TODO: screen mirror
export function getScrcpyBinaryFromSource() {
//https://github.com/Genymobile/scrcpy/releases
}