8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
91 lines
3.6 KiB
Python
Executable File
91 lines
3.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Render a WillTools web page headless and screenshot every tab -- the
|
|
browser dry-run this machine could not do until 2026-09-07 (owner: "浏览器你
|
|
能不能在debian上面自己渲染一个?").
|
|
|
|
python tools/webshot.py pa --reads _scratch/pa_reads_x.json --out /tmp/shots
|
|
python tools/webshot.py ba --reads beamcalc/fixtures/rba5_16x22.json --out /tmp/shots
|
|
|
|
Starts tools/pa_web.py or tools/ba_web.py on a free port, drives Chrome
|
|
headless (google-chrome / chromium on PATH) to /?tab=N&expand=all for each
|
|
tab, writes <out>/<tool>-tab<N>.png plus <out>/<tool>-console.txt (the page's
|
|
console errors, which is the half a screenshot cannot show), then closes the
|
|
server through POST /api/close. Exit 1 if any tab logged a console error.
|
|
CPython 3 only; no Revit anywhere.
|
|
"""
|
|
import argparse, json, os, shutil, subprocess, sys, time, urllib.request
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def chrome():
|
|
for c in ('google-chrome', 'chromium', 'chromium-browser', 'chrome'):
|
|
p = shutil.which(c)
|
|
if p:
|
|
return p
|
|
sys.exit('no chrome/chromium on PATH')
|
|
|
|
|
|
def start_server(tool, reads):
|
|
args = [sys.executable, os.path.join(ROOT, 'tools', '%s_web.py' % tool), '--port', '0']
|
|
if reads:
|
|
args += ['--reads', reads]
|
|
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=ROOT)
|
|
url = None
|
|
for _ in range(200):
|
|
line = p.stdout.readline()
|
|
if not line:
|
|
break
|
|
if '_WEB_URL=' in line:
|
|
url = line.split('=', 1)[1].strip()
|
|
break
|
|
if not url:
|
|
err = p.stderr.read()
|
|
sys.exit('server did not print its URL\n' + err)
|
|
return p, url
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('tool', choices=['pa', 'ba'])
|
|
ap.add_argument('--reads')
|
|
ap.add_argument('--out', default='/tmp/webshot')
|
|
ap.add_argument('--tabs', type=int, default=5)
|
|
ap.add_argument('--width', type=int, default=1400)
|
|
ap.add_argument('--height', type=int, default=2200)
|
|
a = ap.parse_args()
|
|
os.makedirs(a.out, exist_ok=True)
|
|
p, url = start_server(a.tool, a.reads)
|
|
exe = chrome()
|
|
errors = []
|
|
try:
|
|
spec = json.load(urllib.request.urlopen(url + 'api/spec', timeout=30))
|
|
tabs = len(spec.get('pages', [])) or a.tabs
|
|
for n in range(1, tabs + 1):
|
|
png = os.path.join(a.out, '%s-tab%d.png' % (a.tool, n))
|
|
log = os.path.join(a.out, '%s-tab%d.console.txt' % (a.tool, n))
|
|
cmd = [exe, '--headless=new', '--no-sandbox', '--disable-gpu', '--hide-scrollbars',
|
|
'--window-size=%d,%d' % (a.width, a.height), '--virtual-time-budget=4000',
|
|
'--enable-logging=stderr', '--v=0',
|
|
'--screenshot=' + png, '%s?tab=%d&expand=all' % (url, n)]
|
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
console = [l for l in r.stderr.splitlines() if 'CONSOLE' in l]
|
|
with open(log, 'w') as fh:
|
|
fh.write('\n'.join(console))
|
|
bad = [l for l in console if 'error' in l.lower() or 'uncaught' in l.lower()]
|
|
errors += bad
|
|
print('tab %d -> %s (%d console lines, %d errors)' % (n, png, len(console), len(bad)))
|
|
finally:
|
|
try:
|
|
urllib.request.urlopen(urllib.request.Request(url + 'api/close', method='POST', data=b'{}'), timeout=5)
|
|
except Exception:
|
|
pass
|
|
p.terminate()
|
|
if errors:
|
|
print('CONSOLE ERRORS:\n' + '\n'.join(errors[:20]))
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|