#!/usr/bin/env bash # ============================================================================= # PinCabOS - 02-install-engine.sh # Role: Web package installer worker called by go-pincabos during RUN_02. # Created by Karots Sugarpie # # This script does NOT manage workflow flags. # go-pincabos is the only owner of RUN flags, resume, reset and reboot flow. # # Dependencies / requisites: # Required packages: # ca-certificates curl wget jq tar zstd rsync unzip xz-utils file sudo # python3 python3-venv python3-pip python3-requests python3-yaml # systemd # # Required commands: # bash curl wget jq tar unzstd rsync python3 systemctl visudo # # Input official Web package: # https://ins.pincabos.cc/install/pkg/pkg-pincabos-web.zst # # Package checksum: # https://ins.pincabos.cc/install/pkg/pkg-pincabos-web.sha256 # # Global default paths: # PinCabOS root: /opt/pincabos # WebApp: /opt/pincabos/web # WebApp venv: /opt/pincabos/web/.venv # VPX runtime: /opt/pincabos/apps/vpinball # VPX launcher: /opt/pincabos/bin/vpx.sh # VPX INI: /home/pinball/.vpinball/VPinballX.ini # Tables: /home/pinball/Tables # VPinFE runtime: /opt/pincabos/apps/frontend/vpinfe/current # VPinFE INI: /opt/pincabos/config/vpinfe/vpinfe.ini # # ============================================================================= set -Eeuo pipefail ORANGE="\033[38;5;208m" CYAN="\033[36m" GREEN="\033[32m" RED="\033[31m" YELLOW="\033[33m" NC="\033[0m" TS="$(date +%Y%m%d-%H%M%S)" LOG_DIR="/opt/pincabos/logs" LOG_FILE="$LOG_DIR/02-install-engine-$TS.log" PCO_ROOT="/opt/pincabos" PCO_STAGE="${PCO_CLEAN_ENGINE_STAGE:-/tmp/pincabos-engine-audit/clean-bgfx-only-v2}" PCO_QUAR="$PCO_STAGE/_quarantine_forbidden_gl" PCO_BACKUP="/opt/pincabos/backups/run02-engine-$TS" INSTALL_BASE="${PIN_INSTALL_BASE:-https://ins.pincabos.cc/install}" UPDATE_BASE="${PIN_UPDATE_BASE:-https://ins.pincabos.cc/updates}" WEB_PKG_URL="${PIN_WEB_PKG_URL:-$INSTALL_BASE/pkg/pkg-pincabos-web.zst}" WEB_PKG_SHA_URL="${PIN_WEB_PKG_SHA_URL:-$INSTALL_BASE/pkg/pkg-pincabos-web.sha256}" WEB_PKG_MANIFEST_URL="${PIN_WEB_PKG_MANIFEST_URL:-$INSTALL_BASE/pkg/pkg-pincabos-web.manifest.json}" WEB_PKG_CACHE_DIR="/opt/pincabos/download/webpkg" WEB_PKG_FILE="$WEB_PKG_CACHE_DIR/pkg-pincabos-web.zst" WEB_PKG_SHA_FILE="$WEB_PKG_CACHE_DIR/pkg-pincabos-web.sha256" WEB_PKG_MANIFEST_FILE="$WEB_PKG_CACHE_DIR/pkg-pincabos-web.manifest.json" WEB_PKG_VERIFY_ROOT="/tmp/pincabos-webpkg-verify-$TS" WEB_DIR="/opt/pincabos/web" WEB_VENV="/opt/pincabos/web/.venv" VPX_DIR="/opt/pincabos/apps/vpinball" VPX_ALT_DIR="/opt/pincabos/apps/vpx" VPX_BIN="/opt/pincabos/bin/vpx.sh" VPX_INI="/home/pinball/.vpinball/VPinballX.ini" TABLES_DIR="/home/pinball/Tables" ROMS_DIR="/opt/pincabos/apps/vpinball/PinMAME/roms" VPINFE_DIR="/opt/pincabos/apps/frontend/vpinfe/current" VPINFE_INI="/opt/pincabos/config/vpinfe/vpinfe.ini" mkdir -p "$LOG_DIR" exec > >(tee -a "$LOG_FILE") 2>&1 pco_title() { echo -e "${CYAN}────────────────────────────────────────────────────────────────${NC}" echo -e "${ORANGE} PinCabOS - 02 Web package worker${NC}" echo -e "${CYAN}────────────────────────────────────────────────────────────────${NC}" } pco_step() { local id="${1:-??}" shift || true echo echo -e "${CYAN}─[${id}]─► ${ORANGE}${*:-Step}${CYAN} ◄────────────────────────────────────────${NC}" } pco_info() { echo -e "${CYAN:-}INFO ${*:-Info}${NC:-}"; } pco_go() { echo -e "${GREEN}GO [√] ${*:-OK}${NC}"; } pco_warn() { echo -e "${YELLOW}WARN ${*:-Warning}${NC}"; } pco_nogo() { local code="${1:-ERR-02-UNKNOWN}" shift || true echo -e "${RED}NOGO [***] ${code}${NC} ${*:-No detail}" echo "Log: $LOG_FILE" exit 1 } run_cmd() { echo "+ $*" "$@" } apt_install() { export DEBIAN_FRONTEND=noninteractive apt-get update DEBIAN_FRONTEND=noninteractive apt-get -y autoremove apt-get install -y "$@" } assert_no_run_flags_management() { pco_step "00" "Workflow ownership guard" pco_go "go-pincabos owns RUN flags; 02 does not create or delete workflow flags" } pincabos_validate_webpkg_sha() { local pkg_file="$1" local sha_file="$2" local expected local actual if [ ! -s "$pkg_file" ]; then pco_nogo "ERR-02-WEBPKG-MISSING-001" "Downloaded Web package missing or empty: $pkg_file" fi if [ ! -s "$sha_file" ]; then pco_nogo "ERR-02-WEBPKG-SHA-MISSING-001" "Downloaded SHA256 file missing or empty: $sha_file" fi expected="$(awk 'NF {print $1; exit}' "$sha_file")" if ! printf '%s\n' "$expected" | grep -Eq '^[a-fA-F0-9]{64}$'; then pco_nogo "ERR-02-WEBPKG-SHA-FORMAT-001" "Invalid SHA256 format in $sha_file: $expected" fi actual="$(sha256sum "$pkg_file" | awk '{print $1}')" if [ "$actual" != "$expected" ]; then echo "Expected: $expected" echo "Actual: $actual" pco_nogo "ERR-02-WEBPKG-SHA" "SHA256 validation failed" fi pco_go "SHA256 validation OK for $(basename "$pkg_file")" } fetch_official_web_package() { pco_step "01" "Fetch official PinCabOS Web package" mkdir -p "$WEB_PKG_CACHE_DIR" echo "Package URL: $WEB_PKG_URL" echo "SHA URL: $WEB_PKG_SHA_URL" echo "Manifest URL: $WEB_PKG_MANIFEST_URL" rm -f "$WEB_PKG_FILE.tmp" "$WEB_PKG_SHA_FILE.tmp" "$WEB_PKG_MANIFEST_FILE.tmp" curl -fL --retry 3 --connect-timeout 20 "$WEB_PKG_URL" -o "$WEB_PKG_FILE.tmp" \ || pco_nogo "ERR-02-WEBPKG-DOWNLOAD" "Unable to download official Web package" mv -f "$WEB_PKG_FILE.tmp" "$WEB_PKG_FILE" if curl -fL --retry 2 --connect-timeout 15 "$WEB_PKG_SHA_URL" -o "$WEB_PKG_SHA_FILE.tmp"; then mv -f "$WEB_PKG_SHA_FILE.tmp" "$WEB_PKG_SHA_FILE" pincabos_validate_webpkg_sha "$WEB_PKG_FILE" "$WEB_PKG_SHA_FILE" else pco_warn "SHA file not available; package downloaded without SHA validation" fi if curl -fL --retry 2 --connect-timeout 15 "$WEB_PKG_MANIFEST_URL" -o "$WEB_PKG_MANIFEST_FILE.tmp"; then mv -f "$WEB_PKG_MANIFEST_FILE.tmp" "$WEB_PKG_MANIFEST_FILE" python3 -m json.tool "$WEB_PKG_MANIFEST_FILE" >/dev/null \ && pco_go "Manifest JSON validation OK" \ || pco_warn "Manifest downloaded but JSON validation failed" else pco_warn "Manifest not available" fi ls -lh "$WEB_PKG_FILE" "$WEB_PKG_SHA_FILE" "$WEB_PKG_MANIFEST_FILE" 2>/dev/null || true pco_go "Official Web package fetched" } validate_official_web_package() { pco_step "02" "Validate official Web package policy" [ -f "$WEB_PKG_FILE" ] || pco_nogo "ERR-02-WEBPKG-MISSING" "Missing package file: $WEB_PKG_FILE" echo echo "=== Package forbidden state/log/backup paths, must be empty ===" # Official engine WebPkg is allowed to ship VPX/VPinFE runtime assets under /opt/pincabos/apps. # It must not ship machine-local state, logs, backups, or user runtime config. if tar --zstd -tf "$WEB_PKG_FILE" | grep -E '(^\./|^)home/pinball/\.vpinball(/|$)|(^\./|^)home/pinball/\.config/vpinfe(/|$)|(^\./|^)opt/pincabos/logs(/|$)|(^\./|^)opt/pincabos/backups(/|$)|(^\./|^)opt/pincabos/config/backups(/|$)|(^\./|^)opt/pincabos/flags(/|$)|(^\./|^)opt/pincabos/state(/|$)'; then pco_nogo "ERR-02-WEBPKG-FORBIDDEN-PATH" "Official Web package contains forbidden machine-local state/log/backup paths" fi echo echo "=== Required Web package content ===" tar --zstd -tf "$WEB_PKG_FILE" | grep -E 'opt/pincabos/web/|opt/pincabos/media/|etc/sudoers.d/pincabos-|etc/systemd/system/pincabos-' | sed -n '1,180p' || true echo echo "=== Nginx package policy, must be absent in direct-port runtime ===" if tar --zstd -tf "$WEB_PKG_FILE" | grep -E '^\./?etc/nginx/'; then pco_nogo "ERR-02-WEBPKG-NGINX-FORBIDDEN-001" "Official direct-port Web package must not ship nginx files" fi rm -rf "$WEB_PKG_VERIFY_ROOT" mkdir -p "$WEB_PKG_VERIFY_ROOT" tar --zstd -xf "$WEB_PKG_FILE" -C "$WEB_PKG_VERIFY_ROOT" echo echo "=== Active forbidden GL usage in package, must be empty ===" if grep -RInE '/opt/pincabos/apps/vpinball/VPinballX[_]GL|exec .*VPinballX[_]GL|vpxbinpath.*VPinballX[_]GL|find .*VPinballX[_]GL' "$WEB_PKG_VERIFY_ROOT" 2>/dev/null; then pco_nogo "ERR-02-WEBPKG-GL" "Official Web package contains active forbidden GL usage" fi echo echo "=== WebApp Python syntax from package ===" if [ -f "$WEB_PKG_VERIFY_ROOT/opt/pincabos/web/app.py" ]; then python3 -m py_compile "$WEB_PKG_VERIFY_ROOT/opt/pincabos/web/app.py" \ || pco_nogo "ERR-02-WEBPKG-APP-PY" "Package app.py failed Python compile" pco_go "Package app.py compile OK" else pco_nogo "ERR-02-WEBPKG-NO-APP" "Package missing /opt/pincabos/web/app.py" fi rm -rf "$WEB_PKG_VERIFY_ROOT" pco_go "Official Web package policy validation OK" } install_official_web_package() { pco_step "03" "Install official Web package to /" [ -f "$WEB_PKG_FILE" ] || pco_nogo "ERR-02-WEBPKG-MISSING" "Missing package file: $WEB_PKG_FILE" tar --zstd -xpf "$WEB_PKG_FILE" -C / pco_go "Official Web package extracted to /" } assert_stage_ready() { pco_step "01" "Validate clean engine module from /tmp" [ -d "$PCO_STAGE" ] || pco_nogo "ERR-02-STAGE-001" "Missing clean engine module: $PCO_STAGE" if [ -d "$PCO_QUAR" ]; then pco_go "Quarantine exists and will not be imported: $PCO_QUAR" else pco_warn "No quarantine directory found" fi if grep -RInE 'VPinballX[_]GL|vpinball[_]gl|VPinball[_]GL|VPINBALL[_]GL' "$PCO_STAGE" 2>/dev/null \ | grep -v '/_quarantine_forbidden_gl/' >/tmp/pincabos-02-stage-gl-leftovers.txt; then sed -n '1,160p' /tmp/pincabos-02-stage-gl-leftovers.txt pco_nogo "ERR-02-STAGE-GL" "Forbidden GL reference found outside quarantine" fi pco_go "Clean engine module accepted: $PCO_STAGE" du -sh "$PCO_STAGE" || true } backup_live_system() { pco_step "02" "Backup live PinCabOS targets" mkdir -p "$PCO_BACKUP" for p in \ "$WEB_DIR" \ /opt/pincabos/media \ /opt/pincabos/tools \ /opt/pincabos/scripts \ /opt/pincabos/bin \ /opt/pincabos/config \ "$VPX_INI" \ "$VPINFE_INI" \ /etc/nginx/sites-available/default \ /etc/nginx/sites-enabled/default \ /etc/nginx/sites-available/pincabos-web \ /etc/nginx/sites-available/pincabos-web.conf \ /etc/nginx/sites-enabled/pincabos-web \ /etc/nginx/sites-enabled/pincabos-web.conf \ /etc/systemd/system/pincabos-web.service \ /etc/systemd/system/pincabos-console.service \ /etc/systemd/system/pincabos-vpinfe.service \ /etc/sudoers.d/pincabos-web do if [ -e "$p" ]; then mkdir -p "$PCO_BACKUP$(dirname "$p")" cp -a "$p" "$PCO_BACKUP$p" 2>/dev/null || true echo "BACKUP: $p" fi done pco_go "Backup root: $PCO_BACKUP" } import_engine_module() { pco_step "03" "Import engine module from /tmp" mkdir -p /opt/pincabos rsync -a "$PCO_STAGE"/ / \ --exclude='_quarantine_forbidden_gl/***' \ --exclude='opt/pincabos/apps/vpinball/***' \ --exclude='opt/pincabos/apps/vpx/***' \ --exclude='home/pinball/.vpinball/***' \ --exclude='home/pinball/.config/vpinfe/***' \ --exclude='opt/pincabos/config/vpinfe/***' \ --exclude='opt/pincabos/config/dof/***' \ --exclude='opt/pincabos/config/screens/***' \ --exclude='opt/pincabos/config/backups/***' \ --exclude='opt/pincabos/config/ssh-backup-*' \ --exclude='opt/pincabos/config/*calibration*.json' \ --exclude='opt/pincabos/config/*display*.json' \ --exclude='opt/pincabos/config/vpx-engine.json' \ --exclude='opt/pincabos/config/audio-router.json' \ --exclude='opt/pincabos/config/firstrun.json' \ --exclude='opt/pincabos/config/pincabos-update.json' \ --exclude='usr/local/bin/00-install-admin.sh' \ --exclude='usr/local/bin/01-install-system.sh' \ --exclude='usr/local/bin/02-install-engine.sh' \ --exclude='usr/local/bin/03-install-check.sh' \ --exclude='usr/local/bin/update-vpx.sh' \ --exclude='opt/pincabos/tools/update-vpx.sh' \ --exclude='usr/local/bin/pincabos-run-vpx' \ --exclude='opt/pincabos/bin/vpx.sh' \ --exclude='opt/pincabos/bin/pincabos-vpx-launch-with-pinmame-overlay.sh' \ --exclude='opt/pincabos/tools/pincabos-ensure-vpx-overlay-runtime.sh' \ --exclude='opt/pincabos/web/app.py' \ --exclude='opt/pincabos/tools/pincabos-apply-update.sh' \ --exclude='opt/pincabos/tools/pincabos-smart-archive-import.py' \ --exclude='usr/local/bin/pincabos-apply-update.sh' pco_go "Engine module imported without VPX runtime, user configs, old GL files or quarantine" } sanitize_file_from_quarantine() { local rel="$1" local dst="$2" local src="$PCO_QUAR/$rel" [ -f "$src" ] || return 1 mkdir -p "$(dirname "$dst")" cp -a "$src" "$dst" sed -Ei \ -e 's|VPinballX[_]GL|VPinballX-BGFX|g' \ -e 's|vpinball[_]gl|vpinballX-BGFX|g' \ -e 's|VPinball[_]GL|VPinballX-BGFX|g' \ -e 's|VPINBALL[_]GL|VPINBALL_BGFX|g' \ "$dst" if grep -qE 'VPinballX[_]GL|vpinball[_]gl|VPinball[_]GL|VPINBALL[_]GL' "$dst"; then rm -f "$dst" return 1 fi return 0 } restore_or_create_webapp() { pco_step "04" "Restore WebApp and sanitize app.py" mkdir -p "$WEB_DIR/static" if sanitize_file_from_quarantine "opt/pincabos/web/app.py" "$WEB_DIR/app.py"; then if python3 -m py_compile "$WEB_DIR/app.py"; then pco_go "Sanitized app.py restored from quarantine" else mv "$WEB_DIR/app.py" "$WEB_DIR/app.py.bad-sanitized-$TS" || true pco_warn "Sanitized app.py failed Python compile; creating minimal clean WebApp" fi fi if [ ! -f "$WEB_DIR/app.py" ]; then cat >"$WEB_DIR/app.py" <<'PYAPP' #!/usr/bin/env python3 # PinCabOS minimal WebApp # Created by Karots Sugarpie import os from flask import Flask, jsonify app = Flask(__name__) @app.get("/") def index(): return """ PinCabOS

PinCabOS WebApp

Engine worker completed. WebApp is online.

VPX runtime policy: BGFX only.

GL runtime is forbidden.

""" @app.get("/health") def health(): return jsonify(ok=True, service="pincabos-web", vpx_policy="BGFX only") PYAPP pco_go "Minimal clean app.py created" fi chmod 755 "$WEB_DIR/app.py" } restore_sanitized_tools() { pco_step "05" "Restore sanitized optional tools from quarantine" if sanitize_file_from_quarantine "opt/pincabos/tools/pincabos-apply-update.sh" "/opt/pincabos/tools/pincabos-apply-update.sh"; then chmod 755 /opt/pincabos/tools/pincabos-apply-update.sh pco_go "Sanitized pincabos-apply-update.sh restored" else pco_warn "pincabos-apply-update.sh not restored" fi if sanitize_file_from_quarantine "opt/pincabos/tools/pincabos-smart-archive-import.py" "/opt/pincabos/tools/pincabos-smart-archive-import.py"; then chmod 755 /opt/pincabos/tools/pincabos-smart-archive-import.py pco_go "Sanitized pincabos-smart-archive-import.py restored" else pco_warn "pincabos-smart-archive-import.py not restored" fi } install_plymouth_and_wallpapers() { pco_go "RUN_02 skips final Plymouth; RUN_03 applies final loading theme once" } write_global_paths_config() { pco_step "07" "Write global default paths" mkdir -p /opt/pincabos/config /opt/pincabos/config/vpinfe "$(dirname "$VPX_INI")" "$TABLES_DIR" "$ROMS_DIR" "$VPX_DIR" cat >/opt/pincabos/config/pincabos-paths.json </dev/null | head -n1 || true)" if [ -n "$bgfx_bin" ]; then canonical="$VPX_DIR/current/VPinballX-BGFX" mkdir -p "$VPX_DIR/current" # Never replace the real canonical binary with a symlink to itself. if [ "$bgfx_bin" != "$canonical" ]; then if [ ! -e "$canonical" ] || [ -L "$canonical" ]; then rm -f "$canonical" ln -sfn "$bgfx_bin" "$canonical" fi fi ln -sfn "$canonical" "$VPX_DIR/VPinballX-BGFX" ln -sfn "$VPX_DIR/VPinballX-BGFX" "$VPX_DIR/VPinballX" if [ -L "$canonical" ] && [ "$(readlink "$canonical")" = "$canonical" ]; then pco_nogo "ERR-02-VPX-SELF-SYMLINK" "Canonical VPX BGFX binary is a self-symlink: $canonical" fi pco_go "BGFX binary normalized: $bgfx_bin" fi cat >"$VPX_BIN" <<'EOF_VPX' #!/usr/bin/env bash set -Eeuo pipefail export SDL_VIDEODRIVER="${SDL_VIDEODRIVER:-x11}" for candidate in \ /opt/pincabos/apps/vpinball/current/VPinballX-BGFX \ /opt/pincabos/apps/vpinball/VPinballX-BGFX \ /opt/pincabos/apps/vpinball/vpinballX-BGFX \ /opt/pincabos/apps/vpinball/VPinballX_BGFX \ /opt/pincabos/apps/vpinball/vpinballX_BGFX do if [ -x "$candidate" ]; then exec "$candidate" "$@" fi done echo "NOGO: VPX BGFX binary not installed under /opt/pincabos/apps/vpinball" >&2 exit 127 EOF_VPX chmod 755 "$VPX_BIN" ln -sfn "$VPX_BIN" /usr/local/bin/pincabos-vpx ln -sfn "$VPX_BIN" /usr/local/bin/vpx-pincabos if [ -n "$bgfx_bin" ]; then pco_go "VPX launcher ready: $VPX_BIN" else pco_warn "No BGFX VPX binary found yet. Launcher is ready; pkg-vpx-bgfx-runtime.sh must install the runtime." fi } ensure_vpx_ini() { pco_step "09" "Ensure VPX INI defaults without destroying existing file" mkdir -p "$(dirname "$VPX_INI")" if [ ! -f "$VPX_INI" ]; then cat >"$VPX_INI" <<'EOF_INI' ; PinCabOS - VPinballX.ini minimal defaults ; Created by Karots Sugarpie [Player] FullScreen = 1 [Controller] DOFPlugin = 1 B2SPlugins = 1 [Displays] tablescreenid = 0 bgscreenid = 1 dmdscreenid = 2 EOF_INI pco_go "Created: $VPX_INI" else cp -a "$VPX_INI" "$VPX_INI.backup-run02-$TS" 2>/dev/null || true pco_go "Existing VPX INI preserved: $VPX_INI" fi } ensure_vpinfe_ini() { pco_step "10" "Ensure VPinFE INI global defaults" mkdir -p "$(dirname "$VPINFE_INI")" if [ ! -f "$VPINFE_INI" ]; then cat >"$VPINFE_INI" </dev/null || true python3 - "$VPINFE_INI" "$VPX_BIN" "$TABLES_DIR" "$VPX_INI" "$VPX_DIR" "$VPINFE_DIR" "$ROMS_DIR" <<'PY' import configparser, sys from pathlib import Path ini = Path(sys.argv[1]) vpxbin, tables, vpxini, vpxdir, vpinfedir, roms = sys.argv[2:] cp = configparser.ConfigParser() cp.optionxform = str cp.read(ini) for sec in ("Settings", "PinCabOS"): if not cp.has_section(sec): cp.add_section(sec) cp.set("Settings", "tablerootdir", tables) cp.set("Settings", "vpxbinpath", vpxbin) cp.set("Settings", "vpxinipath", vpxini) cp.set("Settings", "themeassetsport", "8001") cp.set("Settings", "manageruiport", "8000") cp.set("PinCabOS", "vpxbinpath", vpxbin) cp.set("PinCabOS", "vpxinipath", vpxini) cp.set("PinCabOS", "vpxdir", vpxdir) cp.set("PinCabOS", "vpinfedir", vpinfedir) cp.set("PinCabOS", "romsdir", roms) with ini.open("w") as f: cp.write(f) PY pco_go "Merged VPinFE defaults into existing INI" fi } rebuild_python_venv() { pco_step "11" "Rebuild Python WebApp venv" mkdir -p "$WEB_DIR" rm -rf "$WEB_VENV" python3 -m venv "$WEB_VENV" "$WEB_VENV/bin/python" -m pip install --upgrade pip wheel setuptools if [ -f "$WEB_DIR/requirements.txt" ]; then "$WEB_VENV/bin/pip" install -r "$WEB_DIR/requirements.txt" || true fi "$WEB_VENV/bin/pip" install flask requests pyyaml psutil || true pco_go "Python venv rebuilt: $WEB_VENV" } write_nginx() { pco_step "12" "Disable nginx runtime for official direct ports" rm -f /etc/nginx/sites-enabled/default \ /etc/nginx/sites-enabled/pincabos-web \ /etc/nginx/sites-enabled/pincabos-web.conf 2>/dev/null || true if systemctl list-unit-files nginx.service >/dev/null 2>&1; then systemctl stop nginx.service 2>/dev/null || true systemctl disable nginx.service 2>/dev/null || true systemctl mask nginx.service 2>/dev/null || true pco_go "nginx disabled/masked; PinCabOS runtime uses direct ports" else pco_go "nginx.service absent; direct ports model OK" fi pco_go "PinCabOS direct ports OK: WebApp=80 Console=8090 VPinFE=8000 VPinFE-API=8001" } write_systemd() { pco_step "13" "Rewrite systemd services" mkdir -p /etc/systemd/system/pincabos-webapp.service.d /etc/systemd/system/pincabos-web.service.d /opt/pincabos/tools cat >/opt/pincabos/tools/run-vpinfe.sh <<'EOF_RUN_VPINFE' #!/usr/bin/env bash # PinCabOs-File created by Karots Sugarpie # Created by Karots Sugarpie # PINCABOS_SCRIPT_NAME="run-vpinfe.sh" # PINCABOS_SCRIPT_ROLE="Launch VPinFE binary or Python main.py with PinCabOS config" # PINCABOS_SCRIPT_REQUIRES_FILES="/opt/pincabos/apps/frontend/vpinfe/current/main.py /opt/pincabos/config/vpinfe/vpinfe.ini" # PINCABOS_SCRIPT_REQUIRES_COMMANDS="bash" set -Eeuo pipefail export HOME="${HOME:-/home/pinball}" export USER="${USER:-pinball}" export LOGNAME="${LOGNAME:-pinball}" export DISPLAY="${DISPLAY:-:0}" export XAUTHORITY="${XAUTHORITY:-/home/pinball/.Xauthority}" export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/1000}" export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=/run/user/1000/bus}" export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-/home/pinball/.config}" ROOT="/opt/pincabos/apps/frontend/vpinfe" CUR="$ROOT/current" CFG="/opt/pincabos/config/vpinfe/vpinfe.ini" LOG="/opt/pincabos/logs/vpinfe-launch.log" mkdir -p /opt/pincabos/logs touch "$LOG" 2>/dev/null || true chown pinball:pinball "$LOG" 2>/dev/null || true { echo "────────────────────────────────────────────────────────────────" echo "PinCabOS VPinFE launch $(date -Is)" echo "USER=$(id -un 2>/dev/null || true)" echo "DISPLAY=$DISPLAY" echo "XAUTHORITY=$XAUTHORITY" echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" echo "XDG_CONFIG_HOME=$XDG_CONFIG_HOME" echo "CFG=$CFG" } >>"$LOG" for candidate in \ "$CUR/vpinfe" \ "$CUR/VPinFE" \ "$ROOT/vpinfe" \ "$ROOT/VPinFE" do if [ -x "$candidate" ]; then echo "BIN=$candidate" >>"$LOG" cd "$(dirname "$candidate")" exec "$candidate" "$@" fi done PY="$CUR/.venv/bin/python" MAIN="$CUR/main.py" if [ ! -x "$PY" ]; then echo "NOGOOD: VPinFE Python missing/executable: $PY" >>"$LOG" exit 1 fi if [ ! -f "$MAIN" ]; then echo "NOGOOD: VPinFE main.py missing: $MAIN" >>"$LOG" exit 1 fi if [ ! -f "$CFG" ]; then echo "NOGOOD: VPinFE config missing: $CFG" >>"$LOG" exit 1 fi echo "PYTHON=$PY" >>"$LOG" echo "MAIN=$MAIN" >>"$LOG" echo "MODE=python-main-visible" >>"$LOG" # Wait briefly for LightDM/Openbox X session so VPinFE can open Chromium frontend. for i in $(seq 1 30); do if [ -S /tmp/.X11-unix/X0 ]; then break fi echo "WAIT_X=$i no /tmp/.X11-unix/X0 yet" >>"$LOG" sleep 1 done # Prefer pinball Xauthority when available; fallback to LightDM root auth. if [ -f /home/pinball/.Xauthority ]; then export XAUTHORITY=/home/pinball/.Xauthority elif [ -f /var/run/lightdm/root/:0 ]; then export XAUTHORITY=/var/run/lightdm/root/:0 fi echo "FINAL_DISPLAY=$DISPLAY" >>"$LOG" echo "FINAL_XAUTHORITY=$XAUTHORITY" >>"$LOG" cd "$CUR" exec "$PY" "$MAIN" --configfile "$CFG" EOF_RUN_VPINFE chmod 0755 /opt/pincabos/tools/run-vpinfe.sh chown pinball:pinball /opt/pincabos/tools/run-vpinfe.sh 2>/dev/null || true mkdir -p /home/pinball/.config/vpinfe touch /home/pinball/.config/vpinfe/vpinfe.log chown -R pinball:pinball /home/pinball/.config/vpinfe chmod 0755 /home/pinball/.config /home/pinball/.config/vpinfe chmod 0644 /home/pinball/.config/vpinfe/vpinfe.log pco_go "VPinFE Python runner and pinball config ownership prepared" cat >/etc/systemd/system/pincabos-webapp.service </etc/systemd/system/pincabos-web.service </etc/systemd/system/pincabos-webapp.service.d/10-pincabos-web-port.conf <<'EOF_WEB_DROPIN' [Service] Environment=PINCABOS_WEB_HOST=0.0.0.0 Environment=PINCABOS_WEB_PORT=80 Environment=PCO_WEB_HOST=0.0.0.0 Environment=PCO_WEB_PORT=80 EOF_WEB_DROPIN cat >/etc/systemd/system/pincabos-web.service.d/10-pincabos-legacy.conf <<'EOF_WEB_LEGACY_DROPIN' [Unit] Documentation=https://pincabos.cc/ EOF_WEB_LEGACY_DROPIN cat >/etc/default/ttyd <<'EOF_TTYD_DEFAULT' # /etc/default/ttyd # PinCabOS official console backend. TTYD_OPTIONS="-W -i lo -p 8090 -O login" EOF_TTYD_DEFAULT cat >/etc/systemd/system/pincabos-console.service <<'EOF_CONSOLE' [Unit] Description=PinCabOS Console ttyd After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/bin/ttyd -p 8090 -W /bin/bash Restart=always RestartSec=3 [Install] WantedBy=multi-user.target EOF_CONSOLE cat >/etc/systemd/system/pincabos-vpinfe.service </etc/systemd/system/pincabos-frontend.service </dev/null 2>&1 || true systemctl restart pincabos-webapp.service || true systemctl restart pincabos-web.service || true if command -v ttyd >/dev/null 2>&1; then systemctl enable pincabos-console.service >/dev/null 2>&1 || true systemctl restart pincabos-console.service || true echo echo "=== Disable raw ttyd.service; PinCabOS console owns ttyd on port 8090 ===" systemctl stop ttyd.service >/dev/null 2>&1 || true systemctl disable ttyd.service >/dev/null 2>&1 || true systemctl mask ttyd.service >/dev/null 2>&1 || true systemctl reset-failed ttyd.service >/dev/null 2>&1 || true systemctl enable pincabos-console.service >/dev/null 2>&1 || true systemctl restart pincabos-console.service >/dev/null 2>&1 || true else pco_warn "ttyd missing; console service created but not started" fi systemctl enable pincabos-vpinfe.service pincabos-frontend.service >/dev/null 2>&1 || true pco_go "Systemd services written: webapp, legacy web alias, console, vpinfe, frontend wrapper" } install_sudoers() { pco_step "14" "Install sudoers" mkdir -p /etc/sudoers.d if [ -d "$PCO_STAGE/etc/sudoers.d" ]; then find "$PCO_STAGE/etc/sudoers.d" -maxdepth 1 -type f ! -name README -print0 | while IFS= read -r -d '' f; do dst="/etc/sudoers.d/$(basename "$f")" cp -f "$f" "$dst" chmod 440 "$dst" if visudo -cf "$dst"; then echo "GO: sudoers OK: $dst" else rm -f "$dst" pco_nogo "ERR-02-SUDOERS" "Invalid sudoers rejected: $f" fi done fi if [ -x /opt/pincabos/tools/install-pincabos-sudoers.sh ]; then /opt/pincabos/tools/install-pincabos-sudoers.sh || true fi pco_go "Sudoers installed" } write_path_commands() { pco_step "15" "Recreate PATH commands" ln -sfn /opt/pincabos/bin/vpx.sh /usr/local/bin/pincabos-vpx ln -sfn /opt/pincabos/bin/vpx.sh /usr/local/bin/vpx-pincabos for f in /opt/pincabos/tools/*.sh /opt/pincabos/scripts/*.sh /opt/pincabos/bin/*.sh; do [ -f "$f" ] || continue chmod 755 "$f" || true base="$(basename "$f")" name="${base%.sh}" case "$name" in update-vpx|02-install-engine|01-install-system|00-install-admin|03-install-check) continue ;; esac ln -sfn "$f" "/usr/local/bin/$name" 2>/dev/null || true done ln -sfn /opt/pincabos/install/02-install-engine.sh /usr/local/bin/02-install-engine pco_go "PATH commands refreshed" } set_permissions() { pco_step "16" "Set permissions and ownership" mkdir -p /opt/pincabos/logs /opt/pincabos/config "$TABLES_DIR" "$ROMS_DIR" chmod 755 /opt/pincabos/bin/*.sh 2>/dev/null || true chmod 755 /opt/pincabos/tools/*.sh 2>/dev/null || true chmod 755 /opt/pincabos/scripts/*.sh 2>/dev/null || true chown -R pinball:pinball /opt/pincabos 2>/dev/null || true chown -R pinball:pinball /home/pinball 2>/dev/null || true pco_go "Permissions completed" } final_validation() { pco_step "17" "Final validation" echo echo "=== Forbidden GL scan in live PinCabOS critical files ===" if grep -RInE 'VPinballX[_]GL|vpinball[_]gl|VPinball[_]GL|VPINBALL[_]GL' \ /opt/pincabos/bin \ /opt/pincabos/tools \ /opt/pincabos/web \ /opt/pincabos/config \ 2>/dev/null | sed -n '1,160p'; then pco_warn "GL text still exists in live files. Review above." else pco_go "No forbidden GL text in live critical files" fi echo echo echo "=== Services ===" systemctl is-active pincabos-webapp.service 2>/dev/null || true systemctl is-active pincabos-web.service 2>/dev/null || true echo echo "=== Paths ===" echo "WebApp: $WEB_DIR" echo "Web venv: $WEB_VENV" echo "VPX dir: $VPX_DIR" echo "VPX launcher: $VPX_BIN" echo "VPX INI: $VPX_INI" echo "Tables: $TABLES_DIR" echo "VPinFE INI: $VPINFE_INI" echo "Log: $LOG_FILE" pco_go "02 engine worker completed" } pincabos_write_nginx_canonical_vhost() { pco_go "nginx canonical vhost skipped; direct-port runtime owns WebApp=80" } pincabos_clean_nginx_enabled_sites() { rm -f /etc/nginx/sites-enabled/default \ /etc/nginx/sites-enabled/pincabos-web \ /etc/nginx/sites-enabled/pincabos-web.conf 2>/dev/null || true pco_go "nginx enabled sites removed if present; direct-port runtime owns WebApp=80" } pincabos_assert_nginx_no_active_default_server() { pco_go "nginx runtime disabled; no active nginx guard required" } main() { clear pco_title assert_no_run_flags_management pco_step "A1" "Install dependencies" apt_install \ ca-certificates curl wget jq tar zstd rsync unzip xz-utils file sudo \ python3 python3-venv python3-pip python3-requests python3-yaml \ if apt-cache policy ttyd 2>/dev/null | awk '/Candidate:/ {print $2}' | grep -vqE '^(\(none\)|)$'; then apt-get install -y ttyd || pco_warn "Optional ttyd package install failed; console service will be created but may stay inactive" else pco_warn "Optional ttyd package has no APT candidate on this Ubuntu release" fi fetch_official_web_package validate_official_web_package backup_live_system install_official_web_package restore_or_create_webapp restore_sanitized_tools pco_go "RUN_02 skips final Plymouth; RUN_03 is the single owner" write_global_paths_config pco_import_golden_runtime_package || true ensure_vpx_runtime_paths ensure_vpx_ini ensure_vpinfe_ini rebuild_python_venv write_nginx write_systemd install_sudoers write_path_commands set_permissions final_validation } main "$@"