mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
126 lines
4.0 KiB
JavaScript
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)
|
|
})
|