107 lines
5.3 KiB
JavaScript
107 lines
5.3 KiB
JavaScript
import { createServer } from 'node:http'
|
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
import { createReadStream } from 'node:fs'
|
|
import { existsSync } from 'node:fs'
|
|
import { resolve, extname, join, normalize } from 'node:path'
|
|
import { spawn } from 'node:child_process'
|
|
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
const reportDir = resolve(root, '.cache/freecad/browser-occt')
|
|
await mkdir(reportDir, { recursive: true })
|
|
const contentTypes = { '.html': 'text/html', '.js': 'text/javascript', '.wasm': 'application/wasm', '.json': 'application/json' }
|
|
const reportPromises = new Map()
|
|
const reportResolvers = new Map()
|
|
const waitForReport = (browserId) => {
|
|
if (reportPromises.has(browserId)) return reportPromises.get(browserId)
|
|
const promise = new Promise((resolveReport, reject) => { reportResolvers.set(browserId, { resolve: resolveReport, reject }) })
|
|
reportPromises.set(browserId, promise)
|
|
return promise
|
|
}
|
|
const server = createServer(async (request, response) => {
|
|
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
|
|
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
|
|
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
|
|
if (request.method === 'POST' && request.url?.startsWith('/__occt-report')) {
|
|
let body = ''
|
|
request.setEncoding('utf8')
|
|
request.on('data', (chunk) => { body += chunk })
|
|
request.on('end', async () => {
|
|
try {
|
|
const report = JSON.parse(body)
|
|
const browserId = report.browserId || new URL(request.url, 'http://127.0.0.1').searchParams.get('browser') || 'unknown'
|
|
await writeFile(resolve(reportDir, `${browserId}.json`), `${JSON.stringify(report, null, 2)}\n`)
|
|
reportResolvers.get(browserId)?.resolve(report)
|
|
response.writeHead(204)
|
|
response.end()
|
|
} catch (error) {
|
|
response.writeHead(400)
|
|
response.end(String(error))
|
|
}
|
|
})
|
|
return
|
|
}
|
|
const pathname = decodeURIComponent((request.url || '/').split('?')[0])
|
|
const relative = pathname === '/occt-browser-harness.html' ? 'public/occt-browser-harness.html' : pathname.startsWith('/native/') ? `public${pathname}` : null
|
|
if (!relative) { response.writeHead(404); response.end('not found'); return }
|
|
const file = normalize(join(root, relative))
|
|
if (!file.startsWith(resolve(root, 'public')) || !existsSync(file)) { response.writeHead(404); response.end('not found'); return }
|
|
response.setHeader('content-type', contentTypes[extname(file)] || 'application/octet-stream')
|
|
createReadStream(file).pipe(response)
|
|
})
|
|
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer))
|
|
const port = server.address().port
|
|
|
|
const runBrowser = (browserId, executable, args) => new Promise((resolveRun) => {
|
|
if (!executable || !existsSync(executable)) { resolveRun({ browserId, status: 'unavailable', executable }); return }
|
|
const reportPromise = waitForReport(browserId)
|
|
const child = spawn(executable, [...args, `http://127.0.0.1:${port}/occt-browser-harness.html?browser=${browserId}`], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
let stdout = ''
|
|
child.stdout.on('data', (chunk) => { stdout += String(chunk) })
|
|
let stderr = ''
|
|
child.stderr.on('data', (chunk) => { stderr += String(chunk) })
|
|
let settled = false
|
|
const finish = (result) => {
|
|
if (settled) return
|
|
settled = true
|
|
clearTimeout(timer)
|
|
resolveRun({ ...result, stdout: stdout.slice(-4000), stderr: stderr.slice(-2000) })
|
|
}
|
|
const timer = setTimeout(() => { child.kill('SIGTERM'); finish({ browserId, status: 'timeout' }) }, 240000)
|
|
reportPromise.then((report) => {
|
|
if (settled) return
|
|
child.kill('SIGTERM')
|
|
finish({ browserId, status: report.status || 'pass', report, exitCode: child.exitCode, signal: null })
|
|
}).catch(() => {})
|
|
child.on('close', async (code, signal) => {
|
|
if (!settled) {
|
|
try {
|
|
const report = await Promise.race([reportPromise, new Promise((_, reject) => setTimeout(() => reject(new Error('report timeout')), 5000))])
|
|
finish({ browserId, status: report.status || 'pass', report, exitCode: code, signal })
|
|
} catch {
|
|
finish({ browserId, status: 'failed', exitCode: code, signal })
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
const results = []
|
|
const chromeProfile = await createChromeProfile('occt')
|
|
try {
|
|
results.push(await runBrowser('chrome', process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome', ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--enable-logging=stderr', '--v=1', `--user-data-dir=${chromeProfile}`]))
|
|
} finally {
|
|
await removeChromeProfile(chromeProfile)
|
|
}
|
|
await new Promise((resolveServer) => server.close(resolveServer))
|
|
// Keep committed evidence stable: Chrome's temporary profile paths, timestamps
|
|
// and verbose stderr are diagnostics, not compatibility claims.
|
|
const verification = {
|
|
schemaVersion: 1,
|
|
headers: { coop: 'same-origin', coep: 'require-corp' },
|
|
browsers: results.map(({ stdout, stderr, ...result }) => result),
|
|
status: results.every((result) => result.status === 'pass') ? 'pass' : 'partial',
|
|
}
|
|
await writeFile(resolve(root, 'config/occt-browser-verification.json'), `${JSON.stringify(verification, null, 2)}\n`)
|
|
console.log(JSON.stringify(verification, null, 2))
|
|
if (results.some((result) => result.status !== 'pass')) process.exit(1)
|