Every write path normalises a bundle's texture to WebP Lossless, so the catalog icon was being read back with a PNG-only decoder. decodePng throws on anything that is not a PNG, the callers caught that and returned null, and the user-visible result was "no icon (not in source or bundle)" for every furniture whose source does not serve a standalone icon. Measured against the production asset tree, all 18,505 bundles were WebP; icon extraction succeeded on 0 of them. src/lib/services/imager/ decode-texture.ts keeps PNG on the dependency-free decoder and routes WebP through sharp, which is already a dependency and already encodes these textures. extractFurniIconPng and getPetIconPng become async; the five call sites (upload, clone import, furni import, icon repair and both icon routes) already awaited their surrounding work. Same root cause, second bug: the spritesheet frame key. Converters disagree on packing — some keep a trailing ".png", and some lowercase the whole key while leaving the bundle name mixed-case, so "LTD_fashionistaf" looks up frame "LTD_fashionistaf_LTD_fashionistaf_icon_a" and never finds "ltd_fashionistaf_ltd_fashionistaf_icon_a". Any mixed-case classname could therefore never match, which is most of the catalogue. findFrame tries the two exact spellings, then falls back to one case-insensitive pass. Extraction now succeeds on 18,483 of 18,505 bundles (99.88%); the 22 remainder are data, not code — 9 bundles ship no icon asset, 11 do not parse. Third: three catalogue icons exist only as .gif while catalogueIconUrl hardcoded .png, so the picker offered icons that could only ever 404, and 291 icons that ship as both formats were listed twice. The API now dedupes per id and the two renderers retry with .gif before falling back to the placeholder, matching what catalog-image-picker already did. Verified live: /gamedata/.../icon_1542.png returns 404 while icon_1542.gif returns 200. Separately, close the last hole in the memory cap. Every script in package.json routes through scripts/with-memory-cap.sh, but invoking the builder directly — from a terminal, an IDE or an agent — skipped the wrapper and ran unbounded, on a host with no swap where the OOM killer picks its victim across the whole machine. next.config.ts now refuses a production build that the wrapper has not marked, before anything allocates. next dev and next start are deliberately unaffected. README gains a Memory-capped commands section covering the per-script ceilings, the backends and the ulimit -v trap, and its stale version and script tables are corrected.
179 lines
6.3 KiB
Bash
Executable File
179 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Voer een zwaar commando uit onder een harde geheugenplafond.
|
|
#
|
|
# Waarom dit bestaat:
|
|
# De host draait met `vm.overcommit_memory=0` en ZONDER swap. Vraagt een
|
|
# proces meer geheugen dan er vrij is, dan geeft de kernel niets weg en
|
|
# roept hij meteen de OOM-killer aan. Die kiest zijn slachtoffer over de
|
|
# héle machine, niet alleen in het schuldige proces — dus een build kan de
|
|
# database, nginx of de live release meenemen.
|
|
#
|
|
# `next build` op Turbopack groeit op dit 329-route app voorbij 12GB RSS
|
|
# en is ook met 4GB swap nog steeds dood. Zie commit 3d828a61.
|
|
#
|
|
# Wat dit doet:
|
|
# Het commando komt in zijn eigen cgroup met `MemoryMax`. Als het door die
|
|
# grens heen groeit krijgt alleen die cgroup een OOM-signaal: het commando
|
|
# zelf sterft, de rest van de machine leeft. Dat is precies het gedrag dat
|
|
# je wilt — de build faalt, de site blijft staan.
|
|
#
|
|
# Met `--max-old-space-size` lukt dat niet. Die limiet zit op de V8-heap en
|
|
# Turbopack-geheugen is native Rust-geheugen; met een 2GB cap piekte de RSS
|
|
# alsnog op 8GB. Zie het commentaar in de Dockerfile.
|
|
#
|
|
# Backends:
|
|
#
|
|
# systemd (cgroup MemoryMax)
|
|
# Meet RSS over de héle procesboom. Dit is de echte garantie en wordt
|
|
# overal gebruikt waar systemd beschikbaar is (de host waar deze
|
|
# CMS draait). De RSS-plafonds in package.json zijn hierop gekozen:
|
|
# `next build` piekte op 6,5GB, dus 10GB laat ruimte over terwijl er
|
|
# 6GB basislast naast blijft passen binnen de 23,5GB van deze machine.
|
|
#
|
|
# ulimit -v (per proces, virtuele adresruimte)
|
|
# Alleen als expliciet gevraagd. Meet virtuele adresruimte, NIET RSS, en
|
|
# kan de boom helemaal niet begrenzen: elke worker krijgt z'n eigen
|
|
# limiet. Op moderne V8 is het bovendien een vergiftigde gift: `-v 10g`
|
|
# laat V8 de heaplimiet terugbrengen naar 2,25GB (webpack sterft met
|
|
# std::bad_alloc), en `-v 20g` laat de v8-wasm-memory-toewijzing falen
|
|
# tijdens `next build`. Zet CMS_MEMORY_CAP_VIRTUAL ruim boven de fysieke
|
|
# RAM als je het echt wilt gebruiken.
|
|
#
|
|
# Geen van beide -> weigeren. Stil onbegrensd doorlopen zou precies de
|
|
# valse geruststelling zijn waar 3d828a61 voor waarschuwt. Omgevingen
|
|
# zonder systemd (de Docker-build, de GitLab-runner) kiezen daarom
|
|
# expliciet voor CMS_MEMORY_CAP_BACKEND=none — met een waarschuwing,
|
|
# en met als rechtvaardiging dat die builds al begrensd zijn door
|
|
# `next build --webpack` + `--max-old-space-size` en in hun eigen
|
|
# geïsoleerde container draaien, niet op de host.
|
|
#
|
|
# Markering:
|
|
# Elke backend zet `CMS_MEMORY_CAPPED=1` voordat het commando start.
|
|
# `next.config.ts` weigert een productie-build zonder die markering, zodat
|
|
# een handmatig `npx next build` (of een IDE/agent die de build zelf
|
|
# start) niet meer onbeperkt geheugen kan vragen en de hele host mee
|
|
# neemt. `CMS_MEMORY_CAP_BACKEND=none` telt mee: die omgevingen draaien
|
|
# al in een eigen, geïsoleerde container.
|
|
#
|
|
# Gebruik: bash scripts/with-memory-cap.sh 10g <command...>
|
|
# Backend kiezen: CMS_MEMORY_CAP_BACKEND=systemd|ulimit|none|auto
|
|
# ulimit-waarde kiezen: CMS_MEMORY_CAP_VIRTUAL=40g
|
|
set -Eeuo pipefail
|
|
|
|
usage() {
|
|
echo "gebruik: $0 <plafond, bv. 10g> <command...>" >&2
|
|
exit 64
|
|
}
|
|
|
|
[ "$#" -ge 2 ] || usage
|
|
cap="$1"
|
|
shift
|
|
|
|
# Zet 10g / 512m / 2G / 1234567 om in bytes. Alleen bytes gaan naar
|
|
# systemd: MemoryMax accepteert `10G` maar weigert `10g`, en die
|
|
# hoofdletterval is te makkelijk om per ongeluk te treffen.
|
|
to_bytes() {
|
|
local value="$1" number suffix
|
|
if [[ "$value" =~ ^([0-9]+)([kKmMgGtT]?)$ ]]; then
|
|
number="${BASH_REMATCH[1]}"
|
|
suffix="${BASH_REMATCH[2]}"
|
|
else
|
|
echo "onbekend plafond-formaat: $value" >&2
|
|
return 1
|
|
fi
|
|
case "$suffix" in
|
|
k | K) echo $((number * 1024)) ;;
|
|
m | M) echo $((number * 1024 * 1024)) ;;
|
|
g | G) echo $((number * 1024 * 1024 * 1024)) ;;
|
|
t | T) echo $((number * 1024 * 1024 * 1024 * 1024)) ;;
|
|
*) echo "$number" ;;
|
|
esac
|
|
}
|
|
|
|
bytes="$(to_bytes "$cap")" || exit 64
|
|
kilobytes=$((bytes / 1024))
|
|
|
|
# De virtuele waarde voor ulimit -v. Bewust los van `cap`: zie de toelichting
|
|
# hierboven, 10g -v breekt de webpack-build.
|
|
virtual_bytes="$(to_bytes "${CMS_MEMORY_CAP_VIRTUAL:-40g}")" || exit 64
|
|
virtual_kb=$((virtual_bytes / 1024))
|
|
|
|
backend="${CMS_MEMORY_CAP_BACKEND:-auto}"
|
|
systemd_cmd=()
|
|
|
|
# Vanaf hier is dit script de enige plek waar een zwaar commando nog mag
|
|
# starten. De markering maakt dat afdwingbaar in next.config.ts.
|
|
export CMS_MEMORY_CAPPED=1
|
|
|
|
# Echt proberen, niet alleen uitzoeken of het bestand bestaat: `systemd-run`
|
|
# zonder rechten faalt met "Access denied", en dat moet dan een nette
|
|
# terugval naar ulimit worden in plaats van een kapotte build.
|
|
probe_systemd() {
|
|
command -v systemd-run >/dev/null 2>&1 || return 1
|
|
[ -d /run/systemd/system ] || return 1
|
|
if systemd-run --scope --quiet true 2>/dev/null; then
|
|
systemd_cmd=(systemd-run --scope --quiet)
|
|
return 0
|
|
fi
|
|
if systemd-run --user --scope --quiet true 2>/dev/null; then
|
|
systemd_cmd=(systemd-run --user --scope --quiet)
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
run_systemd() {
|
|
echo "[mem-cap] systemd cgroup MemoryMax=$((bytes / 1024 / 1024 / 1024))GB: $*" >&2
|
|
exec "${systemd_cmd[@]}" -p "MemoryMax=$bytes" "$@"
|
|
}
|
|
|
|
run_ulimit() {
|
|
echo "[mem-cap] ulimit -v ${virtual_kb}KB per proces (geen systemd; RSS-plafond ${cap} niet meetbaar zonder cgroup): $*" >&2
|
|
if [ "$virtual_bytes" -lt $((16 * 1024 * 1024 * 1024)) ]; then
|
|
echo "[mem-cap] let op: -v onder 16g verlaagt V8's heaplimiet en breekt de build; verhoog CMS_MEMORY_CAP_VIRTUAL" >&2
|
|
fi
|
|
ulimit -v "$virtual_kb" || {
|
|
echo "[mem-cap] ulimit -v $virtual_kb werd geweigerd" >&2
|
|
return 1
|
|
}
|
|
exec "$@"
|
|
}
|
|
|
|
run_refuse() {
|
|
echo "[mem-cap] geen systemd hier; weiger onbegrensd te draaien." >&2
|
|
echo "[mem-cap] zet CMS_MEMORY_CAP_BACKEND=none om dit bewust te accepteren, of =ulimit voor een per-proces vangnet." >&2
|
|
return 1
|
|
}
|
|
|
|
run_opted_out() {
|
|
echo "[mem-cap] WAARSCHUWING: plafond bewust uitgeschakeld, dit commando kan de machine laten OOM-killed worden: $*" >&2
|
|
exec "$@"
|
|
}
|
|
|
|
case "$backend" in
|
|
systemd)
|
|
probe_systemd || {
|
|
echo "[mem-cap] CMS_MEMORY_CAP_BACKEND=systemd maar systemd-run reageert niet" >&2
|
|
exit 70
|
|
}
|
|
run_systemd "$@"
|
|
;;
|
|
ulimit)
|
|
run_ulimit "$@"
|
|
;;
|
|
none | off)
|
|
run_opted_out "$@"
|
|
;;
|
|
auto)
|
|
if probe_systemd; then
|
|
run_systemd "$@"
|
|
else
|
|
run_refuse "$@"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "[mem-cap] onbekende backend: $backend" >&2
|
|
exit 64
|
|
;;
|
|
esac
|