Files
orca/mobile/scripts/run-hosted-android-webview-crash-loop.mjs
T

126 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
import process from 'node:process'
import {
readHostedWebViewState,
terminateHostedWebViewProcess,
waitForVisibleHostedWebView
} from './hosted-webview-cdp-session.mjs'
import {
readAndroidRollbackActivation,
runAndroidAdb,
waitForAndroidActivation
} from './hosted-android-mobile-web-cache.mjs'
const packageName = 'com.stably.orca.mobile'
const failureCount = 3
async function forwardInspector(command) {
const pid = await runAndroidAdb(command, ['shell', 'pidof', packageName])
if (!/^\d+$/u.test(pid)) {
throw new Error('Android app process is unavailable')
}
const socket = `webview_devtools_remote_${pid}`
const portValue = await runAndroidAdb(command, ['forward', 'tcp:0', `localabstract:${socket}`])
const port = Number.parseInt(portValue, 10)
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error(`Invalid Android inspector port: ${portValue}`)
}
return { pid, port }
}
async function main() {
const options = parseOptions(process.argv.slice(2))
const initial = await readAndroidRollbackActivation(options.adb)
const inspector = await forwardInspector(options.adb)
const discoveryUrl = `http://127.0.0.1:${inspector.port}`
const documents = []
const startedAt = Date.now()
try {
let document = await waitForVisibleHostedWebView({
discoveryUrl,
expectedText: options.expectedText,
timeoutMs: options.timeoutMs
})
documents.push({ targetId: document.targetId, href: document.href })
for (let index = 0; index < failureCount; index += 1) {
const priorTargetId = document.targetId
await terminateHostedWebViewProcess(document)
document = await waitForVisibleHostedWebView({
discoveryUrl,
expectedText: options.expectedText,
timeoutMs: options.timeoutMs
})
if (document.targetId === priorTargetId) {
throw new Error('Android WebView process did not remount')
}
documents.push({ targetId: document.targetId, href: document.href })
}
const finalActivation = await waitForAndroidActivation(
options.adb,
initial.path,
initial.previous,
options.timeoutMs
)
const finalDocument = await readHostedWebViewState(document)
if (Date.now() - startedAt >= 60_000) {
throw new Error('Android crash-loop drill exceeded the production failure window')
}
if (documents.at(-1)?.href === documents[0]?.href) {
throw new Error('Android rollback did not open a new native package session')
}
console.log(
JSON.stringify(
{
ok: true,
pid: inspector.pid,
failures: failureCount,
durationMs: Date.now() - startedAt,
initial: { active: initial.active, previous: initial.previous },
finalActivation,
documents,
finalText: finalDocument.bodyText.slice(0, 240)
},
null,
2
)
)
} finally {
await runAndroidAdb(options.adb, ['forward', '--remove', `tcp:${inspector.port}`]).catch(
() => {}
)
}
}
function parseOptions(args) {
const options = { adb: 'adb', expectedText: 'mobile-rearch', timeoutMs: 30_000 }
for (let index = 0; index < args.length; index += 1) {
const option = args[index]
if (option === '--adb') {
options.adb = requireValue(args, ++index, option)
} else if (option === '--expected-text') {
options.expectedText = requireValue(args, ++index, option)
} else if (option === '--timeout-ms') {
options.timeoutMs = Number.parseInt(requireValue(args, ++index, option), 10)
if (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1_000) {
throw new Error('--timeout-ms must be an integer of at least 1000')
}
} else {
throw new Error(`Unknown option: ${option}`)
}
}
return options
}
function requireValue(args, index, option) {
if (!args[index]) {
throw new Error(`${option} requires a value`)
}
return args[index]
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
})