fix: keep Android local API forwarding active

This commit is contained in:
tianma-if committed 2026-08-28 00:38:37 +08:00
1 parent 7f5328e232
commit 2ffca8501b
4 files changed
+156 -3

No files matched your search

+23
View File
@@ -10,8 +10,31 @@ import {
inspectLocalD1,
parseEnvironmentFile,
} from "./local-dev.mjs";
import {
buildAdbReverseArguments,
parseAdbDevices,
} from "./mobile-dev.mjs";
describe("local development environment", () => {
test("targets every online Android device for local API reverse forwarding", () => {
const devices = parseAdbDevices([
"List of devices attached",
"emulator-5554\tdevice product:sdk_gphone64_arm64 model:sdk_gphone64_arm64",
"emulator-5556\toffline",
"R58M123456\tdevice usb:1-1",
"",
].join("\n"));
expect(devices).toEqual(["emulator-5554", "R58M123456"]);
expect(buildAdbReverseArguments(devices[0], 8787)).toEqual([
"-s",
"emulator-5554",
"reverse",
"tcp:8787",
"tcp:8787",
]);
});
test("keeps persistent development and resettable demo profiles isolated", () => {
expect(getLocalDevelopmentProfile("local")).toMatchObject({
statePath: ".wrangler/state",
+129
View File
@@ -0,0 +1,129 @@
import { existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn, spawnSync } from "node:child_process";
export const LOCAL_ANDROID_REVERSE_PORTS = Object.freeze([8787]);
export const ANDROID_DEVICE_POLL_INTERVAL_MS = 1_000;
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const MOBILE_DIR = join(PROJECT_ROOT, "apps/mobile");
export const parseAdbDevices = (source) => source
.split(/\r?\n/)
.slice(1)
.map((line) => line.trim().split(/\s+/))
.filter(([serial, state]) => serial && state === "device")
.map(([serial]) => serial);
export const resolveAdbExecutable = (environment = process.env) => {
for (const sdkRoot of [environment.ANDROID_HOME, environment.ANDROID_SDK_ROOT]) {
if (!sdkRoot) continue;
const candidate = join(sdkRoot, "platform-tools", process.platform === "win32" ? "adb.exe" : "adb");
if (existsSync(candidate)) return candidate;
}
return "adb";
};
export const buildAdbReverseArguments = (serial, port) => [
"-s",
serial,
"reverse",
`tcp:${port}`,
`tcp:${port}`,
];
const startAndroidReverseMonitor = ({
adbExecutable = resolveAdbExecutable(),
ports = LOCAL_ANDROID_REVERSE_PORTS,
pollIntervalMs = ANDROID_DEVICE_POLL_INTERVAL_MS,
} = {}) => {
const configuredDevices = new Set();
let unavailableWarningPrinted = false;
let timer = null;
let stopped = false;
const configureConnectedDevices = () => {
if (stopped) return;
const devicesResult = spawnSync(adbExecutable, ["devices"], {
encoding: "utf8",
timeout: 3_000,
});
if (devicesResult.error || devicesResult.status !== 0) {
if (!unavailableWarningPrinted) {
const detail = devicesResult.error?.code === "ENOENT"
? `cannot find ${adbExecutable}`
: (devicesResult.stderr?.trim() || devicesResult.error?.message || `exit ${devicesResult.status}`);
console.warn(`[mobile-dev] Android API forwarding is waiting: ${detail}.`);
unavailableWarningPrinted = true;
}
return;
}
unavailableWarningPrinted = false;
const connectedDevices = new Set(parseAdbDevices(devicesResult.stdout));
for (const serial of configuredDevices) {
if (!connectedDevices.has(serial)) configuredDevices.delete(serial);
}
for (const serial of connectedDevices) {
if (configuredDevices.has(serial)) continue;
let configured = true;
for (const port of ports) {
const reverseResult = spawnSync(adbExecutable, buildAdbReverseArguments(serial, port), {
encoding: "utf8",
timeout: 3_000,
});
if (reverseResult.error || reverseResult.status !== 0) {
configured = false;
const detail = reverseResult.stderr?.trim() || reverseResult.error?.message || `exit ${reverseResult.status}`;
console.warn(`[mobile-dev] Failed to forward ${serial} tcp:${port}: ${detail}`);
}
}
if (configured) {
configuredDevices.add(serial);
console.log(`[mobile-dev] ${serial}: ${ports.map((port) => `tcp:${port} -> host tcp:${port}`).join(", ")}`);
}
}
};
configureConnectedDevices();
timer = setInterval(configureConnectedDevices, pollIntervalMs);
timer.unref();
return () => {
stopped = true;
if (timer) clearInterval(timer);
};
};
const runMobileDevelopment = () => {
const command = process.argv[2] ?? "start";
if (!new Set(["start", "run:android"]).has(command)) {
console.error("Usage: bun scripts/mobile-dev.mjs [start|run:android]");
process.exit(1);
}
const stopAndroidReverseMonitor = startAndroidReverseMonitor();
const expo = spawn(process.execPath, ["--cwd", MOBILE_DIR, "expo", command], {
cwd: PROJECT_ROOT,
env: process.env,
stdio: "inherit",
});
const forwardSignal = (signal) => {
if (!expo.killed) expo.kill(signal);
};
process.once("SIGINT", forwardSignal);
process.once("SIGTERM", forwardSignal);
expo.once("error", (error) => {
stopAndroidReverseMonitor();
console.error(`[mobile-dev] Cannot start Expo: ${error.message}`);
process.exitCode = 1;
});
expo.once("exit", (code, signal) => {
stopAndroidReverseMonitor();
process.exitCode = code ?? (signal ? 1 : 0);
});
};
if (import.meta.main) runMobileDevelopment();