167 lines
6.0 KiB
JavaScript
167 lines
6.0 KiB
JavaScript
/**
|
|
* THROWAWAY SPIKE (milestone MV): can headless Chrome reach a local HTTP server
|
|
* in this environment?
|
|
*
|
|
* scripts/browser/README.md documents a sandbox mode where headless Chrome has
|
|
* no network at all, including 127.0.0.1, and the symptom is indistinguishable
|
|
* from a page boot failure. This probe answers the question in isolation before
|
|
* any harness is built on the assumption.
|
|
*
|
|
* Diagnostic per the README: if `location.href === 'about:blank'` while the CDP
|
|
* target list shows the right URL, it is the environment, not the page.
|
|
*
|
|
* Delete after the answer is recorded.
|
|
*/
|
|
import { spawn } from 'node:child_process'
|
|
import { createServer } from 'node:http'
|
|
|
|
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
|
|
const MARKER = 'SPIKE_MARKER_8FA31C'
|
|
|
|
async function main() {
|
|
// 1. Plain node HTTP server, no vite, no build step.
|
|
const server = createServer((req, res) => {
|
|
console.log(`[server] hit: ${req.method} ${req.url}`)
|
|
if (req.url === '/probe.json') {
|
|
res.writeHead(200, { 'content-type': 'application/json' })
|
|
res.end(JSON.stringify({ marker: MARKER }))
|
|
return
|
|
}
|
|
res.writeHead(200, { 'content-type': 'text/html' })
|
|
res.end(`<!doctype html><html><head><title>${MARKER}</title></head>
|
|
<body><div id="m">${MARKER}</div>
|
|
<script>
|
|
window.__spikeFetch = 'pending'
|
|
fetch('/probe.json').then(r => r.json()).then(j => { window.__spikeFetch = 'ok:' + j.marker })
|
|
.catch(e => { window.__spikeFetch = 'ERR:' + e.message })
|
|
</script></body></html>`)
|
|
})
|
|
await new Promise(r => server.listen(0, '127.0.0.1', r))
|
|
const port = server.address().port
|
|
const baseUrl = `http://127.0.0.1:${port}`
|
|
console.log(`[spike] server at ${baseUrl}`)
|
|
|
|
// 2. Sanity: the server is reachable from node itself.
|
|
const nodeResp = await fetch(`${baseUrl}/probe.json`)
|
|
console.log(`[spike] node fetch -> ${nodeResp.status} ${JSON.stringify(await nodeResp.json())}`)
|
|
|
|
// 3. Headless Chrome, same flags the existing harnesses use.
|
|
const debugPort = 9333
|
|
const chrome = spawn('/usr/bin/google-chrome', [
|
|
'--headless=new',
|
|
`--remote-debugging-port=${debugPort}`,
|
|
'--no-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--enable-webgl',
|
|
'--ignore-gpu-blocklist',
|
|
'--use-gl=angle',
|
|
'--use-angle=swiftshader',
|
|
'--window-size=800,600',
|
|
'about:blank',
|
|
])
|
|
chrome.stderr.on('data', d => {
|
|
const s = String(d).trim()
|
|
if (s) console.log(`[chrome stderr] ${s.slice(0, 300)}`)
|
|
})
|
|
|
|
let verdict = 'UNKNOWN'
|
|
try {
|
|
let wsUrl = null
|
|
for (let i = 0; i < 60; i++) {
|
|
await sleep(200)
|
|
try {
|
|
const res = await fetch(`http://127.0.0.1:${debugPort}/json/list`)
|
|
const pages = await res.json()
|
|
const page = pages.find(p => p.type === 'page')
|
|
if (page?.webSocketDebuggerUrl) {
|
|
wsUrl = page.webSocketDebuggerUrl
|
|
break
|
|
}
|
|
} catch {
|
|
// CDP endpoint not up yet; this retry loop is the only tolerated swallow
|
|
// and it is bounded, after which we throw below.
|
|
}
|
|
}
|
|
if (!wsUrl) throw new Error('could not reach Chrome CDP endpoint at all')
|
|
console.log(`[spike] CDP ws: ${wsUrl}`)
|
|
|
|
const ws = new WebSocket(wsUrl)
|
|
await new Promise(r => { ws.onopen = () => r() })
|
|
|
|
let id = 1
|
|
const send = (method, params = {}, timeoutMs = 15000) =>
|
|
new Promise((resolve, reject) => {
|
|
const myId = id++
|
|
const timer = setTimeout(() => {
|
|
ws.removeEventListener('message', handler)
|
|
reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms`))
|
|
}, timeoutMs)
|
|
const handler = ev => {
|
|
const msg = JSON.parse(String(ev.data))
|
|
if (msg.id === myId) {
|
|
clearTimeout(timer)
|
|
ws.removeEventListener('message', handler)
|
|
if (msg.error) reject(new Error(`CDP ${method}: ${JSON.stringify(msg.error)}`))
|
|
else resolve(msg.result)
|
|
}
|
|
}
|
|
ws.addEventListener('message', handler)
|
|
ws.send(JSON.stringify({ id: myId, method, params }))
|
|
})
|
|
|
|
const evalJs = async expression => {
|
|
const res = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true })
|
|
if (res.exceptionDetails) throw new Error(`JS error: ${JSON.stringify(res.exceptionDetails)}`)
|
|
return res.result.value
|
|
}
|
|
|
|
await send('Page.enable')
|
|
await send('Runtime.enable')
|
|
await send('Network.enable')
|
|
|
|
console.log('[spike] Page.navigate ...')
|
|
let navErr = null
|
|
try {
|
|
await send('Page.navigate', { url: baseUrl }, 20000)
|
|
console.log('[spike] Page.navigate returned')
|
|
} catch (err) {
|
|
navErr = err
|
|
console.log(`[spike] Page.navigate FAILED: ${err.message}`)
|
|
}
|
|
|
|
await sleep(2000)
|
|
|
|
const href = await evalJs('location.href')
|
|
const title = await evalJs('document.title')
|
|
const body = await evalJs('document.getElementById("m") ? document.getElementById("m").textContent : "(no #m)"')
|
|
const fetchState = await evalJs('String(window.__spikeFetch)')
|
|
|
|
console.log(`[spike] location.href = ${href}`)
|
|
console.log(`[spike] document.title = ${title}`)
|
|
console.log(`[spike] #m textContent = ${body}`)
|
|
console.log(`[spike] in-page fetch = ${fetchState}`)
|
|
|
|
const navOk = navErr === null && href.startsWith(baseUrl) && body === MARKER
|
|
const fetchOk = fetchState === `ok:${MARKER}`
|
|
verdict = navOk && fetchOk ? 'YES' : 'NO'
|
|
if (!navOk && href === 'about:blank') {
|
|
console.log('[spike] DIAGNOSIS: href is about:blank -> sandbox network block (README §已知限制)')
|
|
}
|
|
} finally {
|
|
chrome.kill('SIGKILL')
|
|
server.close()
|
|
}
|
|
|
|
console.log('======================================================================')
|
|
console.log(`SPIKE VERDICT: headless Chrome can reach a local HTTP server = ${verdict}`)
|
|
console.log('======================================================================')
|
|
process.exit(verdict === 'YES' ? 0 : 1)
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(`[spike] FATAL: ${err.stack}`)
|
|
console.log('SPIKE VERDICT: headless Chrome can reach a local HTTP server = NO (fatal)')
|
|
process.exit(1)
|
|
})
|