diff --git a/.github/upstream-digests.json b/.github/upstream-digests.json index 6df9f78e..31dd952b 100644 --- a/.github/upstream-digests.json +++ b/.github/upstream-digests.json @@ -1,4 +1,4 @@ { - "linuxserver/plex:latest": "sha256:f6c58cb2f5e41cd1397bf2ed4e61ef63bd86e0736841b3ef426fabfe04606293", + "linuxserver/plex:latest": "sha256:7f9a1d574958fc2f177c14ca190d4b811a58c274477f5bae8fb44ee676fb96bf", "plexinc/pms-docker:latest": "sha256:83a425ae9e133b1cb2cc3b809556e01c61cd8ff65c582e41b4374bc2210bac9e" } diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml new file mode 100644 index 00000000..3c26429b --- /dev/null +++ b/.github/workflows/build-debug.yml @@ -0,0 +1,87 @@ +name: Build Debug / Sanitizer Image + +on: + workflow_dispatch: + inputs: + sanitizer: + description: 'Sanitizer type (address, thread, or empty for debug symbols only)' + required: false + default: 'address' + type: choice + options: + - address + - thread + - '' + +jobs: + build-debug: + name: Build Debug / Sanitizer (${{ matrix.name }} / ${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - name: linuxserver + dockerfile: Dockerfile + image: ghcr.io/${{ github.repository_owner }}/plex-postgresql-debug + arch: amd64 + platform: linux/amd64 + runner: ubuntu-latest + # Optionally add plexinc variant if you use it + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push debug image + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: ${{ matrix.platform }} + push: true + provenance: false + tags: | + ${{ matrix.image }}:${{ github.sha }}-${{ matrix.arch }} + ${{ matrix.image }}:latest-${{ matrix.arch }} + build-args: | + PLEX_PG_SANITIZE=${{ inputs.sanitizer }} + # Optionally add other debug flags via environment variables if needed + + manifest-debug: + name: Create multi-arch manifest for debug + runs-on: ubuntu-latest + needs: build-debug + if: always() + steps: + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push manifest + run: | + set -eux + IMAGE="ghcr.io/${{ github.repository_owner }}/plex-postgresql-debug" + SHA="${{ github.sha }}" + docker manifest create "${IMAGE}:latest" \ + "${IMAGE}:${SHA}-amd64" + docker manifest push "${IMAGE}:latest" + # Also tag with sanitizer type if needed + if [ -n "${{ inputs.sanitizer }}" ]; then + docker manifest create "${IMAGE}:${{ inputs.sanitizer }}" \ + "${IMAGE}:${SHA}-amd64" + docker manifest push "${IMAGE}:${{ inputs.sanitizer }}" + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 426b03bb..969d31b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.15] - 2026-08-31 + +### Changed +- Updated upstream base Docker images (linuxserver/plex:latest / plexinc/pms-docker:latest). + ## [1.3.14] - 2026-08-18 ### Changed diff --git a/Dockerfile b/Dockerfile index a4216729..51f65e1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ FROM alpine:3.15 AS builder ARG PLEX_PG_SANITIZE ENV PLEX_PG_SANITIZE=${PLEX_PG_SANITIZE} -# Install build dependencies +# Install build dependencies – includes sanitizer runtime libraries RUN apk add --no-cache \ build-base \ sqlite-dev \ @@ -18,6 +18,8 @@ RUN apk add --no-cache \ # Verify musl version matches Plex (1.2.2) RUN /lib/ld-musl-*.so.1 --version 2>&1 | head -2 +# ... rest of builder stage unchanged ... + WORKDIR /build # Install Rust toolchain @@ -51,12 +53,15 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ FROM linuxserver/plex:latest # Install PostgreSQL client for health checks, sqlite3 for schema fixes, -# python3 for data migration, gdb for debugging +# python3 for data migration, gdb for debugging, and conditional sanitizer runtime libraries +ARG PLEX_PG_SANITIZE RUN apt-get update && apt-get install -y --no-install-recommends \ postgresql-client \ sqlite3 \ python3 \ gdb \ + $(if [ "$PLEX_PG_SANITIZE" = "address" ]; then echo "libasan8"; fi) \ + $(if [ "$PLEX_PG_SANITIZE" = "thread" ]; then echo "libtsan2"; fi) \ && rm -rf /var/lib/apt/lists/* # NOTE: Do NOT set LANG/LC_ALL/CHARSET here — Plex's bundled musl+boost::locale @@ -118,54 +123,19 @@ RUN if [ -f /etc/s6-overlay/s6-rc.d/init-plex-claim/run ]; then \ echo "Patched init-plex-claim for PostgreSQL shim"; \ fi -# Keep upstream CrashUploader binary. -# With SIGCHLD forced to SIG_IGN, child exits should no longer destabilize Plex. - # s6 finish script — defense-in-depth for the BindAddrInUseException crash loop. -# -# PRIMARY FIX: PLEX_PG_SUPPRESS_DAEMON=1 injected below keeps PMS in the -# foreground so s6 never sees the run script exit during normal startup. -# This finish script is a safety net for the case where daemon suppression is -# disabled (PLEX_PG_SUPPRESS_DAEMON=0) or fails. -# -# HOW THE CRASH LOOP WORKS WITHOUT THE PRIMARY FIX: -# PMS calls daemon() → fork() → parent exits → s6 sees its watched PID exit -# → s6 runs finish + restarts → new PMS tries to bind 32400 → the re-exec'd -# child from the previous cycle still holds 32400 → BindAddrInUseException -# → SIGABRT → loop ~50 times. -# -# WHY THE OLD FINISH SCRIPT DID NOT WORK: -# s6-overlay v3 kills the finish script after S6_KILL_FINISH_MAXTIME ms -# (default 5000ms). The old script's `while pgrep ... do sleep 5; done` -# loop is killed on its first iteration. Also, `nc -z localhost 32400` -# races — the re-exec'd child may not have bound 32400 yet. -# -# THIS finish script sets a 30-second timeout file and polls at 1s intervals -# so s6 does not kill it before it can detect the child. It exits 125 to -# signal s6 that it should not restart immediately (s6-overlay v3: exit codes -# >= 125 in the finish script suppress the automatic restart). RUN printf '#!/bin/bash\n# Exit code 125 tells s6-supervise not to restart the service.\n# See: https://skarnet.org/software/s6/s6-supervise.html\nexit_code=${1:-0}\nif [ "${exit_code}" = "0" ]; then\n deadline=30\n elapsed=0\n while [ $elapsed -lt $deadline ]; do\n if pgrep -x "Plex Media Server" >/dev/null 2>&1; then\n echo "[plex-pg] PMS re-exec child still running (${elapsed}s), suppressing restart"\n sleep 1\n elapsed=$((elapsed+1))\n else\n break\n fi\n done\n if pgrep -x "Plex Media Server" >/dev/null 2>&1; then\n echo "[plex-pg] PMS child still alive after ${deadline}s — suppressing restart, s6 will retry"\n exit 125\n fi\nfi\n' \ > /etc/s6-overlay/s6-rc.d/svc-plex/finish && \ chmod +x /etc/s6-overlay/s6-rc.d/svc-plex/finish && \ printf '30000\n' > /etc/s6-overlay/s6-rc.d/svc-plex/finish-timeout -# Inject shim env into the upstream svc-plex run script and wrap PMS -# with subreaper to prevent the BindAddrInUseException crash loop. -# -# PMS does vfork+execve to re-exec itself during startup: the parent exits -# while the child takes over on port 32400. s6 watches the parent PID, sees -# it exit, and immediately restarts PMS — but the child still holds port -# 32400, causing BindAddrInUseException → SIGABRT in a crash loop. -# -# FIX: subreaper sets PR_SET_CHILD_SUBREAPER, so the re-exec'd child is -# reparented to subreaper (not PID 1). subreaper waits for ALL descendants -# before exiting — s6 never sees a premature death. +# Inject shim env into the upstream svc-plex run script and wrap PMS with subreaper RUN sed -i '/export PLEX_MEDIA_SERVER_INFO_PLATFORM_VERSION/a\ arch="$(uname -m)"\ \nif [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then\ \n export OPENSSL_armcap="${PLEX_PG_OPENSSL_ARMCAP:-0}"\ \nfi\ -\nexport LD_LIBRARY_PATH="/usr/lib/plexmediaserver/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\ +\nexport LD_LIBRARY_PATH="/usr/local/lib/plex-postgresql:/usr/lib/plexmediaserver/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\ \nexport LD_PRELOAD="/usr/local/lib/plex-postgresql/db_interpose_pg.so"\ ' /etc/s6-overlay/s6-rc.d/svc-plex/run && \ sed -i 's|"/usr/lib/plexmediaserver/Plex Media Server"|/usr/local/bin/subreaper "/usr/lib/plexmediaserver/Plex Media Server"|g' \ diff --git a/VERSION b/VERSION index 085c0f26..5bdcf5c3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.14 +1.3.15 diff --git a/scripts/migrate_lib.sh b/scripts/migrate_lib.sh index e4cf282e..2823d993 100644 --- a/scripts/migrate_lib.sh +++ b/scripts/migrate_lib.sh @@ -75,15 +75,15 @@ check_and_migrate() { # Check if PostgreSQL already has data (check multiple tables, not just metadata_items) local pg_count=$(psql -t -c "SELECT COUNT(*) FROM $PG_SCHEMA.metadata_items;" 2>/dev/null | tr -d ' ' || echo "0") local pg_sections=$(psql -t -c "SELECT COUNT(*) FROM $PG_SCHEMA.library_sections;" 2>/dev/null | tr -d ' ' || echo "0") - local pg_accounts=$(psql -t -c "SELECT COUNT(*) FROM $PG_SCHEMA.accounts;" 2>/dev/null | tr -d ' ' || echo "0") + # Do NOT check accounts – it's always created as bootstrap data. local pg_has_data=0 - if [[ "$pg_count" -gt 0 ]] || [[ "$pg_sections" -gt 0 ]] || [[ "$pg_accounts" -gt 0 ]]; then + if [[ "$pg_count" -gt 0 ]] || [[ "$pg_sections" -gt 0 ]]; then pg_has_data=1 fi if [[ "$pg_has_data" -eq 1 ]]; then - echo -e "${YELLOW}PostgreSQL already has data (metadata_items=$pg_count, library_sections=$pg_sections, accounts=$pg_accounts).${NC}" - + echo -e "${YELLOW}PostgreSQL already has data (metadata_items=$pg_count, library_sections=$pg_sections).${NC}" + if [[ "$MIGRATION_INTERACTIVE" == "1" ]]; then echo "" echo "Options:"