Files
genshin-assistant/scripts/live-preflight.cjs
2026-07-09 08:44:50 +02:00

149 lines
5.1 KiB
JavaScript

const fs = require("node:fs");
const path = require("node:path");
function argValue(name, fallback = "") {
const prefix = `--${name}=`;
const match = process.argv.find((entry) => entry.startsWith(prefix));
return match ? match.slice(prefix.length) : fallback;
}
function hasFlag(name) {
return process.argv.includes(`--${name}`);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function parseWaitSeconds(value) {
if (value === "") return 0;
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0) {
throw new Error("--wait must be a non-negative integer number of seconds.");
}
return parsed;
}
function expectedSignature() {
const mainPath = path.join(process.cwd(), "electron", "main.ts");
const source = fs.readFileSync(mainPath, "utf8");
const match = source.match(/APP_RUNTIME_SIGNATURE\s*=\s*"([^"]+)"/);
return match?.[1] || "";
}
async function fetchJson(baseUrl, endpoint) {
let response;
try {
response = await fetch(`${baseUrl}${endpoint}`);
} catch (error) {
throw new Error(`Could not reach ${baseUrl}${endpoint}. Start the elevated app with npm run dev:admin and confirm UAC before running live scans. (${error instanceof Error ? error.message : String(error)})`);
}
const text = await response.text();
let payload;
try {
payload = text ? JSON.parse(text) : null;
} catch {
throw new Error(`${endpoint} returned non-JSON response (${response.status}).`);
}
if (!response.ok) throw new Error(`${endpoint} returned ${response.status}: ${JSON.stringify(payload)}`);
return payload;
}
function validatePreflight({ health, status, expected, requireElevated = true, requireGenshin = true }) {
const errors = [];
const appBuild = health?.appBuild;
const runtime = status?.status?.runtimeInfo;
if (!appBuild?.signature) {
errors.push("/health is missing appBuild.signature.");
} else if (expected && appBuild.signature !== expected) {
errors.push(`Runtime signature '${appBuild.signature}' does not match source '${expected}'.`);
}
if (!status?.status) errors.push("/scanner/status is missing status payload.");
if (!runtime) {
errors.push("/scanner/status is missing runtimeInfo. Open the scanner view and restart the elevated app if needed.");
} else {
if (requireElevated && runtime.isElevated !== true) errors.push("Runtime is not elevated.");
if (requireGenshin && runtime.genshinFound !== true) errors.push("Genshin process/window was not found.");
}
return {
ok: errors.length === 0,
errors,
signature: appBuild?.signature || "",
expectedSignature: expected || "",
isElevated: runtime?.isElevated,
genshinFound: runtime?.genshinFound,
targetProcess: runtime?.targetProcess || "",
foregroundProcess: runtime?.foregroundProcess || "",
};
}
function formatSummary(result) {
const lines = [
`live preflight: ${result.ok ? "PASS" : "FAIL"}`,
`signature: ${result.signature || "missing"}`,
`expected: ${result.expectedSignature || "unknown"}`,
`elevated: ${result.isElevated === true ? "yes" : result.isElevated === false ? "no" : "unknown"}`,
`genshin: ${result.genshinFound === true ? "yes" : result.genshinFound === false ? "no" : "unknown"}`,
`target: ${result.targetProcess || "unknown"}`,
`foreground: ${result.foregroundProcess || "unknown"}`,
];
if (!result.ok) {
lines.push("errors:");
for (const error of result.errors) lines.push(`- ${error}`);
}
return lines.join("\n");
}
async function runPreflight({ baseUrl, expected, requireElevated, requireGenshin }) {
const health = await fetchJson(baseUrl, "/health");
const status = await fetchJson(baseUrl, "/scanner/status");
return validatePreflight({ health, status, expected, requireElevated, requireGenshin });
}
async function waitForPreflight(options, waitSeconds) {
const deadline = Date.now() + waitSeconds * 1000;
let lastError = null;
let lastResult = null;
while (true) {
try {
const result = await runPreflight(options);
lastResult = result;
if (result.ok || Date.now() >= deadline) return result;
} catch (error) {
lastError = error;
if (Date.now() >= deadline) throw lastError;
}
await sleep(1000);
}
}
async function main() {
const baseUrl = argValue("base-url", "http://127.0.0.1:17317").replace(/\/$/, "");
const expected = argValue("expected-signature", expectedSignature());
const requireElevated = !hasFlag("allow-standard");
const requireGenshin = !hasFlag("allow-missing-genshin");
const waitSeconds = parseWaitSeconds(argValue("wait", ""));
const options = { baseUrl, expected, requireElevated, requireGenshin };
const result = waitSeconds > 0 ? await waitForPreflight(options, waitSeconds) : await runPreflight(options);
console.log(hasFlag("json") ? JSON.stringify(result, null, 2) : formatSummary(result));
if (!result.ok) process.exit(1);
}
if (require.main === module) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
}
module.exports = {
formatSummary,
parseWaitSeconds,
runPreflight,
validatePreflight,
};