Merge branch 'master' into dev

Resolved conflicts:
- adb.ts: keep both goToMachineHome() (dev) and executeStreamingCmd() (master)
- messageHandler.ts / ws_messageSender.ts: adopt master's semver.satisfies
  version check (forward-correct vs startsWith('0.0.2')); keep dev's additive
  bits (raw_stream_end_priceslot handler, ciphertext/iv guard) + master's
  (announce handler, 'upload-log' command); drop now-unused isSecuredAppVersion
- AnnouncementDialog.svelte: drop invalid export on interface (Svelte 5)
- install @types/semver + semver (bun.lock)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
thanawat saiyota 2026-06-30 17:55:37 +07:00
commit 7a406fd409
20 changed files with 2615 additions and 57 deletions

View file

@ -11,6 +11,7 @@ import { Consumable, MaybeConsumable, ReadableStream } from '@yume-chan/stream-e
import { AdbScrcpyClient } from '@yume-chan/adb-scrcpy';
import { addNotification } from '../stores/noti';
import { handleAdbPayload } from '../handlers/adbPayloadHandler';
import { GlobalEventBus } from '../utils/eventBus';
import { adbWriter } from '../stores/adbWriter';
import { WritableStream } from '@yume-chan/stream-extra';
import { env } from '$env/dynamic/public';
@ -371,6 +372,83 @@ export async function goToMachineHome() {
}
}
/**
* Execute an ADB command and stream its output via callbacks.
* Used for commands like `logcat` that run indefinitely.
*
* Returns a cleanup function that can be called to abort the stream.
*/
export async function executeStreamingCmd(
command: string,
callbacks: {
onData?: (chunk: string) => void;
onError?: (error: string) => void;
onExit?: (exitCode: number | undefined) => void;
}
): Promise<() => void> {
const instance = getAdbInstance();
let aborted = false;
if (!instance) {
callbacks.onError?.('No ADB device connected');
return () => {};
}
try {
// NOTE: Always use noneProtocol.spawn() for streaming.
// shellProtocol.spawnWaitText() waits for the process to exit — fine for
// batch commands like 'echo foo', but logcat runs indefinitely, so the
// Promise would never resolve and onData would never fire.
const process = await instance.subprocess.noneProtocol.spawn(command);
const reader = process.output.getReader();
const decoder = new TextDecoder();
// Start reading in the background
(async () => {
try {
while (!aborted) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
if (text && !aborted) {
callbacks.onData?.(text);
}
}
} catch (e: any) {
if (!aborted) {
callbacks.onError?.(e.message ?? 'Stream read error');
}
} finally {
if (!aborted) {
reader.releaseLock();
callbacks.onExit?.(0);
}
}
})();
// Return cleanup function to abort the stream
return () => {
aborted = true;
try {
reader.cancel();
} catch {
// reader may already be done
}
};
} catch (e: any) {
if (!aborted) {
if (e.message?.includes('ExactReadable ended')) {
callbacks.onError?.('Connection closed');
} else {
callbacks.onError?.(e.message ?? 'Unknown error');
}
}
}
return () => {};
}
export async function disconnect() {
let instance = getAdbInstance();
if (instance) {
@ -535,12 +613,32 @@ async function connectToAndroidServer(maxRetries = 5) {
if (writer) {
addNotification('INFO:Enable Brewing Mode T on machine');
const textDecoder = new TextDecoder();
let buffer = '';
(async () => {
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
handleAdbPayload(new TextDecoder().decode(value));
// decode chunk
buffer += textDecoder.decode(value, { stream: true });
let lines = buffer.split('\n');
// save potential incomplete
buffer = lines.pop() ?? '';
for (const line of lines) {
if (line.trim() === '') continue;
GlobalEventBus.emit('adb:raw-payload', line);
handleAdbPayload(line);
}
// GlobalEventBus.emit('adb:raw-payload', new TextDecoder().decode(value));
// handleAdbPayload(new TextDecoder().decode(value));
}
} catch (e) {
console.error('read error', e);
@ -646,6 +744,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
const trimmedMessage = message.trim();
if (trimmedMessage) {
console.log('[ADB Reader] Processing message:', trimmedMessage.slice(0, 200));
GlobalEventBus.emit('adb:raw-payload', trimmedMessage);
handleAdbPayload(trimmedMessage);
}
}
@ -653,6 +752,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
const remainingMessage = messageBuffer.trim();
if (remainingMessage) {
GlobalEventBus.emit('adb:raw-payload', remainingMessage);
handleAdbPayload(remainingMessage);
}
} catch (e) {