- Multi-stage Dockerfile: discover patterns from PMS binary (capstone), compile .so with zig (musl), layer onto lscr.io/linuxserver/plex - Uses LD_PRELOAD instead of patchelf (which corrupts Plex's musl loader) - Auto-discovery: broad structural patterns with string-anchored fallback (//feature) and relationship-based fallback (BITSET_REF within BS_INIT) - hook.cpp uses __has_include for generated patterns with hardcoded fallbacks - Custom wrapper.sh (no traffic_logger preload) - Vendored Freeloader source (github.com/authrequest/Freeloader, AGPL-3.0) - Removed stale plexmediaserver_crack.so binary - Supports Plex 1.43.3+ (verified against 1.43.2 and 1.43.3)
552 lines
24 KiB
Bash
552 lines
24 KiB
Bash
#!/usr/bin/env bash
|
|
# docker/plex-docker-patch.sh
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
#
|
|
# Patch a running Plex Media Server container in place.
|
|
#
|
|
# ── Overview ──────────────────────────────────────────────────────────────
|
|
# Same LD_PRELOAD mechanism as the Dockerfile-based approach: the s6
|
|
# `svc-plex` `run` file is rewritten to exec the in-container wrapper,
|
|
# which sets LD_PRELOAD last and execs the PMS binary. The .so's
|
|
# constructor (src/main.cpp) calls unsetenv, so glibc helper children
|
|
# (Tuner, Script Host, transcoders) are unaffected.
|
|
#
|
|
# No `docker build`, no container recreate, original image untouched.
|
|
# Revertible via `uninstall` (a `.orig` copy of the s6 `run` file is kept).
|
|
#
|
|
# ── Usage ──────────────────────────────────────────────────────────────────
|
|
# plex-docker-patch.sh [flags] <subcommand> [container-name]
|
|
#
|
|
# Subcommands:
|
|
# install [name] Apply the patch in place (default if omitted)
|
|
# uninstall [name] Restore the s6 run file from its .orig
|
|
# status [name] Report whether the patch is active
|
|
# help Show usage
|
|
#
|
|
# Flags (can appear before or after the subcommand):
|
|
# --name <name> Container name (alternative to positional)
|
|
# --no-build Use existing build/plexmediaserver_crack.so; do not invoke build.sh
|
|
# --force-rebuild Delete build/plexmediaserver_crack.so and rebuild from scratch
|
|
# --dry-run Print the actions that would be taken without executing them
|
|
# --verbose, -v Trace every docker/build command to stderr before execution
|
|
# --quiet, -q Suppress non-essential output (only the final report)
|
|
# --version Print the script version and exit
|
|
#
|
|
# Container name defaults to "plex". Both plexinc/pms-docker and
|
|
# lscr.io/linuxserver/plex are auto-detected by reading the s6 run file:
|
|
# LSIO uses `s6-setuidgid abc`; plexinc does not.
|
|
#
|
|
# The .so is built locally via the project's build.sh, so a zig-capable
|
|
# toolchain is required on the host (or an existing build/plexmediaserver_crack.so
|
|
# is reused). See docs/DOCKER.md for the full guide.
|
|
#
|
|
# ── Requirements ──────────────────────────────────────────────────────────
|
|
# - docker on PATH and accessible to the current user
|
|
# - bash 4+ (or bash 3.2+ on macOS; arrays + $'...' are used)
|
|
# - curl + xz-utils (for build.sh) if no prebuilt .so
|
|
#
|
|
# ── Idempotency ────────────────────────────────────────────────────────────
|
|
# install: safe to re-run. The .orig is preserved across re-installs, the
|
|
# .so and wrapper are overwritten with the latest build, the
|
|
# run file is rewritten, the container is restarted.
|
|
# uninstall: safe to re-run. The run file is re-copied from .orig each time.
|
|
# status: read-only, always safe.
|
|
#
|
|
# ── Exit codes ─────────────────────────────────────────────────────────────
|
|
# 0 success
|
|
# 1 runtime error (docker missing, container not found, install failed)
|
|
# 2 usage error (unknown subcommand or flag)
|
|
#
|
|
# ── Design notes ──────────────────────────────────────────────────────────
|
|
# - `set -euo pipefail` for fail-fast. All transient failures must surface
|
|
# as non-zero exits so callers can detect them.
|
|
# - `docker exec sh -c '...' _ "${var}"` is the only safe pattern for
|
|
# passing host-side paths into the container's shell: single-quote the
|
|
# command so the host bash does NOT interpolate, then pass the path as
|
|
# a positional arg. Without this, a path containing `;` or `$()` would
|
|
# be a command-injection vector. (Currently safe because all paths come
|
|
# from a hardcoded candidate list, but the safe pattern is enforced
|
|
# throughout for future-proofing.)
|
|
# - PMS PID lookup scans /proc/*/comm (not ps -ef | grep), which avoids
|
|
# the "Plex" pattern matching the scanning command itself, and works
|
|
# regardless of which process-listing tools the container provides.
|
|
# - All destructive operations route through `run`, which respects
|
|
# --dry-run and --verbose. This is the single place to look to see what
|
|
# side effects the script has.
|
|
# - Logging: stdout is for human-readable status and the final report;
|
|
# stderr is for warnings, errors, --verbose traces, and --dry-run plans.
|
|
# This makes the script CI-friendly (pipe stdout, capture stderr).
|
|
# - `DEBUG=1` env var enables `set -x` for full command tracing.
|
|
#
|
|
# ── Version ────────────────────────────────────────────────────────────────
|
|
SCRIPT_VERSION='1.0.0'
|
|
|
|
set -euo pipefail
|
|
[[ -n "${DEBUG:-}" ]] && set -x
|
|
|
|
# ── Paths / constants ─────────────────────────────────────────────────────
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
# In-container paths (must match what the Dockerfiles copy to).
|
|
SO_PATH_INSIDE="/usr/lib/plexmediaserver/lib/plexmediaserver_crack.so"
|
|
WRAPPER_PATH_INSIDE="/usr/lib/plexmediaserver/plex-crack-wrapper.sh"
|
|
PMS_COMM_NAME="Plex Media Server" # prctl(PR_SET_NAME) sets this
|
|
|
|
# s6-overlay v3 / v2 candidate paths, in preference order. If the upstream
|
|
# image's s6 layout changes, this is the single place to update.
|
|
S6_RUN_CANDIDATES=(
|
|
"/etc/s6-overlay/s6-rc.d/svc-plex/run"
|
|
"/etc/services.d/plex/run"
|
|
)
|
|
|
|
# Operational defaults.
|
|
DEFAULT_CONTAINER="plex"
|
|
MAX_WAIT_ITERATIONS=30
|
|
WAIT_INTERVAL_SECONDS=2
|
|
PMS_HTTP_PORT=32400
|
|
|
|
# ── Arg-parser state ──────────────────────────────────────────────────────
|
|
SUBCOMMAND=""
|
|
CONTAINER_NAME=""
|
|
DRY_RUN=false
|
|
VERBOSE=false
|
|
QUIET=false
|
|
SKIP_BUILD=false
|
|
FORCE_REBUILD=false
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────────
|
|
# stdout -> human-readable status, success messages, final report
|
|
# stderr -> warnings, errors, --verbose traces, --dry-run plans
|
|
log() { [ "${QUIET}" != "true" ] && printf '%s\n' "$*"; }
|
|
warn() { printf 'WARNING: %s\n' "$*" >&2; }
|
|
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
|
trace() { [ "${VERBOSE}" = "true" ] && printf '+ %s\n' "$*" >&2 || true; }
|
|
|
|
# ── Command runner: respects --dry-run and --verbose ──────────────────────
|
|
# All destructive operations go through this wrapper. In dry-run mode
|
|
# the command is echoed to stderr and skipped. In verbose mode the
|
|
# command is echoed to stderr before execution. Stdout is captured by
|
|
# the caller as usual.
|
|
run() {
|
|
if [ "${DRY_RUN}" = "true" ]; then
|
|
printf '[dry-run]'
|
|
local arg
|
|
for arg in "$@"; do
|
|
printf ' %s' "${arg}"
|
|
done
|
|
printf '\n' >&2
|
|
else
|
|
trace "$*"
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
# ── Usage ─────────────────────────────────────────────────────────────────
|
|
print_usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [flags] <subcommand> [container-name]
|
|
|
|
Subcommands:
|
|
install [name] Apply the patch in place (default)
|
|
uninstall [name] Revert the s6 run file from its .orig
|
|
status [name] Report whether the patch is active
|
|
help Show this message
|
|
|
|
Flags (can appear before or after the subcommand):
|
|
--name <name> Container name (alternative to positional)
|
|
--no-build Use existing build/plexmediaserver_crack.so; do not invoke build.sh
|
|
--force-rebuild Delete build/plexmediaserver_crack.so and rebuild from scratch
|
|
--dry-run Print the actions that would be taken without executing them
|
|
--verbose, -v Trace every docker/build command to stderr
|
|
--quiet, -q Suppress non-essential output (only the final report)
|
|
--version Print the script version and exit
|
|
|
|
Container name defaults to "${DEFAULT_CONTAINER}".
|
|
|
|
Examples:
|
|
$(basename "$0") install # default container
|
|
$(basename "$0") install my-plex # custom name
|
|
$(basename "$0") --name my-plex install # flag form
|
|
$(basename "$0") install --dry-run # preview only
|
|
$(basename "$0") --verbose status my-plex # trace every docker call
|
|
$(basename "$0") uninstall my-plex
|
|
|
|
The script auto-detects the base image (plexinc vs linuxserver) by
|
|
reading the s6 svc-plex run file inside the container. The same .so and
|
|
docker/wrapper.sh are used in both cases; only the run file content
|
|
differs (LSIO preserves s6-setuidgid abc).
|
|
EOF
|
|
}
|
|
|
|
# ── Arg parsing ───────────────────────────────────────────────────────────
|
|
parse_args() {
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
install|uninstall|status)
|
|
if [ -n "${SUBCOMMAND}" ]; then
|
|
die "subcommand already specified: ${SUBCOMMAND}"
|
|
fi
|
|
SUBCOMMAND="$1"
|
|
shift
|
|
;;
|
|
help|-h|--help) print_usage; exit 0 ;;
|
|
--name) [ "$#" -ge 2 ] || die "--name requires an argument"
|
|
CONTAINER_NAME="$2"; shift 2 ;;
|
|
--name=*) CONTAINER_NAME="${1#--name=}"; shift ;;
|
|
--no-build) SKIP_BUILD=true; shift ;;
|
|
--force-rebuild) FORCE_REBUILD=true; shift ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
--verbose|-v) VERBOSE=true; shift ;;
|
|
--quiet|-q) QUIET=true; shift ;;
|
|
--version) printf '%s\n' "${SCRIPT_VERSION}"; exit 0 ;;
|
|
--) shift; break ;;
|
|
-*) die "unknown flag: $1 (try --help)" ;;
|
|
*)
|
|
if [ -z "${CONTAINER_NAME}" ]; then
|
|
CONTAINER_NAME="$1"
|
|
else
|
|
die "unexpected positional argument: $1"
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
SUBCOMMAND="${SUBCOMMAND:-install}"
|
|
CONTAINER_NAME="${CONTAINER_NAME:-${DEFAULT_CONTAINER}}"
|
|
|
|
# Mutual-exclusion checks.
|
|
if [ "${SKIP_BUILD}" = "true" ] && [ "${FORCE_REBUILD}" = "true" ]; then
|
|
die "--no-build and --force-rebuild are mutually exclusive"
|
|
fi
|
|
if [ "${QUIET}" = "true" ] && [ "${VERBOSE}" = "true" ]; then
|
|
die "--quiet and --verbose are mutually exclusive"
|
|
fi
|
|
|
|
# Make state available to subcommand functions.
|
|
export SUBCOMMAND CONTAINER_NAME DRY_RUN VERBOSE QUIET SKIP_BUILD FORCE_REBUILD
|
|
}
|
|
|
|
# ── Pre-flight checks ────────────────────────────────────────────────────
|
|
require_docker() {
|
|
command -v docker >/dev/null 2>&1 || die "docker not on PATH"
|
|
}
|
|
|
|
require_container_exists() {
|
|
require_docker
|
|
if ! docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
|
|
die "container '${CONTAINER_NAME}' not found. Start one with: docker run -d --name ${CONTAINER_NAME} ..."
|
|
fi
|
|
}
|
|
|
|
require_container_running() {
|
|
require_container_exists
|
|
local state
|
|
state="$(docker inspect --format '{{.State.Running}}' "${CONTAINER_NAME}" 2>/dev/null || echo unknown)"
|
|
if [ "${state}" != "true" ]; then
|
|
die "container '${CONTAINER_NAME}' is not running (state: ${state}). Start it with: docker start ${CONTAINER_NAME}"
|
|
fi
|
|
}
|
|
|
|
# ── Detection ─────────────────────────────────────────────────────────────
|
|
|
|
# Echoes the s6 svc-plex run file path on stdout, or returns 1.
|
|
# Two candidates are tried in preference order: s6-overlay v3 path, then
|
|
# the legacy v2 path.
|
|
detect_run_script() {
|
|
local p
|
|
for p in "${S6_RUN_CANDIDATES[@]}"; do
|
|
if run docker exec -u root "${CONTAINER_NAME}" test -f "${p}" 2>/dev/null; then
|
|
printf '%s\n' "${p}"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Echoes one of: plexinc, linuxserver, unknown
|
|
# Detection: LSIO's run file content includes `s6-setuidgid abc`; plexinc's
|
|
# does not. If neither marker is found, return `unknown` (the install
|
|
# flow will refuse to proceed).
|
|
detect_base_image() {
|
|
local run_script="$1"
|
|
local content
|
|
content="$(run docker exec -u root "${CONTAINER_NAME}" cat "${run_script}" 2>/dev/null || true)"
|
|
if printf '%s' "${content}" | grep -q 's6-setuidgid abc'; then
|
|
printf 'linuxserver\n'
|
|
elif printf '%s' "${content}" | grep -qE 'Plex Media Server|start\.sh|with-contenv'; then
|
|
printf 'plexinc\n'
|
|
else
|
|
printf 'unknown\n'
|
|
fi
|
|
}
|
|
|
|
# Echoes the PMS PID on stdout, or empty.
|
|
# Strategy: scan /proc/*/comm for the PMS comm name (set via
|
|
# prctl(PR_SET_NAME)). This avoids the "ps -ef | grep | grep -v grep"
|
|
# pattern, which has a tendency to match its own command line, and works
|
|
# regardless of which process-listing tools are in the container.
|
|
find_pms_pid() {
|
|
run docker exec "${CONTAINER_NAME}" sh -c '
|
|
for d in /proc/[0-9]*; do
|
|
[ -r "$d/comm" ] || continue
|
|
if [ "$(cat "$d/comm" 2>/dev/null)" = "$1" ]; then
|
|
basename "$d"
|
|
exit 0
|
|
fi
|
|
done
|
|
exit 1
|
|
' _ "${PMS_COMM_NAME}" 2>/dev/null || true
|
|
}
|
|
|
|
# ── Build ─────────────────────────────────────────────────────────────────
|
|
build_so() {
|
|
local so_path="${PROJECT_ROOT}/build/plexmediaserver_crack.so"
|
|
|
|
if [ "${FORCE_REBUILD}" = "true" ] && [ -f "${so_path}" ]; then
|
|
log "[*] --force-rebuild: removing existing .so"
|
|
run rm -f "${so_path}"
|
|
fi
|
|
|
|
if [ "${SKIP_BUILD}" = "true" ]; then
|
|
if [ ! -f "${so_path}" ]; then
|
|
die "--no-build specified but ${so_path} does not exist. Build it first with: bash build.sh"
|
|
fi
|
|
log "[*] --no-build: reusing existing .so (build.sh not invoked)"
|
|
return 0
|
|
fi
|
|
|
|
if [ -f "${so_path}" ]; then
|
|
log "[*] Reusing existing .so (rm it or pass --force-rebuild to rebuild)"
|
|
return 0
|
|
fi
|
|
|
|
log "[*] Building .so via build.sh (this may take 1-2 min on first run)..."
|
|
( cd "${PROJECT_ROOT}" && run bash build.sh )
|
|
}
|
|
|
|
# ── Filesystem ops ────────────────────────────────────────────────────────
|
|
# Build the new s6 run file content locally and docker cp it in. We do
|
|
# this locally (rather than heredoc-over-docker-exec) to avoid quoting
|
|
# hell and command-injection risk.
|
|
write_new_run_file() {
|
|
local base="$1" out="$2"
|
|
{
|
|
printf '#!/usr/bin/with-contenv bash\n'
|
|
if [ "${base}" = "linuxserver" ]; then
|
|
printf 'exec s6-setuidgid abc %s\n' "${WRAPPER_PATH_INSIDE}"
|
|
else
|
|
printf 'exec %s\n' "${WRAPPER_PATH_INSIDE}"
|
|
fi
|
|
} > "${out}"
|
|
chmod 0755 "${out}"
|
|
}
|
|
|
|
# Back up the s6 run file to .orig. Idempotent: if .orig exists, leave it.
|
|
# Uses the safe `sh -c '...' _ "${path}"` pattern: single-quoted command
|
|
# + positional arg, so the host bash never interpolates the path.
|
|
backup_run_file() {
|
|
local run_script="$1"
|
|
if run docker exec -u root "${CONTAINER_NAME}" test -f "${run_script}.orig" 2>/dev/null; then
|
|
log "[*] ${run_script}.orig already present, leaving it"
|
|
return 0
|
|
fi
|
|
run docker exec -u root "${CONTAINER_NAME}" \
|
|
sh -c 'cp "$1" "$1".orig' _ "${run_script}"
|
|
log "[*] Backed up ${run_script} -> ${run_script}.orig"
|
|
}
|
|
|
|
# Copy the .so and wrapper into the container and chmod the wrapper.
|
|
copy_artifacts() {
|
|
local so_host="${PROJECT_ROOT}/build/plexmediaserver_crack.so"
|
|
local wrapper_host="${SCRIPT_DIR}/wrapper.sh"
|
|
|
|
if [ ! -f "${so_host}" ]; then
|
|
die "${so_host} does not exist. Build it first with: bash build.sh"
|
|
fi
|
|
if [ ! -f "${wrapper_host}" ]; then
|
|
die "${wrapper_host} does not exist. Re-clone the project or restore docker/wrapper.sh."
|
|
fi
|
|
|
|
log "[*] Copying .so and wrapper into the container..."
|
|
run docker cp "${so_host}" "${CONTAINER_NAME}:${SO_PATH_INSIDE}"
|
|
run docker cp "${wrapper_host}" "${CONTAINER_NAME}:${WRAPPER_PATH_INSIDE}"
|
|
run docker exec -u root "${CONTAINER_NAME}" chmod 0755 "${WRAPPER_PATH_INSIDE}"
|
|
}
|
|
|
|
# ── Verification ───────────────────────────────────────────────────────────
|
|
# Polls PMS /identity until it returns 200 or the timeout expires.
|
|
wait_for_pms_ready() {
|
|
log "[*] Waiting for PMS /identity (up to $((MAX_WAIT_ITERATIONS * WAIT_INTERVAL_SECONDS))s)..."
|
|
local i code
|
|
for i in $(seq 1 "${MAX_WAIT_ITERATIONS}"); do
|
|
code="$(curl -fsS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PMS_HTTP_PORT}/identity" 2>/dev/null || echo 000)"
|
|
if [ "${code}" = "200" ]; then
|
|
log "[*] PMS is up (HTTP 200)."
|
|
return 0
|
|
fi
|
|
sleep "${WAIT_INTERVAL_SECONDS}"
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Confirms the .so is mapped into the PMS process.
|
|
verify_so_mapped() {
|
|
local pms_pid="$1"
|
|
if [ -z "${pms_pid}" ]; then
|
|
warn "could not find PMS pid inside container"
|
|
return 1
|
|
fi
|
|
if run docker exec "${CONTAINER_NAME}" grep -F plexmediaserver_crack.so "/proc/${pms_pid}/maps" >/dev/null 2>&1; then
|
|
log "[*] OK: plexmediaserver_crack.so is mapped into PMS (pid ${pms_pid})"
|
|
return 0
|
|
fi
|
|
warn "PMS running (pid ${pms_pid}) but .so is NOT in /proc/${pms_pid}/maps"
|
|
warn " This usually means PMS exited 127 (loader failure). Check: docker logs ${CONTAINER_NAME} | tail -50"
|
|
return 1
|
|
}
|
|
|
|
# ── Subcommands ──────────────────────────────────────────────────────────
|
|
do_install() {
|
|
require_container_running
|
|
log "[*] Container: ${CONTAINER_NAME}"
|
|
|
|
local run_script
|
|
if ! run_script="$(detect_run_script)"; then
|
|
die "could not find s6 svc-plex run file in container. Tried: ${S6_RUN_CANDIDATES[*]}"
|
|
fi
|
|
log "[*] s6 run file: ${run_script}"
|
|
|
|
local base
|
|
base="$(detect_base_image "${run_script}")"
|
|
case "${base}" in
|
|
linuxserver) log "[*] Detected base: lscr.io/linuxserver/plex (s6-setuidgid abc will be preserved)" ;;
|
|
plexinc) log "[*] Detected base: plexinc/pms-docker" ;;
|
|
*)
|
|
die "could not detect base image from s6 run file content. Please file an issue with the file contents."
|
|
;;
|
|
esac
|
|
|
|
build_so
|
|
copy_artifacts
|
|
backup_run_file "${run_script}"
|
|
|
|
# Write the new run file locally and copy it in. The mktemp + trap
|
|
# pattern ensures we never leak the temp file, even on error paths.
|
|
local tmp_run
|
|
tmp_run="$(mktemp)"
|
|
trap 'rm -f "${tmp_run}"' EXIT
|
|
write_new_run_file "${base}" "${tmp_run}"
|
|
run docker cp "${tmp_run}" "${CONTAINER_NAME}:${run_script}"
|
|
rm -f "${tmp_run}"
|
|
trap - EXIT
|
|
|
|
log "[*] Restarting ${CONTAINER_NAME} (s6 will exec the new run file)..."
|
|
run docker restart "${CONTAINER_NAME}" >/dev/null
|
|
|
|
if ! wait_for_pms_ready; then
|
|
warn "PMS did not return 200 within $((MAX_WAIT_ITERATIONS * WAIT_INTERVAL_SECONDS))s. Check: docker logs ${CONTAINER_NAME} | tail -50"
|
|
fi
|
|
|
|
local pms_pid
|
|
pms_pid="$(find_pms_pid)"
|
|
verify_so_mapped "${pms_pid}"
|
|
|
|
cat <<EOF
|
|
|
|
Patch applied. For full verification (all 14 feature bits ON):
|
|
docker cp ${PROJECT_ROOT}/scripts/readbitset.py ${CONTAINER_NAME}:/tmp/readbitset.py
|
|
docker exec -u root ${CONTAINER_NAME} python3 /tmp/readbitset.py ${pms_pid:-<PMS_PID>}
|
|
|
|
To revert: $(basename "$0") uninstall ${CONTAINER_NAME}
|
|
To re-check: $(basename "$0") status ${CONTAINER_NAME}
|
|
EOF
|
|
}
|
|
|
|
do_uninstall() {
|
|
require_container_running
|
|
|
|
local run_script
|
|
if ! run_script="$(detect_run_script)"; then
|
|
die "could not find s6 svc-plex run file. Tried: ${S6_RUN_CANDIDATES[*]}"
|
|
fi
|
|
|
|
if ! run docker exec -u root "${CONTAINER_NAME}" test -f "${run_script}.orig" 2>/dev/null; then
|
|
die "no ${run_script}.orig found -- the patch may not be installed, or the .orig was deleted. Manual restore: docker cp <upstream-image>:${run_script} ${CONTAINER_NAME}:${run_script}"
|
|
fi
|
|
|
|
log "[*] Restoring ${run_script} from .orig..."
|
|
run docker exec -u root "${CONTAINER_NAME}" \
|
|
sh -c 'cp "$1" "$2"' _ "${run_script}.orig" "${run_script}"
|
|
run docker exec -u root "${CONTAINER_NAME}" chmod 0755 "${run_script}"
|
|
|
|
log "[*] Restarting ${CONTAINER_NAME}..."
|
|
run docker restart "${CONTAINER_NAME}" >/dev/null
|
|
|
|
cat <<EOF
|
|
|
|
Patch removed. PMS is back to upstream behavior. Optional cleanup:
|
|
docker exec -u root ${CONTAINER_NAME} rm -f ${SO_PATH_INSIDE} ${WRAPPER_PATH_INSIDE}
|
|
docker exec -u root ${CONTAINER_NAME} rm -f ${run_script}.orig
|
|
EOF
|
|
}
|
|
|
|
do_status() {
|
|
require_container_exists
|
|
|
|
local run_script
|
|
if ! run_script="$(detect_run_script)"; then
|
|
warn "s6 svc-plex run file not found (tried: ${S6_RUN_CANDIDATES[*]})"
|
|
return 1
|
|
fi
|
|
|
|
local run_content has_orig has_so has_wrapper pms_pid
|
|
run_content="$(run docker exec -u root "${CONTAINER_NAME}" cat "${run_script}" 2>/dev/null || true)"
|
|
if run docker exec -u root "${CONTAINER_NAME}" test -f "${run_script}.orig" 2>/dev/null; then
|
|
has_orig="yes"
|
|
else
|
|
has_orig="no"
|
|
fi
|
|
if run docker exec -u root "${CONTAINER_NAME}" test -f "${SO_PATH_INSIDE}" 2>/dev/null; then
|
|
has_so="yes"
|
|
else
|
|
has_so="no"
|
|
fi
|
|
if run docker exec -u root "${CONTAINER_NAME}" test -x "${WRAPPER_PATH_INSIDE}" 2>/dev/null; then
|
|
has_wrapper="yes"
|
|
else
|
|
has_wrapper="no"
|
|
fi
|
|
pms_pid="$(find_pms_pid)"
|
|
|
|
log "Container: ${CONTAINER_NAME}"
|
|
log " s6 run file: ${run_script}"
|
|
log " .orig present: ${has_orig}"
|
|
log " .so present: ${has_so} (${SO_PATH_INSIDE})"
|
|
log " wrapper: ${has_wrapper} (${WRAPPER_PATH_INSIDE})"
|
|
log " PMS pid: ${pms_pid:-<not running>}"
|
|
log " run file content:"
|
|
printf '%s\n' "${run_content}" | sed 's/^/ /'
|
|
log ""
|
|
|
|
if printf '%s' "${run_content}" | grep -q 'plex-crack-wrapper.sh'; then
|
|
log "[*] PATCH IS ACTIVE (s6 run file points to the wrapper)."
|
|
if [ -n "${pms_pid}" ] && run docker exec "${CONTAINER_NAME}" grep -F plexmediaserver_crack.so "/proc/${pms_pid}/maps" >/dev/null 2>&1; then
|
|
log "[*] .so is mapped into PMS (pid ${pms_pid})."
|
|
else
|
|
warn "PMS running (pid ${pms_pid:-?}) but .so is NOT in its maps; check docker logs."
|
|
fi
|
|
else
|
|
log "[*] PATCH IS NOT ACTIVE (s6 run file does not point to the wrapper)."
|
|
fi
|
|
}
|
|
|
|
# ── Main ──────────────────────────────────────────────────────────────────
|
|
parse_args "$@"
|
|
case "${SUBCOMMAND}" in
|
|
install) do_install ;;
|
|
uninstall) do_uninstall ;;
|
|
status) do_status ;;
|
|
esac
|