#!/bin/bash # Native Debian 12 / Ubuntu 24.04+ Intel DRM worker. No secrets in argv/history. set -euo pipefail set +x umask 077 task_source=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) worker_name= peertube_url= token_fd= canonical_origin= tailscale_peer=TAILSCALE_IP while (($#)); do case "$1" in --name) worker_name=${2:?}; shift 2 ;; --peertube-url) peertube_url=${2:?}; shift 2 ;; --tailscale-peer) tailscale_peer=${2:?}; shift 2 ;; --canonical-origin) canonical_origin=${2:?}; shift 2 ;; --registration-token-fd) token_fd=${2:?}; shift 2 ;; *) echo 'Usage: install-peertube-worker.sh --name HOST --peertube-url URL [--tailscale-peer IP] [--registration-token-fd N]' >&2; exit 2 ;; esac done [[ $EUID -eq 0 && $worker_name =~ ^[a-zA-Z0-9._-]+$ && $peertube_url =~ ^https?:// ]] || { echo 'Root, valid name and URL required' >&2; exit 2; } exec 9>/run/lock/pt-worker-install.lock flock -n 9 || { echo 'Another installation is running' >&2; exit 1; } [[ $(ps -p 1 -o comm=) == systemd ]] || { echo 'systemd is required; inspect WSL init before onboarding' >&2; exit 1; } [[ -c /dev/dri/renderD128 ]] || { echo 'No Intel DRM render device; /dev/dxg alone is not this Linux QSV path. No packages or services changed.' >&2; exit 1; } [[ $(cat /sys/class/drm/renderD128/device/vendor) == 0x8086 ]] || { echo 'This installer needs an Intel GPU; NVIDIA needs another hardware adapter' >&2; exit 1; } [[ $(basename "$(readlink -f /sys/class/drm/renderD128/device/driver)") == i915 || $(basename "$(readlink -f /sys/class/drm/renderD128/device/driver)") == xe ]] || { echo 'Intel DRM driver unavailable' >&2; exit 1; } command -v tailscale >/dev/null || { echo 'Tailscale must be joined already' >&2; exit 1; } tailscale ping --c=1 --timeout=5s "$tailscale_peer" >/dev/null canonical_origin=$(python3 - "$peertube_url" "$canonical_origin" <<'PY' import json, sys, urllib.request from urllib.parse import urlsplit with urllib.request.urlopen(sys.argv[1].rstrip('/')+'/api/v1/config', timeout=10) as r: c=json.load(r); v=c['serverVersion'] if v != '7.3.0': raise SystemExit('This adapter is verified against PeerTube 7.3.0; inspect a different version before installing') if sys.version_info < (3,11): raise SystemExit('Python >=3.11 required') origin=sys.argv[2] if not origin: logos=[x['fileUrl'] for x in c['instance']['logo'] if x.get('isFallback')] if not logos: raise SystemExit('Supply --canonical-origin: no fallback asset URL identifies the instance origin') u=urlsplit(logos[0]);origin=u.scheme+'://'+u.netloc print(origin.rstrip('/')) PY ) if [[ -f /etc/pt-worker/worker.json ]]; then python3 - "$worker_name" "$peertube_url" <<'PY' import json, sys c=json.load(open('/etc/pt-worker/worker.json')) if c['name'] != sys.argv[1] or c['peertube_url'].rstrip('/') != sys.argv[2].rstrip('/'): raise SystemExit('Existing name/URL differs: preserve registration and inspect before changing it') PY fi if systemctl is-active --quiet pt-worker.service; then /usr/local/bin/pt-worker drain for _ in $(seq 1 120); do if /usr/local/bin/pt-worker status | python3 -c 'import json,sys;sys.exit(bool(json.load(sys.stdin).get("runner_running")))'; then break; fi sleep 5 done /usr/local/bin/pt-worker status | python3 -c 'import json,sys;sys.exit(bool(json.load(sys.stdin).get("runner_running")))' || { echo 'Active jobs still draining; installer stopped without overwriting code' >&2; exit 1; } systemctl stop pt-worker.service fi if [[ -d /opt/pt-worker ]]; then mkdir -p /var/backups/pt-worker task_backup=/var/backups/pt-worker/$(date -u +%Y%m%dT%H%M%SZ).tgz tar -czf "$task_backup" /opt/pt-worker /etc/pt-worker /var/lib/pt-worker/.config chmod 600 "$task_backup" fi . /etc/os-release case "$ID" in debian|ubuntu) ;; *) echo 'Only Debian/Ubuntu package sets verified' >&2; exit 1;; esac apt-get update # Intel media VA driver includes Alder Lake support; do not change kernel/perf policies. task_media_package=intel-media-va-driver if apt-cache show intel-media-va-driver-non-free >/dev/null 2>&1; then task_media_package=intel-media-va-driver-non-free; fi apt-get install -y ffmpeg vainfo "$task_media_package" intel-gpu-tools python3 curl ca-certificates xz-utils pciutils python3 - <<'PY' import re, subprocess s=subprocess.check_output(['/usr/bin/ffmpeg','-version'],text=True) m=re.search(r'ffmpeg version (\d+)\.(\d+)',s) if not m or tuple(map(int,m.groups())) < (4,3): raise SystemExit('FFmpeg >=4.3 required') subprocess.run(['/usr/bin/ffprobe','-version'],check=True,stdout=subprocess.DEVNULL) PY id pt-worker >/dev/null 2>&1 || useradd --system --create-home --home-dir /var/lib/pt-worker --shell /usr/sbin/nologin pt-worker for task_group in render video; do getent group "$task_group" >/dev/null && usermod -a -G "$task_group" pt-worker done task_render_group=$(stat -c %G /dev/dri/renderD128) [[ $task_render_group != root ]] || { echo 'Render device has root group; fix the host udev policy first' >&2; exit 1; } usermod -a -G "$task_render_group" pt-worker install -d -m 755 /opt/pt-worker install -d -m 750 -o root -g pt-worker /etc/pt-worker install -d -m 700 -o pt-worker -g pt-worker /var/lib/pt-worker/state for task_file in pt-worker.py ffmpeg-qsv.py observe.cjs ipc.cjs; do install -m 755 "$task_source/$task_file" "/opt/pt-worker/$task_file" done install -m 755 "$task_source/pt-worker" /usr/local/bin/pt-worker if [[ ! -x /opt/pt-worker/node/bin/node ]]; then task_tmp=$(mktemp -d) trap 'python3 -c '\''import shutil,sys;shutil.rmtree(sys.argv[1])'\'' "$task_tmp"' EXIT case "$(uname -m)" in x86_64) task_arch=x64;; *) echo 'Intel worker requires x86_64' >&2; exit 1;; esac curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt -o "$task_tmp/SHA256SUMS" task_archive=$(awk '$2 ~ /linux-x64.tar.xz$/ {print $2}' "$task_tmp/SHA256SUMS") [[ $task_archive =~ ^node-v22\.[0-9]+\.[0-9]+-linux-x64.tar.xz$ ]] || exit 1 curl -fsSL "https://nodejs.org/dist/latest-v22.x/$task_archive" -o "$task_tmp/$task_archive" (cd "$task_tmp"; awk -v file="$task_archive" '$2==file' SHA256SUMS | sha256sum -c -) mkdir -p /opt/pt-worker/node tar -xJf "$task_tmp/$task_archive" -C /opt/pt-worker/node --strip-components=1 fi export PATH=/opt/pt-worker/node/bin:$PATH node -e 'if (+process.versions.node.split(".")[0] < 22) process.exit(1)' install -d -m 755 /opt/pt-worker/runtime install -m 644 "$task_source/runtime-package.json" /opt/pt-worker/runtime/package.json install -m 644 "$task_source/runtime-package-lock.json" /opt/pt-worker/runtime/package-lock.json npm ci --prefix /opt/pt-worker/runtime --omit=optional --ignore-scripts --no-audit --no-fund [[ $(/opt/pt-worker/runtime/node_modules/.bin/peertube-runner --version) == 0.2.0 ]] || exit 1 if [[ ! -f /etc/pt-worker/worker.json ]]; then python3 - "$task_source/worker.example.json" "$worker_name" "$peertube_url" "$tailscale_peer" "$canonical_origin" <<'PY' import json,sys c=json.load(open(sys.argv[1]));c.update(name=sys.argv[2],peertube_url=sys.argv[3].rstrip('/'),tailscale_peer=sys.argv[4],canonical_origin=sys.argv[5],probe_url='http://'+sys.argv[4]+':9044') with open('/etc/pt-worker/worker.json','w') as f: json.dump(c,f,indent=2) PY fi chown root:pt-worker /etc/pt-worker/worker.json chmod 640 /etc/pt-worker/worker.json # Real hardware test as the actual service user, with its real device permissions. /usr/local/bin/pt-worker self-test || { echo 'QSV failed or media-engine evidence unavailable: not registering or starting' >&2; exit 1; } task_registered=$(runuser -u pt-worker -- python3 - <<'PY' import tomllib,json from pathlib import Path c=json.load(open('/etc/pt-worker/worker.json')); p=Path('/var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml') d=tomllib.loads(p.read_text()) if p.exists() else {} print(int(any(x['url'].rstrip('/')==c['peertube_url'] and x['runnerName']==c['name'] for x in d.get('registeredInstances',[])))) PY ) if [[ $task_registered == 0 ]]; then [[ ! -e /var/lib/pt-worker/state/enrollment.pending ]] || { echo 'Previous registration outcome is uncertain; inspect the named runner in PeerTube before retrying. No duplicate enrollment attempted.' >&2; exit 1; } # Registration requires a running server. concurrency=0 makes enrollment refuse jobs. install -d -m 700 -o pt-worker -g pt-worker /var/lib/pt-worker/.config/peertube-runner-nodejs/default if [[ -e /var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml ]]; then python3 - <<'PY' import tomllib from pathlib import Path p=Path('/var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml') c=tomllib.loads(p.read_text()) if c.get('registeredInstances'): raise SystemExit('Other registered instances exist: inspect before enrollment') PY fi cat > /var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml <<'TOML' [jobs] concurrency = 0 [ffmpeg] threads = 2 nice = 20 TOML chown pt-worker:pt-worker /var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml # Logs go to /dev/null during enrollment; the official CLI secret flag is deliberately avoided. runuser -u pt-worker -- env HOME=/var/lib/pt-worker PATH="$PATH" \ /opt/pt-worker/runtime/node_modules/.bin/peertube-runner server \ --enable-job vod-web-video-transcoding --enable-job vod-hls-transcoding --enable-job vod-audio-merge-transcoding >/dev/null 2>&1 & task_enroll_pid=$! trap 'kill "$task_enroll_pid" 2>/dev/null || true; [[ -z ${task_tmp:-} ]] || python3 -c '\''import shutil,sys;shutil.rmtree(sys.argv[1])'\'' "$task_tmp"' EXIT for _ in $(seq 1 30); do [[ -S /var/lib/pt-worker/.local/share/peertube-runner-nodejs/default/peertube-runner.sock ]] && break sleep 1 done if [[ -n $token_fd ]]; then [[ $token_fd =~ ^[0-9]+$ ]] || exit 2 touch /var/lib/pt-worker/state/enrollment.pending runuser -u pt-worker -- env HOME=/var/lib/pt-worker /opt/pt-worker/node/bin/node /opt/pt-worker/ipc.cjs register <&"$token_fd" else [[ -t 0 ]] || { echo 'Provide a secure token FD or run interactively' >&2; exit 1; } read -r -s -p 'Dedicated runner registration token: ' task_token echo touch /var/lib/pt-worker/state/enrollment.pending printf '%s' "$task_token" | runuser -u pt-worker -- env HOME=/var/lib/pt-worker /opt/pt-worker/node/bin/node /opt/pt-worker/ipc.cjs register unset task_token fi runuser -u pt-worker -- env HOME=/var/lib/pt-worker PATH="$PATH" /opt/pt-worker/runtime/node_modules/.bin/peertube-runner graceful-shutdown >/dev/null wait "$task_enroll_pid" # IPC returns success only after saving the dedicated runner credential to TOML. fi python3 - <<'PY' from pathlib import Path Path('/var/lib/pt-worker/state/enrollment.pending').unlink(missing_ok=True) PY python3 - <<'PY' import re,json from pathlib import Path p=Path('/var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml') c=json.load(open('/etc/pt-worker/worker.json')) s=p.read_text(); s,n=re.subn(r'(?m)^concurrency\s*=\s*\d+', 'concurrency = '+str(c['concurrency']), s,count=1) if n!=1: raise SystemExit('Missing concurrency configuration') p.write_text(s) PY chmod 600 /var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml install -m 644 "$task_source/systemd/pt-worker.service" /etc/systemd/system/pt-worker.service systemctl daemon-reload systemctl enable --now pt-worker.service echo 'Registered. Admission starts PAUSED; measure bandwidth or use pt-worker start --calibrate for initial real jobs.' /usr/local/bin/pt-worker status