Update docs and screenshots
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUT_DIR="${1:-./data/diagnostics}"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
WORK_DIR="$OUT_DIR/tuxdmx-diagnostics-$STAMP"
|
||||
ARCHIVE="$OUT_DIR/tuxdmx-diagnostics-$STAMP.tar.gz"
|
||||
ENV_FILE="/etc/tuxdmx/tuxdmx.env"
|
||||
DB_FILE="/var/lib/tuxdmx/tuxdmx.db"
|
||||
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
systemctl status olad tuxdmx.service >"$WORK_DIR/service-status.txt" 2>&1 || true
|
||||
journalctl -u olad -u tuxdmx.service -n 500 >"$WORK_DIR/journal.txt" 2>&1 || true
|
||||
lsusb >"$WORK_DIR/lsusb.txt" 2>&1 || true
|
||||
dmesg | grep -Ei "usb|tty|ola|audio" >"$WORK_DIR/dmesg-filtered.txt" 2>&1 || true
|
||||
uname -a >"$WORK_DIR/os.txt"
|
||||
python3 --version >"$WORK_DIR/python-version.txt" 2>&1 || true
|
||||
python3 -m pip freeze >"$WORK_DIR/python-packages.txt" 2>&1 || true
|
||||
ip addr >"$WORK_DIR/network.txt" 2>&1 || true
|
||||
arecord -l >"$WORK_DIR/audio.txt" 2>&1 || true
|
||||
curl -fsS http://127.0.0.1:8000/api/v1/system/diagnostics >"$WORK_DIR/api-diagnostics.json" 2>&1 || true
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
sed -E 's/(SECRET_KEY|TOKEN|PASSWORD)=.*/\1=<redacted>/g' "$ENV_FILE" >"$WORK_DIR/tuxdmx-env.redacted"
|
||||
fi
|
||||
|
||||
if [[ -f "$DB_FILE" ]]; then
|
||||
sqlite3 "$DB_FILE" "PRAGMA integrity_check;" >"$WORK_DIR/sqlite-integrity.txt" 2>&1 || true
|
||||
fi
|
||||
|
||||
tar -czf "$ARCHIVE" -C "$OUT_DIR" "tuxdmx-diagnostics-$STAMP"
|
||||
echo "Diagnostics skrevet til $ARCHIVE"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SYSTEMD_RUN="${SYSTEMD_RUN:-/usr/bin/systemd-run}"
|
||||
SYSTEMCTL_BIN="${SYSTEMCTL_BIN:-/usr/bin/systemctl}"
|
||||
UNIT_SUFFIX="$(date +%s)-$$"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "control-system.sh skal koeres som root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "$SYSTEMD_RUN" ]]; then
|
||||
echo "systemd-run blev ikke fundet." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "$SYSTEMCTL_BIN" ]]; then
|
||||
echo "systemctl blev ikke fundet." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${1:-}" in
|
||||
restart-service)
|
||||
exec "$SYSTEMD_RUN" \
|
||||
--quiet \
|
||||
--collect \
|
||||
--unit "tuxdmx-restart-service-$UNIT_SUFFIX" \
|
||||
--description "Genstart TuxDMX service" \
|
||||
/bin/sh -lc "sleep 2 && '$SYSTEMCTL_BIN' restart tuxdmx.service"
|
||||
;;
|
||||
reboot-host)
|
||||
exec "$SYSTEMD_RUN" \
|
||||
--quiet \
|
||||
--collect \
|
||||
--unit "tuxdmx-reboot-host-$UNIT_SUFFIX" \
|
||||
--description "Genstart TuxDMX host" \
|
||||
/bin/sh -lc "sleep 2 && '$SYSTEMCTL_BIN' reboot"
|
||||
;;
|
||||
*)
|
||||
echo "Brug: $0 {restart-service|reboot-host}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/lib/install-common.sh"
|
||||
|
||||
parse_common_args "$@"
|
||||
verify_debian_13_amd64
|
||||
run_install_flow "Debian 13 amd64"
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/lib/install-common.sh"
|
||||
|
||||
parse_common_args "$@"
|
||||
verify_pi_platform
|
||||
run_install_flow "Raspberry Pi / ARM"
|
||||
@@ -0,0 +1,773 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
APP_DIR="/opt/tuxdmx"
|
||||
STAGE_DIR="/opt/tuxdmx.new"
|
||||
DATA_DIR="/var/lib/tuxdmx"
|
||||
BACKUP_DIR="/var/lib/tuxdmx/backups"
|
||||
DIAGNOSTICS_DIR="/var/lib/tuxdmx/diagnostics"
|
||||
UPLOADS_DIR="/var/lib/tuxdmx/uploads"
|
||||
FIXTURE_CACHE_DIR="/var/lib/tuxdmx/fixtures"
|
||||
LOG_DIR="/var/log/tuxdmx"
|
||||
CONFIG_DIR="/etc/tuxdmx"
|
||||
ENV_FILE="$CONFIG_DIR/tuxdmx.env"
|
||||
SERVICE_USER="tuxdmx"
|
||||
SERVICE_GROUP="tuxdmx"
|
||||
SYSTEMD_UNIT_NAME="tuxdmx.service"
|
||||
SYSTEMD_UNIT_PATH="/etc/systemd/system/$SYSTEMD_UNIT_NAME"
|
||||
SYSTEM_CONTROL_SUDOERS_PATH="/etc/sudoers.d/tuxdmx-control"
|
||||
SYSTEMD_WORKDIR="/opt/tuxdmx"
|
||||
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||
NODE_REQUIRED_MAJOR="20"
|
||||
PNPM_REQUIRED_VERSION="10.28.2"
|
||||
SKIP_FRONTEND_BUILD="false"
|
||||
NON_INTERACTIVE="false"
|
||||
FORCE_OVERWRITE="false"
|
||||
APT_UPDATED="false"
|
||||
PNPM_BIN=""
|
||||
ROLLBACK_APP_ARCHIVE=""
|
||||
ROLLBACK_DATA_ARCHIVE=""
|
||||
ROLLBACK_CONFIG_ARCHIVE=""
|
||||
ROLLBACK_UNIT_FILE=""
|
||||
HAD_EXISTING_APP="false"
|
||||
HAD_EXISTING_DATA="false"
|
||||
HAD_EXISTING_CONFIG="false"
|
||||
HAD_EXISTING_UNIT="false"
|
||||
OLA_SERVICE_NAME="olad"
|
||||
OLA_SYSTEMD_UNIT="olad.service"
|
||||
|
||||
readonly COMMON_PACKAGES=(
|
||||
python3
|
||||
python3-venv
|
||||
python3-pip
|
||||
python3-dev
|
||||
sqlite3
|
||||
sudo
|
||||
curl
|
||||
git
|
||||
jq
|
||||
zip
|
||||
unzip
|
||||
rsync
|
||||
ca-certificates
|
||||
nodejs
|
||||
npm
|
||||
ola
|
||||
alsa-utils
|
||||
usbutils
|
||||
)
|
||||
|
||||
readonly OLA_PYTHON_PACKAGES=(
|
||||
python3-ola
|
||||
ola-python
|
||||
)
|
||||
|
||||
readonly RUNTIME_ROOT_FILES=(
|
||||
".env.example"
|
||||
"LICENSE"
|
||||
"NOTICE"
|
||||
"package.json"
|
||||
"pnpm-lock.yaml"
|
||||
"pnpm-workspace.yaml"
|
||||
"pyproject.toml"
|
||||
"README.md"
|
||||
"requirements.lock"
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
"tsconfig.json"
|
||||
"tsconfig.node.json"
|
||||
"vite.config.ts"
|
||||
)
|
||||
|
||||
readonly RUNTIME_DIRS=(
|
||||
"backend"
|
||||
"frontend"
|
||||
"scripts"
|
||||
"systemd"
|
||||
"test-data"
|
||||
)
|
||||
|
||||
log() {
|
||||
echo "[TuxDMX] $*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "[TuxDMX] FEJL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
parse_common_args() {
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--non-interactive) NON_INTERACTIVE="true" ;;
|
||||
--force) FORCE_OVERWRITE="true" ;;
|
||||
--skip-frontend-build) SKIP_FRONTEND_BUILD="true" ;;
|
||||
*) echo "Ukendt argument: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
fail "Koer scriptet som root eller via sudo."
|
||||
fi
|
||||
}
|
||||
|
||||
apt_update_once() {
|
||||
if [[ "$APT_UPDATED" == "true" ]]; then
|
||||
return
|
||||
fi
|
||||
apt-get update
|
||||
APT_UPDATED="true"
|
||||
}
|
||||
|
||||
install_packages() {
|
||||
apt_update_once
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
|
||||
}
|
||||
|
||||
package_available() {
|
||||
apt-cache show "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_network_connectivity() {
|
||||
curl -fsS https://open-fixture-library.org >/dev/null
|
||||
}
|
||||
|
||||
verify_debian_13_amd64() {
|
||||
source /etc/os-release
|
||||
local arch
|
||||
arch="$(dpkg --print-architecture)"
|
||||
|
||||
[[ "${ID:-}" == "debian" ]] || fail "Debian 13 amd64 er paakraevet. Fundet ID=${ID:-ukendt}."
|
||||
[[ "${VERSION_ID:-}" == "13" ]] || fail "Debian 13 er paakraevet. Fundet VERSION_ID=${VERSION_ID:-ukendt}."
|
||||
[[ "$arch" == "amd64" ]] || fail "amd64 er paakraevet. Fundet arkitektur=$arch."
|
||||
}
|
||||
|
||||
verify_pi_platform() {
|
||||
source /etc/os-release
|
||||
local arch
|
||||
arch="$(uname -m)"
|
||||
[[ "${ID:-}" =~ ^(debian|raspbian)$ ]] || fail "Pi-installation forventer Debian eller Raspberry Pi OS."
|
||||
[[ "$arch" =~ ^(aarch64|arm64|armv7l)$ ]] || log "Advarsel: forventet ARM-arkitektur, men fandt $arch."
|
||||
}
|
||||
|
||||
verify_source_tree() {
|
||||
[[ -f "$SOURCE_ROOT/backend/app/main.py" ]] || fail "Source tree mangler backend/app/main.py."
|
||||
[[ -f "$SOURCE_ROOT/package.json" ]] || fail "Source tree mangler package.json."
|
||||
[[ -f "$SOURCE_ROOT/requirements.lock" ]] || fail "Source tree mangler requirements.lock."
|
||||
}
|
||||
|
||||
confirm_install_overwrite() {
|
||||
if [[ -d "$APP_DIR" ]] && find "$APP_DIR" -mindepth 1 -print -quit >/dev/null 2>&1; then
|
||||
if [[ "$FORCE_OVERWRITE" == "true" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
fail "Eksisterende installation fundet. Brug --force sammen med --non-interactive."
|
||||
fi
|
||||
read -r -p "Eksisterende installation fundet i $APP_DIR. Fortsaet? [y/N] " answer
|
||||
[[ "$answer" =~ ^[Yy]$ ]] || exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_service_user() {
|
||||
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
useradd --system --home "$DATA_DIR" --create-home --groups dialout,plugdev,audio "$SERVICE_USER"
|
||||
fi
|
||||
usermod -a -G dialout,plugdev,audio "$SERVICE_USER"
|
||||
}
|
||||
|
||||
ensure_directories() {
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$APP_DIR"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$STAGE_DIR"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$DATA_DIR"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$BACKUP_DIR"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$DIAGNOSTICS_DIR"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$UPLOADS_DIR"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$FIXTURE_CACHE_DIR"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$LOG_DIR"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$CONFIG_DIR"
|
||||
}
|
||||
|
||||
ensure_backup_dir() {
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$BACKUP_DIR"
|
||||
}
|
||||
|
||||
install_base_packages() {
|
||||
install_packages "${COMMON_PACKAGES[@]}"
|
||||
}
|
||||
|
||||
install_ola_python_binding() {
|
||||
local installed="false"
|
||||
for candidate in "${OLA_PYTHON_PACKAGES[@]}"; do
|
||||
if package_available "$candidate"; then
|
||||
install_packages "$candidate"
|
||||
installed="true"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$installed" != "true" ]]; then
|
||||
fail "Kunne ikke finde Debian-pakke til OLA Python-binding (proevede: ${OLA_PYTHON_PACKAGES[*]})."
|
||||
fi
|
||||
|
||||
if ! "$PYTHON_BIN" - <<'PY'
|
||||
from ola.ClientWrapper import ClientWrapper
|
||||
print(ClientWrapper.__name__)
|
||||
PY
|
||||
then
|
||||
fail "OLA Python-binding kunne ikke importeres med system-python."
|
||||
fi
|
||||
|
||||
command -v olad >/dev/null 2>&1 || fail "olad blev ikke fundet efter installation af OLA."
|
||||
}
|
||||
|
||||
ensure_node_toolchain() {
|
||||
local node_version
|
||||
local node_major
|
||||
local global_prefix
|
||||
|
||||
node_version="$(node --version | sed 's/^v//')"
|
||||
node_major="${node_version%%.*}"
|
||||
[[ "$node_major" == "$NODE_REQUIRED_MAJOR" ]] || fail "Node 20 er paakraevet. Fundet $node_version."
|
||||
npm --version >/dev/null
|
||||
|
||||
global_prefix="$(npm config get prefix)"
|
||||
npm install -g --force "pnpm@$PNPM_REQUIRED_VERSION"
|
||||
export PATH="$global_prefix/bin:$PATH"
|
||||
hash -r
|
||||
PNPM_BIN="$(command -v pnpm || true)"
|
||||
[[ -n "$PNPM_BIN" ]] || fail "pnpm blev ikke fundet efter installation."
|
||||
[[ "$("$PNPM_BIN" --version)" == "$PNPM_REQUIRED_VERSION" ]] \
|
||||
|| fail "Aktiv pnpm-version er ikke $PNPM_REQUIRED_VERSION."
|
||||
}
|
||||
|
||||
require_source_frontend_artifacts() {
|
||||
[[ -f "$SOURCE_ROOT/frontend/dist/index.html" ]] \
|
||||
|| fail "--skip-frontend-build kraever eksisterende frontend/dist/index.html."
|
||||
}
|
||||
|
||||
build_frontend() {
|
||||
if [[ "$SKIP_FRONTEND_BUILD" == "true" ]]; then
|
||||
require_source_frontend_artifacts
|
||||
return
|
||||
fi
|
||||
|
||||
ensure_node_toolchain
|
||||
(
|
||||
cd "$SOURCE_ROOT"
|
||||
"$PNPM_BIN" install --frozen-lockfile
|
||||
"$PNPM_BIN" build
|
||||
)
|
||||
|
||||
[[ -f "$SOURCE_ROOT/frontend/dist/index.html" ]] \
|
||||
|| fail "Frontend-build afsluttede uden frontend/dist/index.html."
|
||||
}
|
||||
|
||||
cleanup_stage_dir() {
|
||||
rm -rf "$STAGE_DIR"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$STAGE_DIR"
|
||||
}
|
||||
|
||||
sync_directory() {
|
||||
local relative_path="$1"
|
||||
rsync -a --delete --chown=root:"$SERVICE_GROUP" \
|
||||
--chmod=Du=rwx,Dg=rx,Do=rx,Fu=rw,Fg=r,Fo=r \
|
||||
"$SOURCE_ROOT/$relative_path/" "$STAGE_DIR/$relative_path/"
|
||||
}
|
||||
|
||||
sync_file() {
|
||||
local relative_path="$1"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$(dirname "$STAGE_DIR/$relative_path")"
|
||||
rsync -a --chown=root:"$SERVICE_GROUP" \
|
||||
--chmod=Fu=rw,Fg=r,Fo=r \
|
||||
"$SOURCE_ROOT/$relative_path" "$STAGE_DIR/$relative_path"
|
||||
}
|
||||
|
||||
sync_runtime_tree_to_stage() {
|
||||
local dir_path
|
||||
local file_path
|
||||
|
||||
cleanup_stage_dir
|
||||
|
||||
for dir_path in "${RUNTIME_DIRS[@]}"; do
|
||||
sync_directory "$dir_path"
|
||||
done
|
||||
|
||||
for file_path in "${RUNTIME_ROOT_FILES[@]}"; do
|
||||
if [[ -f "$SOURCE_ROOT/$file_path" ]]; then
|
||||
sync_file "$file_path"
|
||||
fi
|
||||
done
|
||||
|
||||
ln -sfn "$DATA_DIR" "$STAGE_DIR/data"
|
||||
find "$STAGE_DIR/scripts" -type f -name "*.sh" -exec chmod 0755 {} +
|
||||
|
||||
[[ -f "$STAGE_DIR/backend/app/main.py" ]] || fail "Backend-layout er forkert: $STAGE_DIR/backend/app/main.py mangler."
|
||||
[[ -f "$STAGE_DIR/frontend/dist/index.html" ]] || fail "Kopieret frontend/dist/index.html mangler i stage."
|
||||
}
|
||||
|
||||
create_or_refresh_venv() {
|
||||
log "Opretter virtualenv i stage"
|
||||
"$PYTHON_BIN" -m venv --system-site-packages "$STAGE_DIR/.venv"
|
||||
"$STAGE_DIR/.venv/bin/pip" install --upgrade pip
|
||||
"$STAGE_DIR/.venv/bin/pip" install -r "$STAGE_DIR/requirements.lock"
|
||||
run_runtime_pip_check
|
||||
}
|
||||
|
||||
run_runtime_pip_check() {
|
||||
local output=""
|
||||
local status=0
|
||||
local filtered=""
|
||||
|
||||
output="$("$STAGE_DIR/.venv/bin/pip" check 2>&1)" || status=$?
|
||||
if [[ "$status" -eq 0 ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
filtered="$(printf '%s\n' "$output" | grep -Ev '^apt-listchanges .+ requires debconf, which is not installed\.$' || true)"
|
||||
if [[ -z "${filtered//[[:space:]]/}" ]]; then
|
||||
log "Ignorerer kendt Debian pip check-advarsel fra apt-listchanges/debconf."
|
||||
return
|
||||
fi
|
||||
|
||||
printf '%s\n' "$filtered" >&2
|
||||
fail "pip check fejlede i runtime-venv."
|
||||
}
|
||||
|
||||
write_default_env_file() {
|
||||
local secret
|
||||
secret="$("$STAGE_DIR/.venv/bin/python" - <<'PY'
|
||||
import secrets
|
||||
print(secrets.token_hex(32))
|
||||
PY
|
||||
)"
|
||||
|
||||
cat >"$ENV_FILE" <<EOF
|
||||
TUXDMX_ENV=production
|
||||
TUXDMX_SECRET_KEY=$secret
|
||||
TUXDMX_DATABASE_URL=sqlite+aiosqlite:////var/lib/tuxdmx/tuxdmx.db
|
||||
TUXDMX_LOG_LEVEL=INFO
|
||||
TUXDMX_SIMULATOR_ENABLED=false
|
||||
TUXDMX_OLA_UNIVERSE=1
|
||||
TUXDMX_OLA_OUTPUT_PORT=
|
||||
TUXDMX_OLA_SEND_TIMEOUT_MS=1000
|
||||
TUXDMX_DATA_DIR=/var/lib/tuxdmx
|
||||
TUXDMX_BACKUP_DIR=/var/lib/tuxdmx/backups
|
||||
TUXDMX_FIXTURE_CACHE_DIR=/var/lib/tuxdmx/fixtures
|
||||
TUXDMX_DIAGNOSTICS_DIR=/var/lib/tuxdmx/diagnostics
|
||||
TUXDMX_UPLOADS_DIR=/var/lib/tuxdmx/uploads
|
||||
TUXDMX_LOG_DIR=/var/log/tuxdmx
|
||||
TUXDMX_FRONTEND_DIST=/opt/tuxdmx/frontend/dist
|
||||
EOF
|
||||
chown root:"$SERVICE_GROUP" "$ENV_FILE"
|
||||
chmod 0640 "$ENV_FILE"
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
write_default_env_file
|
||||
else
|
||||
chown root:"$SERVICE_GROUP" "$ENV_FILE"
|
||||
chmod 0640 "$ENV_FILE"
|
||||
fi
|
||||
|
||||
ensure_env_setting "TUXDMX_DATABASE_URL" "sqlite+aiosqlite:////var/lib/tuxdmx/tuxdmx.db"
|
||||
ensure_env_setting "TUXDMX_LOG_LEVEL" "INFO"
|
||||
ensure_env_setting "TUXDMX_SIMULATOR_ENABLED" "false"
|
||||
ensure_env_setting "TUXDMX_OLA_UNIVERSE" "1"
|
||||
ensure_env_setting "TUXDMX_OLA_OUTPUT_PORT" ""
|
||||
ensure_env_setting "TUXDMX_OLA_SEND_TIMEOUT_MS" "1000"
|
||||
ensure_env_setting "TUXDMX_DATA_DIR" "/var/lib/tuxdmx"
|
||||
ensure_env_setting "TUXDMX_BACKUP_DIR" "/var/lib/tuxdmx/backups"
|
||||
ensure_env_setting "TUXDMX_FIXTURE_CACHE_DIR" "/var/lib/tuxdmx/fixtures"
|
||||
ensure_env_setting "TUXDMX_DIAGNOSTICS_DIR" "/var/lib/tuxdmx/diagnostics"
|
||||
ensure_env_setting "TUXDMX_UPLOADS_DIR" "/var/lib/tuxdmx/uploads"
|
||||
ensure_env_setting "TUXDMX_LOG_DIR" "/var/log/tuxdmx"
|
||||
ensure_env_setting "TUXDMX_FRONTEND_DIST" "/opt/tuxdmx/frontend/dist"
|
||||
}
|
||||
|
||||
ensure_env_setting() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if grep -q "^${key}=" "$ENV_FILE"; then
|
||||
return
|
||||
fi
|
||||
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||
}
|
||||
|
||||
load_env_lines() {
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
printf '%s\0' "$line"
|
||||
done < "$ENV_FILE"
|
||||
}
|
||||
|
||||
run_as_service_user() {
|
||||
local app_root="$1"
|
||||
shift
|
||||
local env_lines=()
|
||||
while IFS= read -r -d '' line; do
|
||||
env_lines+=("$line")
|
||||
done < <(load_env_lines)
|
||||
|
||||
runuser -u "$SERVICE_USER" -- env \
|
||||
"PYTHONPATH=$app_root/backend" \
|
||||
"${env_lines[@]}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
verify_python_runtime() {
|
||||
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c "import greenlet; print(greenlet.__version__)"
|
||||
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c "import sqlalchemy; print(sqlalchemy.__version__)"
|
||||
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c "import ola; print(ola.__file__)"
|
||||
}
|
||||
|
||||
verify_stage_permissions_and_layout() {
|
||||
test -f "$STAGE_DIR/backend/app/main.py" || fail "Stage mangler backend/app/main.py."
|
||||
runuser -u "$SERVICE_USER" -- test -r "$STAGE_DIR/frontend/dist/index.html"
|
||||
runuser -u "$SERVICE_USER" -- "$PYTHON_BIN" -c \
|
||||
"from pathlib import Path; print(Path('$STAGE_DIR/frontend/dist/index.html').read_text()[:20])"
|
||||
}
|
||||
|
||||
verify_app_import() {
|
||||
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c \
|
||||
"import sys; sys.path.insert(0, '$STAGE_DIR/backend'); import app.main"
|
||||
}
|
||||
|
||||
run_migrations() {
|
||||
log "Koerer database migration som $SERVICE_USER"
|
||||
run_as_service_user "$STAGE_DIR" \
|
||||
"$STAGE_DIR/.venv/bin/python" -m alembic -c "$STAGE_DIR/backend/alembic.ini" upgrade head
|
||||
}
|
||||
|
||||
verify_database_writable() {
|
||||
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c \
|
||||
"from pathlib import Path; db=Path('/var/lib/tuxdmx/tuxdmx.db'); print(db.exists()); wal=Path('/var/lib/tuxdmx/tuxdmx.db-wal'); wal.touch(exist_ok=True); print(wal.exists())"
|
||||
chown "$SERVICE_USER":"$SERVICE_GROUP" /var/lib/tuxdmx/tuxdmx.db* 2>/dev/null || true
|
||||
}
|
||||
|
||||
install_systemd_unit() {
|
||||
install -m 0644 "$STAGE_DIR/systemd/tuxdmx.service" "$SYSTEMD_UNIT_PATH"
|
||||
systemctl daemon-reload
|
||||
enable_ola_service
|
||||
install_system_control_sudoers
|
||||
systemctl enable "$SYSTEMD_UNIT_NAME"
|
||||
}
|
||||
|
||||
install_system_control_sudoers() {
|
||||
cat >"$SYSTEM_CONTROL_SUDOERS_PATH" <<'EOF'
|
||||
Defaults:tuxdmx !requiretty
|
||||
tuxdmx ALL=(root) NOPASSWD: /opt/tuxdmx/scripts/control-system.sh restart-service, /opt/tuxdmx/scripts/control-system.sh reboot-host
|
||||
EOF
|
||||
chmod 0440 "$SYSTEM_CONTROL_SUDOERS_PATH"
|
||||
visudo -cf "$SYSTEM_CONTROL_SUDOERS_PATH" >/dev/null
|
||||
}
|
||||
|
||||
show_service_diagnostics() {
|
||||
systemctl status "$SYSTEMD_UNIT_NAME" --no-pager --full || true
|
||||
journalctl -u "$SYSTEMD_UNIT_NAME" -n 200 --no-pager || true
|
||||
}
|
||||
|
||||
resolve_ola_service_name() {
|
||||
local unit_files
|
||||
unit_files="$(systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}')"
|
||||
|
||||
if grep -Fxq "olad.service" <<<"$unit_files"; then
|
||||
OLA_SERVICE_NAME="olad"
|
||||
OLA_SYSTEMD_UNIT="olad.service"
|
||||
return
|
||||
fi
|
||||
|
||||
if grep -Fxq "ola.service" <<<"$unit_files"; then
|
||||
OLA_SERVICE_NAME="ola"
|
||||
OLA_SYSTEMD_UNIT="ola.service"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -x /etc/init.d/olad ]]; then
|
||||
OLA_SERVICE_NAME="olad"
|
||||
OLA_SYSTEMD_UNIT="olad.service"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -x /etc/init.d/ola ]]; then
|
||||
OLA_SERVICE_NAME="ola"
|
||||
OLA_SYSTEMD_UNIT="ola.service"
|
||||
fi
|
||||
}
|
||||
|
||||
has_sysv_boot_link() {
|
||||
local service_name="$1"
|
||||
compgen -G "/etc/rc?.d/S??$service_name" >/dev/null
|
||||
}
|
||||
|
||||
enable_ola_service() {
|
||||
resolve_ola_service_name
|
||||
|
||||
systemctl enable "$OLA_SYSTEMD_UNIT" >/dev/null 2>&1 || true
|
||||
|
||||
if [[ -x "/etc/init.d/$OLA_SERVICE_NAME" ]]; then
|
||||
update-rc.d "$OLA_SERVICE_NAME" defaults >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ "$OLA_SERVICE_NAME" != "olad" && -x /etc/init.d/olad ]]; then
|
||||
update-rc.d olad defaults >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ "$OLA_SERVICE_NAME" != "ola" && -x /etc/init.d/ola ]]; then
|
||||
update-rc.d ola defaults >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if systemctl is-enabled "$OLA_SYSTEMD_UNIT" >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
if has_sysv_boot_link "$OLA_SERVICE_NAME"; then
|
||||
return
|
||||
fi
|
||||
|
||||
fail "Kunne ikke registrere $OLA_SYSTEMD_UNIT til autostart."
|
||||
}
|
||||
|
||||
show_ola_diagnostics() {
|
||||
systemctl status "$OLA_SYSTEMD_UNIT" --no-pager --full || true
|
||||
journalctl -u "$OLA_SYSTEMD_UNIT" -n 200 --no-pager || true
|
||||
}
|
||||
|
||||
stop_existing_service_for_snapshot() {
|
||||
if systemctl list-unit-files "$SYSTEMD_UNIT_NAME" >/dev/null 2>&1; then
|
||||
systemctl stop "$SYSTEMD_UNIT_NAME" || true
|
||||
for _ in $(seq 1 15); do
|
||||
local state
|
||||
state="$(systemctl is-active "$SYSTEMD_UNIT_NAME" || true)"
|
||||
if [[ "$state" != "active" && "$state" != "activating" && "$state" != "deactivating" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
restart_and_healthcheck() {
|
||||
local max_checks=30
|
||||
local check
|
||||
|
||||
resolve_ola_service_name
|
||||
|
||||
if ! systemctl restart "$OLA_SYSTEMD_UNIT"; then
|
||||
if ! service "$OLA_SERVICE_NAME" restart; then
|
||||
show_ola_diagnostics
|
||||
fail "Kunne ikke restarte $OLA_SYSTEMD_UNIT."
|
||||
fi
|
||||
fi
|
||||
if ! systemctl restart "$SYSTEMD_UNIT_NAME"; then
|
||||
show_service_diagnostics
|
||||
fail "Kunne ikke starte $SYSTEMD_UNIT_NAME."
|
||||
fi
|
||||
|
||||
for check in $(seq 1 "$max_checks"); do
|
||||
local service_state
|
||||
service_state="$(systemctl is-active "$SYSTEMD_UNIT_NAME" || true)"
|
||||
if [[ "$service_state" == "failed" || "$service_state" == "deactivating" ]]; then
|
||||
show_service_diagnostics
|
||||
fail "tuxdmx.service gik i state '$service_state' under health check."
|
||||
fi
|
||||
if curl -fsS http://127.0.0.1:8000/api/v1/health >/dev/null; then
|
||||
systemctl is-active "$SYSTEMD_UNIT_NAME" >/dev/null
|
||||
return
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
show_service_diagnostics
|
||||
fail "Health check mod http://127.0.0.1:8000/api/v1/health fejlede."
|
||||
}
|
||||
|
||||
make_archive() {
|
||||
local source_dir="$1"
|
||||
local archive_path="$2"
|
||||
if [[ ! -d "$source_dir" ]]; then
|
||||
return 1
|
||||
fi
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$BACKUP_DIR"
|
||||
if [[ "$source_dir" == "$DATA_DIR" ]]; then
|
||||
tar \
|
||||
--warning=no-file-changed \
|
||||
--exclude="./backups" \
|
||||
--exclude="./tuxdmx.json.log" \
|
||||
-czf "$archive_path" -C "$source_dir" .
|
||||
return
|
||||
fi
|
||||
tar -czf "$archive_path" -C "$source_dir" .
|
||||
}
|
||||
|
||||
restore_archive() {
|
||||
local archive_path="$1"
|
||||
local target_dir="$2"
|
||||
[[ -f "$archive_path" ]] || return
|
||||
rm -rf "$target_dir"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$target_dir"
|
||||
tar -xzf "$archive_path" -C "$target_dir"
|
||||
}
|
||||
|
||||
prepare_install_rollback() {
|
||||
local stamp
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
ensure_backup_dir
|
||||
|
||||
if [[ -d "$APP_DIR" ]] && find "$APP_DIR" -mindepth 1 -print -quit >/dev/null 2>&1; then
|
||||
HAD_EXISTING_APP="true"
|
||||
ROLLBACK_APP_ARCHIVE="$BACKUP_DIR/install-app-$stamp.tar.gz"
|
||||
make_archive "$APP_DIR" "$ROLLBACK_APP_ARCHIVE"
|
||||
fi
|
||||
|
||||
if [[ -d "$DATA_DIR" ]] && find "$DATA_DIR" -mindepth 1 -print -quit >/dev/null 2>&1; then
|
||||
HAD_EXISTING_DATA="true"
|
||||
ROLLBACK_DATA_ARCHIVE="$BACKUP_DIR/install-data-$stamp.tar.gz"
|
||||
make_archive "$DATA_DIR" "$ROLLBACK_DATA_ARCHIVE"
|
||||
fi
|
||||
|
||||
if [[ -d "$CONFIG_DIR" ]] && find "$CONFIG_DIR" -mindepth 1 -print -quit >/dev/null 2>&1; then
|
||||
HAD_EXISTING_CONFIG="true"
|
||||
ROLLBACK_CONFIG_ARCHIVE="$BACKUP_DIR/install-config-$stamp.tar.gz"
|
||||
make_archive "$CONFIG_DIR" "$ROLLBACK_CONFIG_ARCHIVE"
|
||||
fi
|
||||
|
||||
if [[ -f "$SYSTEMD_UNIT_PATH" ]]; then
|
||||
HAD_EXISTING_UNIT="true"
|
||||
ROLLBACK_UNIT_FILE="$BACKUP_DIR/install-$SYSTEMD_UNIT_NAME.$stamp.bak"
|
||||
cp "$SYSTEMD_UNIT_PATH" "$ROLLBACK_UNIT_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_update_rollback() {
|
||||
local stamp
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
ensure_backup_dir
|
||||
|
||||
ROLLBACK_APP_ARCHIVE="$BACKUP_DIR/update-app-$stamp.tar.gz"
|
||||
ROLLBACK_DATA_ARCHIVE="$BACKUP_DIR/update-data-$stamp.tar.gz"
|
||||
ROLLBACK_CONFIG_ARCHIVE="$BACKUP_DIR/update-config-$stamp.tar.gz"
|
||||
ROLLBACK_UNIT_FILE="$BACKUP_DIR/update-$SYSTEMD_UNIT_NAME.$stamp.bak"
|
||||
|
||||
make_archive "$APP_DIR" "$ROLLBACK_APP_ARCHIVE" || fail "Kunne ikke oprette backup af eksisterende appmappe."
|
||||
make_archive "$DATA_DIR" "$ROLLBACK_DATA_ARCHIVE" || fail "Kunne ikke oprette backup af datamappe."
|
||||
make_archive "$CONFIG_DIR" "$ROLLBACK_CONFIG_ARCHIVE" || true
|
||||
if [[ -f "$SYSTEMD_UNIT_PATH" ]]; then
|
||||
cp "$SYSTEMD_UNIT_PATH" "$ROLLBACK_UNIT_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
restore_rollbacks() {
|
||||
log "Fejl opdaget. Forsoger rollback."
|
||||
|
||||
if [[ -n "$ROLLBACK_APP_ARCHIVE" && -f "$ROLLBACK_APP_ARCHIVE" ]]; then
|
||||
restore_archive "$ROLLBACK_APP_ARCHIVE" "$APP_DIR"
|
||||
fi
|
||||
|
||||
if [[ -n "$ROLLBACK_DATA_ARCHIVE" && -f "$ROLLBACK_DATA_ARCHIVE" ]]; then
|
||||
restore_archive "$ROLLBACK_DATA_ARCHIVE" "$DATA_DIR"
|
||||
chown -R "$SERVICE_USER":"$SERVICE_GROUP" "$DATA_DIR"
|
||||
fi
|
||||
|
||||
if [[ -n "$ROLLBACK_CONFIG_ARCHIVE" && -f "$ROLLBACK_CONFIG_ARCHIVE" ]]; then
|
||||
restore_archive "$ROLLBACK_CONFIG_ARCHIVE" "$CONFIG_DIR"
|
||||
chown -R root:"$SERVICE_GROUP" "$CONFIG_DIR"
|
||||
chmod 0750 "$CONFIG_DIR"
|
||||
[[ -f "$ENV_FILE" ]] && chmod 0640 "$ENV_FILE"
|
||||
fi
|
||||
|
||||
if [[ -f "$ROLLBACK_UNIT_FILE" ]]; then
|
||||
install -m 0644 "$ROLLBACK_UNIT_FILE" "$SYSTEMD_UNIT_PATH"
|
||||
fi
|
||||
|
||||
rm -rf "$STAGE_DIR"
|
||||
systemctl daemon-reload || true
|
||||
resolve_ola_service_name
|
||||
systemctl restart "$OLA_SYSTEMD_UNIT" || true
|
||||
systemctl restart "$SYSTEMD_UNIT_NAME" || true
|
||||
}
|
||||
|
||||
promote_stage() {
|
||||
systemctl stop "$SYSTEMD_UNIT_NAME" || true
|
||||
rm -rf "$APP_DIR"
|
||||
mv "$STAGE_DIR" "$APP_DIR"
|
||||
chown -R root:"$SERVICE_GROUP" "$APP_DIR"
|
||||
}
|
||||
|
||||
finalize_runtime_layout() {
|
||||
ln -sfn "$DATA_DIR" "$APP_DIR/data"
|
||||
}
|
||||
|
||||
run_install_flow() {
|
||||
local platform_label="$1"
|
||||
require_root
|
||||
confirm_install_overwrite
|
||||
check_network_connectivity
|
||||
|
||||
log "Starter installation for $platform_label"
|
||||
verify_source_tree
|
||||
install_base_packages
|
||||
install_ola_python_binding
|
||||
ensure_node_toolchain
|
||||
build_frontend
|
||||
stop_existing_service_for_snapshot
|
||||
prepare_install_rollback
|
||||
trap 'restore_rollbacks' ERR
|
||||
ensure_service_user
|
||||
ensure_directories
|
||||
sync_runtime_tree_to_stage
|
||||
create_or_refresh_venv
|
||||
ensure_env_file
|
||||
verify_python_runtime
|
||||
verify_stage_permissions_and_layout
|
||||
verify_app_import
|
||||
run_migrations
|
||||
verify_database_writable
|
||||
install_systemd_unit
|
||||
promote_stage
|
||||
finalize_runtime_layout
|
||||
restart_and_healthcheck
|
||||
|
||||
trap - ERR
|
||||
log "Installation fuldfoert"
|
||||
}
|
||||
|
||||
run_update_flow() {
|
||||
local platform_label="$1"
|
||||
require_root
|
||||
check_network_connectivity
|
||||
|
||||
log "Starter update for $platform_label"
|
||||
verify_source_tree
|
||||
install_base_packages
|
||||
install_ola_python_binding
|
||||
ensure_node_toolchain
|
||||
build_frontend
|
||||
stop_existing_service_for_snapshot
|
||||
prepare_update_rollback
|
||||
trap 'restore_rollbacks' ERR
|
||||
ensure_service_user
|
||||
ensure_directories
|
||||
sync_runtime_tree_to_stage
|
||||
create_or_refresh_venv
|
||||
ensure_env_file
|
||||
verify_python_runtime
|
||||
verify_stage_permissions_and_layout
|
||||
verify_app_import
|
||||
run_migrations
|
||||
verify_database_writable
|
||||
install_systemd_unit
|
||||
promote_stage
|
||||
finalize_runtime_layout
|
||||
restart_and_healthcheck
|
||||
|
||||
trap - ERR
|
||||
log "Update gennemfoert"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
failures=0
|
||||
|
||||
check() {
|
||||
local description="$1"
|
||||
shift
|
||||
echo "== $description =="
|
||||
if "$@"; then
|
||||
echo "OK: $description"
|
||||
else
|
||||
echo "FEJL: $description"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
check "OS og arkitektur" uname -a
|
||||
check "OLA version" ola_dev_info --version
|
||||
check "olad status" systemctl is-active --quiet olad
|
||||
check "tuxdmx service status" systemctl is-active --quiet tuxdmx.service
|
||||
check "API health" curl -fsS http://127.0.0.1:8000/api/v1/health
|
||||
check "Databasefil findes" test -f /var/lib/tuxdmx/tuxdmx.db
|
||||
check "USB devices list" lsusb
|
||||
check "Audio devices" arecord -l
|
||||
check "Disk plads" df -h /
|
||||
|
||||
echo "== Simulatorframe =="
|
||||
curl -fsS http://127.0.0.1:8000/api/v1/dmx/frame >/dev/null || failures=$((failures + 1))
|
||||
|
||||
if [[ "${1:-}" == "--hardware-channel-test" ]]; then
|
||||
echo "Hardwarekanaltest er eksplicit valgt. Brugeren skal visuelt bekræfte lampereaktion."
|
||||
curl -fsS -X POST "http://127.0.0.1:8000/api/v1/dmx/test-channel?channel=1&value=64" >/dev/null || failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
if (( failures > 0 )); then
|
||||
echo "Smoke test fejlede med $failures fejl"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Smoke test bestået. Bemærk: bestået test dokumenterer ikke i sig selv fysisk DMX-signal ved lampen."
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$RepositoryRoot = "",
|
||||
[string]$RemoteName = "origin",
|
||||
[string]$RemoteUrl = "https://git.tuxinet.dk/thomas/TuxDMX-WebUI.git",
|
||||
[string]$Branch = "main",
|
||||
[string]$CommitMessage = "Update TuxDMX",
|
||||
[string[]]$StageExclude = @(
|
||||
".pnpm-store/**",
|
||||
"node_modules/**",
|
||||
".venv/**",
|
||||
"__pycache__/**",
|
||||
".pytest_cache/**",
|
||||
".ruff_cache/**",
|
||||
".mypy_cache/**",
|
||||
"frontend/dist/**",
|
||||
"backend/.pytest_cache/**",
|
||||
"backend/htmlcov/**",
|
||||
"data/**",
|
||||
".env"
|
||||
),
|
||||
[switch]$InitIfNeeded,
|
||||
[switch]$SetOrigin,
|
||||
[switch]$StageAll,
|
||||
[switch]$Commit,
|
||||
[switch]$Push
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Info {
|
||||
param([string]$Message)
|
||||
Write-Host "[TuxDMX] $Message"
|
||||
}
|
||||
|
||||
function Normalize-RepoPath {
|
||||
param([string]$Path)
|
||||
return ($Path -replace "\\", "/").Trim()
|
||||
}
|
||||
|
||||
function Test-IsExcludedPath {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string[]]$Patterns
|
||||
)
|
||||
|
||||
$normalizedPath = Normalize-RepoPath -Path $Path
|
||||
|
||||
foreach ($pattern in $Patterns) {
|
||||
$normalizedPattern = Normalize-RepoPath -Path $pattern
|
||||
|
||||
if ($normalizedPattern.EndsWith("/**")) {
|
||||
$prefix = $normalizedPattern.Substring(0, $normalizedPattern.Length - 3).TrimEnd("/")
|
||||
if ($normalizedPath -eq $prefix -or $normalizedPath.StartsWith("$prefix/")) {
|
||||
return $true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ($normalizedPath -eq $normalizedPattern) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Invoke-Git {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Arguments,
|
||||
[switch]$IgnoreFailure
|
||||
)
|
||||
|
||||
& git -C $RepositoryRoot @Arguments
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
if (-not $IgnoreFailure -and $exitCode -ne 0) {
|
||||
throw "git $($Arguments -join ' ') fejlede med exit code $exitCode."
|
||||
}
|
||||
|
||||
return $exitCode
|
||||
}
|
||||
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
throw "Git blev ikke fundet i PATH."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
|
||||
$scriptRoot = if (-not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
|
||||
$PSScriptRoot
|
||||
} else {
|
||||
Split-Path -Parent $PSCommandPath
|
||||
}
|
||||
$RepositoryRoot = (Resolve-Path (Join-Path $scriptRoot "..")).Path
|
||||
}
|
||||
|
||||
$gitDirectory = Join-Path $RepositoryRoot ".git"
|
||||
|
||||
if (-not (Test-Path $gitDirectory)) {
|
||||
if (-not $InitIfNeeded) {
|
||||
throw "RepositoryRoot er ikke et git-repository endnu. Koer med -InitIfNeeded for at initialisere det."
|
||||
}
|
||||
|
||||
Write-Info "Initialiserer git-repository paa branch '$Branch'."
|
||||
& git -C $RepositoryRoot init -b $Branch
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git init fejlede med exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Info "Repo: $RepositoryRoot"
|
||||
Write-Info "Remote: $RemoteName -> $RemoteUrl"
|
||||
|
||||
$remoteExists = $false
|
||||
$currentRemoteUrl = ""
|
||||
|
||||
& git -C $RepositoryRoot remote get-url $RemoteName *> $null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$remoteExists = $true
|
||||
$currentRemoteUrl = (& git -C $RepositoryRoot remote get-url $RemoteName).Trim()
|
||||
}
|
||||
|
||||
if ($SetOrigin) {
|
||||
if ($remoteExists) {
|
||||
if ($currentRemoteUrl -ne $RemoteUrl) {
|
||||
Write-Info "Opdaterer remote '$RemoteName'."
|
||||
Invoke-Git -Arguments @("remote", "set-url", $RemoteName, $RemoteUrl) | Out-Null
|
||||
} else {
|
||||
Write-Info "Remote '$RemoteName' peger allerede paa den oenskede URL."
|
||||
}
|
||||
} else {
|
||||
Write-Info "Tilfoejer remote '$RemoteName'."
|
||||
Invoke-Git -Arguments @("remote", "add", $RemoteName, $RemoteUrl) | Out-Null
|
||||
$remoteExists = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($StageAll) {
|
||||
Write-Info "Stager alle aendringer."
|
||||
Write-Info ("Ekskluderer fra staging: " + ($StageExclude -join ", "))
|
||||
|
||||
Invoke-Git -Arguments @("add", "--update", "--", ".") | Out-Null
|
||||
|
||||
$untrackedFiles = & git -C $RepositoryRoot ls-files --others --exclude-standard --full-name
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Kunne ikke hente untracked filer fra git."
|
||||
}
|
||||
|
||||
$filesToStage = @()
|
||||
foreach ($file in $untrackedFiles) {
|
||||
if ([string]::IsNullOrWhiteSpace($file)) {
|
||||
continue
|
||||
}
|
||||
if (-not (Test-IsExcludedPath -Path $file -Patterns $StageExclude)) {
|
||||
$filesToStage += $file.Trim()
|
||||
}
|
||||
}
|
||||
|
||||
if ($filesToStage.Count -gt 0) {
|
||||
Invoke-Git -Arguments (@("add", "--") + $filesToStage) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
if ($Commit) {
|
||||
Write-Info "Opretter commit."
|
||||
& git -C $RepositoryRoot diff --cached --quiet
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
throw "Der er ingen staged aendringer at committe."
|
||||
}
|
||||
Invoke-Git -Arguments @("commit", "-m", $CommitMessage) | Out-Null
|
||||
}
|
||||
|
||||
$currentBranch = (& git -C $RepositoryRoot branch --show-current).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($currentBranch)) {
|
||||
$currentBranch = $Branch
|
||||
}
|
||||
|
||||
if ($Push) {
|
||||
if (-not $remoteExists) {
|
||||
throw "Remote '$RemoteName' findes ikke endnu. Koer med -SetOrigin foerst."
|
||||
}
|
||||
|
||||
Write-Info "Pusher '$currentBranch' til '$RemoteName/$Branch'."
|
||||
Invoke-Git -Arguments @("push", "-u", $RemoteName, "$currentBranch`:$Branch") | Out-Null
|
||||
}
|
||||
|
||||
Write-Info "Git status:"
|
||||
Invoke-Git -Arguments @("status", "--short") -IgnoreFailure | Out-Null
|
||||
|
||||
Write-Info "Faerdig."
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0sync-windows-deploy.ps1" %*
|
||||
endlocal
|
||||
@@ -0,0 +1,140 @@
|
||||
param(
|
||||
[string]$SourceRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path,
|
||||
[string]$DestinationRoot = "Q:\home\thomas\TuxDMXWebUI",
|
||||
[string]$StageRoot = (Join-Path $env:USERPROFILE "Desktop\TuxDMXWebUI-update"),
|
||||
[switch]$PackageOnly,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Info {
|
||||
param([string]$Message)
|
||||
Write-Host "[TuxDMX] $Message"
|
||||
}
|
||||
|
||||
function Invoke-Robocopy {
|
||||
param(
|
||||
[string]$Source,
|
||||
[string]$Destination,
|
||||
[string[]]$ExtraArguments = @()
|
||||
)
|
||||
|
||||
$arguments = @(
|
||||
$Source,
|
||||
$Destination,
|
||||
"/E",
|
||||
"/R:2",
|
||||
"/W:1",
|
||||
"/FFT",
|
||||
"/XJ",
|
||||
"/NP",
|
||||
"/NDL"
|
||||
) + $ExtraArguments
|
||||
|
||||
if ($DryRun) {
|
||||
$arguments += "/L"
|
||||
}
|
||||
|
||||
Write-Info ("robocopy " + ($arguments -join " "))
|
||||
& robocopy @arguments
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
if ($exitCode -ge 8) {
|
||||
throw "Robocopy fejlede med exit code $exitCode."
|
||||
}
|
||||
}
|
||||
|
||||
function Reset-Directory {
|
||||
param([string]$Path)
|
||||
|
||||
if (Test-Path $Path) {
|
||||
Remove-Item -LiteralPath $Path -Recurse -Force
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $Path | Out-Null
|
||||
}
|
||||
|
||||
$resolvedSource = (Resolve-Path $SourceRoot).Path
|
||||
|
||||
if (-not (Test-Path $resolvedSource)) {
|
||||
throw "Kildemappen findes ikke: $SourceRoot"
|
||||
}
|
||||
|
||||
$excludedDirectories = @(
|
||||
".git",
|
||||
".github",
|
||||
".venv",
|
||||
".mypy_cache",
|
||||
".pnpm-store",
|
||||
".pytest_cache",
|
||||
".ruff_cache",
|
||||
"node_modules",
|
||||
"data",
|
||||
"__pycache__"
|
||||
)
|
||||
|
||||
$excludedFiles = @(
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.log"
|
||||
)
|
||||
|
||||
$extraArguments = @()
|
||||
|
||||
if ($excludedDirectories.Count -gt 0) {
|
||||
$extraArguments += "/XD"
|
||||
$extraArguments += $excludedDirectories
|
||||
}
|
||||
|
||||
if ($excludedFiles.Count -gt 0) {
|
||||
$extraArguments += "/XF"
|
||||
$extraArguments += $excludedFiles
|
||||
}
|
||||
|
||||
Write-Info "Kilde: $resolvedSource"
|
||||
Write-Info "Maal: $DestinationRoot"
|
||||
Write-Info "Ekskluderer data, dependencies, caches og git-metadata."
|
||||
Write-Info "Fallback-pakke: $StageRoot"
|
||||
|
||||
$syncFailed = $false
|
||||
|
||||
if (-not $PackageOnly) {
|
||||
if (-not (Test-Path $DestinationRoot)) {
|
||||
Write-Info "Maalmappe findes ikke, opretter: $DestinationRoot"
|
||||
New-Item -ItemType Directory -Force -Path $DestinationRoot | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-Robocopy -Source $resolvedSource -Destination $DestinationRoot -ExtraArguments $extraArguments
|
||||
} catch {
|
||||
$syncFailed = $true
|
||||
Write-Info "Direkte sync til share fejlede: $($_.Exception.Message)"
|
||||
Write-Info "Bygger i stedet en lokal update-pakke med de deploy-klare filer."
|
||||
}
|
||||
}
|
||||
|
||||
$distSource = Join-Path $resolvedSource "frontend\dist"
|
||||
if (-not (Test-Path $distSource)) {
|
||||
Write-Info "Advarsel: frontend/dist findes ikke endnu. Koer frontend build foer deployment, hvis UI-assets skal med."
|
||||
}
|
||||
|
||||
if ($PackageOnly -or $syncFailed) {
|
||||
if (-not $DryRun) {
|
||||
Reset-Directory -Path $StageRoot
|
||||
}
|
||||
|
||||
Invoke-Robocopy -Source $resolvedSource -Destination $StageRoot -ExtraArguments $extraArguments
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Info "Dry-run af lokal update-pakke afsluttet uden at skrive filer."
|
||||
} else {
|
||||
Write-Info "Lokal update-pakke er klar: $StageRoot"
|
||||
Write-Info "Kopier indholdet herfra over paa Linux-maskinen med de rettigheder du har."
|
||||
}
|
||||
} elseif ($DryRun) {
|
||||
Write-Info "Dry-run afsluttet uden at skrive filer."
|
||||
} else {
|
||||
Write-Info "Synk afsluttet direkte til share."
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$SCRIPT_DIR/uninstall-pi.sh" "$@"
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PURGE_APP="false"
|
||||
PURGE_DATA="false"
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--purge-app) PURGE_APP="true" ;;
|
||||
--purge-data) PURGE_DATA="true" ;;
|
||||
*) echo "Ukendt argument: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Kør uninstall-pi.sh som root eller via sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
systemctl disable --now tuxdmx.service || true
|
||||
rm -f /etc/systemd/system/tuxdmx.service
|
||||
systemctl daemon-reload
|
||||
|
||||
if [[ "$PURGE_APP" == "true" ]]; then
|
||||
rm -rf /opt/tuxdmx
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_DATA" == "true" ]]; then
|
||||
rm -rf /var/lib/tuxdmx /var/log/tuxdmx /etc/tuxdmx
|
||||
fi
|
||||
|
||||
echo "TuxDMX service er fjernet."
|
||||
echo "Brug --purge-app og/eller --purge-data, hvis applikationsfiler eller data også skal slettes."
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/lib/install-common.sh"
|
||||
|
||||
parse_common_args "$@"
|
||||
verify_debian_13_amd64
|
||||
run_update_flow "Debian 13 amd64"
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/lib/install-common.sh"
|
||||
|
||||
parse_common_args "$@"
|
||||
verify_pi_platform
|
||||
run_update_flow "Raspberry Pi / ARM"
|
||||
Reference in New Issue
Block a user